Compare commits
1210 Commits
2.1.4+fork
...
merge-upst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26df5762f | ||
|
|
2021c335ac | ||
|
|
d121f14d30 | ||
|
|
d1a2a70cdc | ||
|
|
89ef482e2e | ||
|
|
9918649d7c | ||
|
|
09185faf9a | ||
|
|
bed201a2f7 | ||
|
|
5e7a4c0136 | ||
|
|
bcb8717d5f | ||
|
|
ed1c1bd097 | ||
|
|
f480532fd6 | ||
|
|
cc056cef08 | ||
|
|
9e7445b8d8 | ||
|
|
e2d96d3bc7 | ||
|
|
4f5c99be21 | ||
|
|
0388f9d9be | ||
|
|
c45128ced0 | ||
|
|
f404d2f9cd | ||
|
|
2dada69eb8 | ||
|
|
b7e0596014 | ||
|
|
dbef984908 | ||
|
|
55259f103d | ||
|
|
81519fe906 | ||
|
|
07ab3c394a | ||
|
|
620cc94351 | ||
|
|
2494918171 | ||
|
|
a0bed5e739 | ||
|
|
a42bf86a1e | ||
|
|
9c7ae9653b | ||
|
|
44473705b9 | ||
|
|
f1d40f8963 | ||
|
|
fbae5d8816 | ||
|
|
43afbb7523 | ||
|
|
080815846f | ||
|
|
4b6c6cbcfe | ||
|
|
117037e7e8 | ||
|
|
05972fc702 | ||
|
|
28084b9f9e | ||
|
|
02010df408 | ||
|
|
38f77c69d1 | ||
|
|
d0a8c26b65 | ||
|
|
401602e5bc | ||
|
|
ccd9dbed13 | ||
|
|
736d5d9f3e | ||
|
|
32451c0eea | ||
|
|
e7ed8d5590 | ||
|
|
79d04a949b | ||
|
|
5cd99b9763 | ||
|
|
3f30c2f3be | ||
|
|
db8187bbc9 | ||
|
|
4e1632aa19 | ||
|
|
a813f961af | ||
|
|
f6417662b9 | ||
|
|
2d1bc09616 | ||
|
|
d9e5ea5b80 | ||
|
|
1ab6bc3663 | ||
|
|
effe3a079f | ||
|
|
7d65563096 | ||
|
|
857c5b9a55 | ||
|
|
e49760c5a0 | ||
|
|
93b97e99a8 | ||
|
|
6d148b1f7a | ||
|
|
4d24e4e846 | ||
|
|
9f5c420e66 | ||
|
|
ca07240a70 | ||
|
|
1b6978bb93 | ||
|
|
d4b20fc5f7 | ||
|
|
d3d95c7963 | ||
|
|
98c5baecad | ||
|
|
766b7b8c45 | ||
|
|
896ded9ff3 | ||
|
|
7b31543d7a | ||
|
|
ff61c3c02e | ||
|
|
aa8562dc88 | ||
|
|
ec495750fe | ||
|
|
af33c593b5 | ||
|
|
4586e42459 | ||
|
|
2a45b7d13d | ||
|
|
60d573de58 | ||
|
|
2d7499e8cc | ||
|
|
9ec82ae090 | ||
|
|
da783c3771 | ||
|
|
9869581515 | ||
|
|
f45fb87ea5 | ||
|
|
d80ac7557e | ||
|
|
58403fef59 | ||
|
|
87ca8b1ad7 | ||
|
|
04e1f9e148 | ||
|
|
1e1fe47638 | ||
|
|
c567e264de | ||
|
|
c142f82fd1 | ||
|
|
c0cf5b40fa | ||
|
|
b45e87b271 | ||
|
|
958243e65d | ||
|
|
8cc91b0f02 | ||
|
|
0ac7d3530e | ||
|
|
10d42264c8 | ||
|
|
72fee62472 | ||
|
|
9b4528b69a | ||
|
|
4b0cf4311d | ||
|
|
4ceea9100d | ||
|
|
2522cd26d1 | ||
|
|
294bcef5f6 | ||
|
|
e61618bf2c | ||
|
|
70e5030fe1 | ||
|
|
7c270aadda | ||
|
|
30eaeb006d | ||
|
|
5e11b3fb7a | ||
|
|
d6089d0c1e | ||
|
|
1bb288e565 | ||
|
|
d42eb934d5 | ||
|
|
2fecd6f0a3 | ||
|
|
c3a2b5a6e1 | ||
|
|
ccff874bcf | ||
|
|
9e7f351174 | ||
|
|
a9e7fab029 | ||
|
|
aad8abd3bf | ||
|
|
d938c8c470 | ||
|
|
124ad8cb0e | ||
|
|
a17c3293b5 | ||
|
|
5868da3337 | ||
|
|
731ee17d6d | ||
|
|
edddc297dd | ||
|
|
85152102fd | ||
|
|
fba4c1c6d6 | ||
|
|
593e8d0eb7 | ||
|
|
bafb1ba8f8 | ||
|
|
36124db2aa | ||
|
|
155a093eb7 | ||
|
|
ddee29bf03 | ||
|
|
99e2958649 | ||
|
|
519afb6259 | ||
|
|
6ab8991c45 | ||
|
|
44200a4d56 | ||
|
|
e929478b6a | ||
|
|
cf98aa4939 | ||
|
|
22585a2ec5 | ||
|
|
fa6abd44c3 | ||
|
|
1d7cbcc4e1 | ||
|
|
5edbe9b826 | ||
|
|
b5027ee66f | ||
|
|
499baeb496 | ||
|
|
72d486e992 | ||
|
|
3020c826ed | ||
|
|
34f3e33efc | ||
|
|
5b25168eb7 | ||
|
|
c785bbb2d7 | ||
|
|
45324a5598 | ||
|
|
55ad624209 | ||
|
|
ed0fe1e803 | ||
|
|
18079454a9 | ||
|
|
87cb80867a | ||
|
|
1829dc1d9d | ||
|
|
519cb672d2 | ||
|
|
e0a5e259f7 | ||
|
|
86512e237e | ||
|
|
b9efdbbb40 | ||
|
|
d369129ac7 | ||
|
|
c01135d822 | ||
|
|
653a66bd87 | ||
|
|
ffc2990b32 | ||
|
|
8b26fb3184 | ||
|
|
3fec39835c | ||
|
|
5402e78342 | ||
|
|
8995cfcc9d | ||
|
|
8d3b1f40a3 | ||
|
|
f775bae93e | ||
|
|
ca84bc36e3 | ||
|
|
2a775aba70 | ||
|
|
7cd65dcb32 | ||
|
|
4d694b2725 | ||
|
|
2e39f81c36 | ||
|
|
803e66f999 | ||
|
|
ed22d3b4ed | ||
|
|
ec72653dba | ||
|
|
9b1e79eba8 | ||
|
|
ca4a1d461a | ||
|
|
b90607582a | ||
|
|
0c95f6db1b | ||
|
|
4caa6cf650 | ||
|
|
bc08c149b7 | ||
|
|
4a783957ed | ||
|
|
113b47d9e2 | ||
|
|
96ccb14a59 | ||
|
|
bc8b0e192c | ||
|
|
72400703ab | ||
|
|
91345268e8 | ||
|
|
bff6ac4a14 | ||
|
|
75183f5625 | ||
|
|
7654b869ba | ||
|
|
f176384bcc | ||
|
|
a4f2a733b5 | ||
|
|
9ea48fa0ab | ||
|
|
cc2076ec10 | ||
|
|
b5a0c293c5 | ||
|
|
3265cfe772 | ||
|
|
857d0ce539 | ||
|
|
31a52c2790 | ||
|
|
94ce329f49 | ||
|
|
a67c8b36b1 | ||
|
|
ff90e21e86 | ||
|
|
5fd2e322f6 | ||
|
|
cdd9b0553f | ||
|
|
6157d4942a | ||
|
|
e68e870a7c | ||
|
|
0788b03828 | ||
|
|
b670da04ed | ||
|
|
f70abbbb73 | ||
|
|
a0dd75890c | ||
|
|
38df70cd9e | ||
|
|
e18fa57d73 | ||
|
|
51f6264534 | ||
|
|
feff45721f | ||
|
|
20558f0a19 | ||
|
|
e97a479e65 | ||
|
|
f590fde7a4 | ||
|
|
77c5173014 | ||
|
|
dd4bed0027 | ||
|
|
229c0b359f | ||
|
|
0d4158a612 | ||
|
|
cfde4425b7 | ||
|
|
15f84af757 | ||
|
|
39895ff79a | ||
|
|
3d2b67efc5 | ||
|
|
ebd637546f | ||
|
|
618946a8c6 | ||
|
|
e8ce2a7e35 | ||
|
|
f8dbecc3e1 | ||
|
|
76030c041c | ||
|
|
998e186f8b | ||
|
|
75bc0aa052 | ||
|
|
edb4b7152b | ||
|
|
66c9e0d908 | ||
|
|
0bdb23e462 | ||
|
|
d9ce0e6d31 | ||
|
|
aa3c8b5812 | ||
|
|
4392ce20b6 | ||
|
|
d5085c5899 | ||
|
|
9a1668a29a | ||
|
|
4d598bd2fe | ||
|
|
57911ce070 | ||
|
|
f9f8c4a9ef | ||
|
|
6ad8a85044 | ||
|
|
14e6187efc | ||
|
|
bd88606c48 | ||
|
|
b38c78c50a | ||
|
|
4c9f7fc8be | ||
|
|
4f11a79d2a | ||
|
|
7ab920d943 | ||
|
|
c8f2e7a752 | ||
|
|
cdcc428e86 | ||
|
|
7bb5584dd9 | ||
|
|
0c5c51dc17 | ||
|
|
b17b7afd03 | ||
|
|
e2e8173db6 | ||
|
|
5e7f4bda82 | ||
|
|
38996d8921 | ||
|
|
6cb8961639 | ||
|
|
18ac0423c0 | ||
|
|
d2704c1f0d | ||
|
|
ed23b7cc13 | ||
|
|
47ab6b5a08 | ||
|
|
70686bbbd0 | ||
|
|
b53997261e | ||
|
|
efd9b1e916 | ||
|
|
b51033a421 | ||
|
|
e0a793e176 | ||
|
|
542c24ff75 | ||
|
|
965f7c6d1d | ||
|
|
2df6d9ce60 | ||
|
|
5d3afc1b0e | ||
|
|
0c8f903eb6 | ||
|
|
ef23734b22 | ||
|
|
c0ab3a47ae | ||
|
|
f4a94bc42e | ||
|
|
69b95c27ec | ||
|
|
c64d6db859 | ||
|
|
730adc34dd | ||
|
|
a082a3d325 | ||
|
|
c7820ddac8 | ||
|
|
169fbc2d52 | ||
|
|
44e3e5faaf | ||
|
|
711c70af2f | ||
|
|
1d405d9e48 | ||
|
|
892ce130ca | ||
|
|
fea9d6e761 | ||
|
|
88e11f25a7 | ||
|
|
6faa497569 | ||
|
|
1d45899f8c | ||
|
|
938643f9e2 | ||
|
|
1ccf9bf4b7 | ||
|
|
ad9b5f028d | ||
|
|
e52154fd17 | ||
|
|
54202f3e8d | ||
|
|
d4b8c350dc | ||
|
|
daaf467168 | ||
|
|
eda52d5a55 | ||
|
|
0700274d6b | ||
|
|
faee3e3dd6 | ||
|
|
129ce09c9f | ||
|
|
368e226257 | ||
|
|
93321720e1 | ||
|
|
96c1c036a8 | ||
|
|
edffe0fd42 | ||
|
|
d1d8f2ef45 | ||
|
|
95ba52b761 | ||
|
|
02c8a56c17 | ||
|
|
b34a855150 | ||
|
|
b736cf2925 | ||
|
|
eea78302ab | ||
|
|
09a7da2952 | ||
|
|
ebf3b075b8 | ||
|
|
28c851a630 | ||
|
|
44194e5d43 | ||
|
|
58bb492461 | ||
|
|
00726abec1 | ||
|
|
c9e93bb6a6 | ||
|
|
f980bba7cd | ||
|
|
d87d656002 | ||
|
|
9e9061e29c | ||
|
|
70d0ba88e4 | ||
|
|
9cb48e2f25 | ||
|
|
6f89dd7331 | ||
|
|
a59c20d239 | ||
|
|
3e36a72852 | ||
|
|
7801d28a23 | ||
|
|
c2e6c802a1 | ||
|
|
2dbfe88397 | ||
|
|
60f0a3d5ee | ||
|
|
86506f9ba4 | ||
|
|
7269788831 | ||
|
|
f7d0bda90f | ||
|
|
eea350f84e | ||
|
|
44bec713ae | ||
|
|
d5cd016776 | ||
|
|
849172ad7f | ||
|
|
de57ceb939 | ||
|
|
bf36f12f79 | ||
|
|
e9684441c0 | ||
|
|
aa4bd9d5a3 | ||
|
|
86f82f88da | ||
|
|
fb5c8504ab | ||
|
|
cff824e745 | ||
|
|
200583f492 | ||
|
|
49e8083c9a | ||
|
|
15fe3595a3 | ||
|
|
dd7e9be803 | ||
|
|
8ce7987591 | ||
|
|
0b7f5f7f64 | ||
|
|
db7d223be6 | ||
|
|
cabbc934ec | ||
|
|
c277a13d39 | ||
|
|
404de46eee | ||
|
|
b5f062ca99 | ||
|
|
6d1ef8b59d | ||
|
|
83ced914ef | ||
|
|
08a468c56f | ||
|
|
66cb74249e | ||
|
|
2d10387404 | ||
|
|
60de667feb | ||
|
|
d01fa27f85 | ||
|
|
56560e741c | ||
|
|
3d57954866 | ||
|
|
dab3ad79ed | ||
|
|
0d9ac99313 | ||
|
|
2883881402 | ||
|
|
760cfa0b9f | ||
|
|
ceb89e4dd7 | ||
|
|
f3c9b4e2cf | ||
|
|
cc07c539e0 | ||
|
|
57ff23e8c1 | ||
|
|
bfe6559690 | ||
|
|
138e633acd | ||
|
|
68ab66bed7 | ||
|
|
f22de651c9 | ||
|
|
5a46ae8d7b | ||
|
|
297c37a72e | ||
|
|
a6f654cb36 | ||
|
|
f6a844138b | ||
|
|
2d857f4f1b | ||
|
|
915f2ca108 | ||
|
|
cd49b5f0b4 | ||
|
|
7d22f93b5d | ||
|
|
da74ae0a7c | ||
|
|
d67ab02443 | ||
|
|
c821326326 | ||
|
|
464c80ed8f | ||
|
|
4adf250645 | ||
|
|
190755d2cc | ||
|
|
6deae2341b | ||
|
|
a23b39255d | ||
|
|
20378cc1c5 | ||
|
|
0c5f7552e0 | ||
|
|
99e382c9f8 | ||
|
|
ec332cb867 | ||
|
|
4e3ddaf782 | ||
|
|
4a4fec06f6 | ||
|
|
43d4978b1b | ||
|
|
7fbfae7388 | ||
|
|
1772655c8c | ||
|
|
6631de85e3 | ||
|
|
a19a39ff51 | ||
|
|
89f96cba52 | ||
|
|
dae32e2645 | ||
|
|
3af3993e1d | ||
|
|
9cd0b90c9a | ||
|
|
798325339f | ||
|
|
d17e25e7eb | ||
|
|
73f15caae2 | ||
|
|
76c64195b0 | ||
|
|
e1315eafde | ||
|
|
1c9b7b8cbe | ||
|
|
f9844a7337 | ||
|
|
4cb0eed126 | ||
|
|
bfab791850 | ||
|
|
a2767d6002 | ||
|
|
62a65ab4c6 | ||
|
|
04d6ca9485 | ||
|
|
36620d7415 | ||
|
|
d5e9697420 | ||
|
|
e5da0681ae | ||
|
|
863ff48bd4 | ||
|
|
a9948ee996 | ||
|
|
fba231c855 | ||
|
|
09911e4494 | ||
|
|
ff2407239f | ||
|
|
4d990647f3 | ||
|
|
5ac91605ed | ||
|
|
4a8a3530ab | ||
|
|
e8a600ff29 | ||
|
|
6df1cffc55 | ||
|
|
5826b17840 | ||
|
|
fdc052f8f1 | ||
|
|
a59774fe78 | ||
|
|
19bf9c4ee6 | ||
|
|
2139dbd76b | ||
|
|
aab829ac4d | ||
|
|
d047379785 | ||
|
|
ff9587661e | ||
|
|
e6a4f81b78 | ||
|
|
ad92a08271 | ||
|
|
038923bf8f | ||
|
|
b0518b807e | ||
|
|
2c743b11e6 | ||
|
|
6c519b3cb9 | ||
|
|
5ed4fdbb36 | ||
|
|
a844852669 | ||
|
|
4f3d711d2b | ||
|
|
24be1c68ad | ||
|
|
fe96ee5f2a | ||
|
|
e6501ad8a3 | ||
|
|
18f8c1d29e | ||
|
|
56540d7e7b | ||
|
|
37f63b1142 | ||
|
|
da3c19cdd0 | ||
|
|
9a28d23be0 | ||
|
|
51a9c7c58e | ||
|
|
8e00b04f71 | ||
|
|
d08c64e749 | ||
|
|
974774a913 | ||
|
|
608ac60aec | ||
|
|
a4586f2c0c | ||
|
|
1da2df220b | ||
|
|
13ed669423 | ||
|
|
0046fa5d9e | ||
|
|
0931702802 | ||
|
|
d7a229afeb | ||
|
|
14f260d6a1 | ||
|
|
2ab74de236 | ||
|
|
7a4387a459 | ||
|
|
629262c266 | ||
|
|
a7c558afd8 | ||
|
|
8d6593e12c | ||
|
|
72cf5c3b59 | ||
|
|
b018b817e8 | ||
|
|
bf0c80d6b9 | ||
|
|
daa391a877 | ||
|
|
1fedfce00d | ||
|
|
78805116f8 | ||
|
|
98a9966461 | ||
|
|
2d127daf67 | ||
|
|
2497ece9f1 | ||
|
|
44b9f8c0ec | ||
|
|
6ae234cf42 | ||
|
|
39f3e72a47 | ||
|
|
b1e2cab5fa | ||
|
|
75da570682 | ||
|
|
e08718fc08 | ||
|
|
4ebe4434f2 | ||
|
|
5cc9ecc8e0 | ||
|
|
e943ac1a05 | ||
|
|
c439171a08 | ||
|
|
a9c6bf24fd | ||
|
|
ad4423526d | ||
|
|
445bf28e94 | ||
|
|
f33edfa70f | ||
|
|
e694715dae | ||
|
|
8f69358d34 | ||
|
|
f425a73c74 | ||
|
|
1b1138242b | ||
|
|
5fae68f68f | ||
|
|
0100ecff9b | ||
|
|
8651db933a | ||
|
|
5082930a80 | ||
|
|
cc8a81aa31 | ||
|
|
ccb22802da | ||
|
|
c55f2056c2 | ||
|
|
6f5af40f2a | ||
|
|
2fb879bd0d | ||
|
|
a95c0058d8 | ||
|
|
b1b03a3ada | ||
|
|
6b5344bd11 | ||
|
|
4d91ff3866 | ||
|
|
a488776daa | ||
|
|
740ff45bb6 | ||
|
|
7773c08387 | ||
|
|
4a13398801 | ||
|
|
685e6e947f | ||
|
|
1dd46df540 | ||
|
|
c73fa2dd6b | ||
|
|
ad1de60968 | ||
|
|
d8a88d1803 | ||
|
|
f29a9b6748 | ||
|
|
60be7191aa | ||
|
|
307f886ea5 | ||
|
|
47473a6372 | ||
|
|
25f9e60527 | ||
|
|
a7b51095fb | ||
|
|
75f14371d5 | ||
|
|
7c8d0cb459 | ||
|
|
14a36d2602 | ||
|
|
3939fc9795 | ||
|
|
06a26d308f | ||
|
|
d1656a525e | ||
|
|
b0dc521b90 | ||
|
|
732de52ebb | ||
|
|
e9b6acb92d | ||
|
|
75e8d738a4 | ||
|
|
ec02aed557 | ||
|
|
20d0acc88c | ||
|
|
71dd974d19 | ||
|
|
0d7e10ca8a | ||
|
|
fa31b0f2d6 | ||
|
|
a91e08fe1a | ||
|
|
3ea8b370bb | ||
|
|
02eb178443 | ||
|
|
e8b77d0dfa | ||
|
|
f880d4b7b6 | ||
|
|
f5514e35e1 | ||
|
|
193ef88814 | ||
|
|
7de58a5e35 | ||
|
|
633261c16b | ||
|
|
e295c8c381 | ||
|
|
e71232ad40 | ||
|
|
123ed6c56d | ||
|
|
cf31a1be57 | ||
|
|
64e82bdeed | ||
|
|
76867a971b | ||
|
|
0af8dbf09b | ||
|
|
8dffbff97c | ||
|
|
efb8cd565b | ||
|
|
1f5bdb975b | ||
|
|
22dfc33974 | ||
|
|
2071602607 | ||
|
|
72940832a6 | ||
|
|
8e984a7cad | ||
|
|
34b2a4e2a0 | ||
|
|
2291c2bb28 | ||
|
|
6915d19fb4 | ||
|
|
ad2ef39ace | ||
|
|
3cff655e6f | ||
|
|
ed86a5a3e8 | ||
|
|
f329435f51 | ||
|
|
c8604ad68e | ||
|
|
6a6a80bcd7 | ||
|
|
62e4983f02 | ||
|
|
6dfd991e87 | ||
|
|
e205462bf4 | ||
|
|
03f341f6f8 | ||
|
|
b9b08c5ea7 | ||
|
|
2b5498ff5d | ||
|
|
84b058873d | ||
|
|
fcf5c0822e | ||
|
|
53c3da6a3d | ||
|
|
68371c9a0f | ||
|
|
e7295aac07 | ||
|
|
ae7f65954a | ||
|
|
350a73c3eb | ||
|
|
7581a6cf7e | ||
|
|
85b135fa34 | ||
|
|
a195aa56ca | ||
|
|
b694329bda | ||
|
|
79362b59c4 | ||
|
|
fa51b6acb2 | ||
|
|
0aa277ee72 | ||
|
|
d111be7293 | ||
|
|
dd06292b53 | ||
|
|
98953e8afa | ||
|
|
8a8cefe68a | ||
|
|
bbd72e299b | ||
|
|
24b227f665 | ||
|
|
71118b1f84 | ||
|
|
095e1543fe | ||
|
|
7bdd9811da | ||
|
|
c086900ae6 | ||
|
|
143df89855 | ||
|
|
b0da6897a0 | ||
|
|
b798bbf234 | ||
|
|
93ba515a29 | ||
|
|
df5bfa77c6 | ||
|
|
797293fc4b | ||
|
|
66d8ba9b5d | ||
|
|
f944b12f45 | ||
|
|
61928a1cf0 | ||
|
|
f06196802e | ||
|
|
e162833ad7 | ||
|
|
936ffdc793 | ||
|
|
0bbf6abc0c | ||
|
|
5552dc2ac6 | ||
|
|
a65d6fbeb3 | ||
|
|
43612ffbc1 | ||
|
|
971881bbd3 | ||
|
|
390cc6b65d | ||
|
|
ee31288769 | ||
|
|
401986af29 | ||
|
|
e41e89c5cd | ||
|
|
53de0cfc63 | ||
|
|
e68481395f | ||
|
|
2c86356389 | ||
|
|
9a361e0688 | ||
|
|
b8cce74824 | ||
|
|
f1ad6fc511 | ||
|
|
2aa4cc1a88 | ||
|
|
fb17ba4777 | ||
|
|
6e3c464c97 | ||
|
|
640e5163a8 | ||
|
|
fdd3f2f398 | ||
|
|
dfc55a13b8 | ||
|
|
5a83b79ac2 | ||
|
|
7d954ab3c2 | ||
|
|
ec0b830f4f | ||
|
|
26256b67d3 | ||
|
|
2f9d60b9c0 | ||
|
|
499a325bc8 | ||
|
|
a1a4c59b83 | ||
|
|
97ca2634a0 | ||
|
|
6630f0f8da | ||
|
|
129b253176 | ||
|
|
c2382d065e | ||
|
|
38f74c96bf | ||
|
|
085264755a | ||
|
|
baac955e52 | ||
|
|
4a0501209a | ||
|
|
e471b36d39 | ||
|
|
5c2961cf7c | ||
|
|
6e980f17c6 | ||
|
|
2860ce8755 | ||
|
|
ca25a868a0 | ||
|
|
74f3bd5905 | ||
|
|
e0a53b4296 | ||
|
|
c20f043f38 | ||
|
|
daf3005178 | ||
|
|
d17e24faae | ||
|
|
0cd17accf9 | ||
|
|
65f7b97e60 | ||
|
|
c7324285f3 | ||
|
|
6bc795ebea | ||
|
|
f2616cdd58 | ||
|
|
d50f65ffd8 | ||
|
|
b39b2d0544 | ||
|
|
cdaaa91bcc | ||
|
|
109dca0b8a | ||
|
|
ee87da564b | ||
|
|
b143559a0f | ||
|
|
9b89727c80 | ||
|
|
68a252c85c | ||
|
|
d99cb91e89 | ||
|
|
38879cd2fe | ||
|
|
af4d98a48b | ||
|
|
39bb93d650 | ||
|
|
0a3568f424 | ||
|
|
e0b45720f0 | ||
|
|
f5b7024bb5 | ||
|
|
f1bfa1f598 | ||
|
|
653304f9a4 | ||
|
|
3d416a038a | ||
|
|
e0eeb87182 | ||
|
|
2570a86da9 | ||
|
|
7b110f16b3 | ||
|
|
d170e87325 | ||
|
|
4a60f0c576 | ||
|
|
4b5e9d604c | ||
|
|
f9562d5087 | ||
|
|
786091c0a4 | ||
|
|
436b8240ef | ||
|
|
e7253dcf97 | ||
|
|
6815cd77e4 | ||
|
|
4f9a1db26b | ||
|
|
d3bcf9d8ee | ||
|
|
48f9aabaf7 | ||
|
|
14d353ae27 | ||
|
|
9a82846b84 | ||
|
|
a4c9bbadc4 | ||
|
|
35d39b63e2 | ||
|
|
15c77e4220 | ||
|
|
fa70c55084 | ||
|
|
962c094f7e | ||
|
|
c6081fb4d4 | ||
|
|
1832de3aab | ||
|
|
8d0a89fb06 | ||
|
|
3caf6cb94c | ||
|
|
5c15914bab | ||
|
|
9f29d72212 | ||
|
|
f4854061ea | ||
|
|
bf7607674e | ||
|
|
9786e324b7 | ||
|
|
a7fef67d48 | ||
|
|
30726cb364 | ||
|
|
e0ff1f6725 | ||
|
|
7e244d65bf | ||
|
|
9c8e6647bc | ||
|
|
d8cc578537 | ||
|
|
f9f863ea5e | ||
|
|
2efb79f6cb | ||
|
|
d534557915 | ||
|
|
68ebc1aa93 | ||
|
|
251d90e0ec | ||
|
|
ccd313533b | ||
|
|
f70ea97d9e | ||
|
|
3c9efdbbf2 | ||
|
|
3bd67f9ab1 | ||
|
|
42946eca44 | ||
|
|
b2d502ae79 | ||
|
|
4d9f625ff4 | ||
|
|
18e3fadb26 | ||
|
|
0364d95300 | ||
|
|
d6c05f0850 | ||
|
|
137a8ca27b | ||
|
|
b9ed4e0ee2 | ||
|
|
bc04672d32 | ||
|
|
70c668ecf1 | ||
|
|
5cc94fa2b0 | ||
|
|
64bbe2c438 | ||
|
|
08d4c135ea | ||
|
|
6220a20734 | ||
|
|
f1c55aa5e8 | ||
|
|
32209e766e | ||
|
|
99349cff0a | ||
|
|
74ca1961e0 | ||
|
|
ef6ba7fe0c | ||
|
|
9ace2b71cc | ||
|
|
0c54654b8b | ||
|
|
bf686309fb | ||
|
|
ce4f46537b | ||
|
|
4c43207f17 | ||
|
|
b8fe1fd640 | ||
|
|
928506b360 | ||
|
|
167a2e1e2f | ||
|
|
afe5bcd1f3 | ||
|
|
3bda81bd43 | ||
|
|
7339b2325f | ||
|
|
ee84a9ee7e | ||
|
|
fef594150a | ||
|
|
10371f69cb | ||
|
|
75cf3d76fb | ||
|
|
51a7d00c47 | ||
|
|
9ac8261cc4 | ||
|
|
1f4ad80b7d | ||
|
|
4b090f0d68 | ||
|
|
4002bcde26 | ||
|
|
ded3777b40 | ||
|
|
7236066003 | ||
|
|
033f07ea09 | ||
|
|
283c0cba4b | ||
|
|
e3a1fc2fbb | ||
|
|
95de9e2917 | ||
|
|
a82ebeed11 | ||
|
|
3a3aa0be1c | ||
|
|
e72491c2d1 | ||
|
|
36dede1f93 | ||
|
|
ed15daf9e9 | ||
|
|
c6052c841d | ||
|
|
ce39c7ca8f | ||
|
|
b7723dcb98 | ||
|
|
ad0774f8a5 | ||
|
|
9172feb72b | ||
|
|
a297bd3281 | ||
|
|
e713a9cfc3 | ||
|
|
195395a22d | ||
|
|
7b6a62b047 | ||
|
|
ada1c9ff6d | ||
|
|
5a0a14ed56 | ||
|
|
cad3879646 | ||
|
|
5d961991d4 | ||
|
|
e27536743f | ||
|
|
9f8d4a0f34 | ||
|
|
67b6a89fd9 | ||
|
|
dabc4058ba | ||
|
|
6c468602c6 | ||
|
|
9c5d29a860 | ||
|
|
da5e2a6b50 | ||
|
|
a194569fd4 | ||
|
|
78a4ace9b2 | ||
|
|
9a664088cd | ||
|
|
1d2e6f880b | ||
|
|
2cd2918d53 | ||
|
|
9b49db6677 | ||
|
|
9f6c61e5c0 | ||
|
|
b6a2bb7881 | ||
|
|
62262010b9 | ||
|
|
72fe9a04a6 | ||
|
|
d8cf55ae21 | ||
|
|
4d128b4408 | ||
|
|
dfb393b934 | ||
|
|
cd27716f6a | ||
|
|
469553b34e | ||
|
|
5d7c37262e | ||
|
|
3f3867473f | ||
|
|
b08cd1eb4b | ||
|
|
e0098efe32 | ||
|
|
1f9ff8d341 | ||
|
|
528b362f64 | ||
|
|
1db10c5047 | ||
|
|
f295f5f4e7 | ||
|
|
08924bd9b0 | ||
|
|
5d432435a1 | ||
|
|
8bd76aa833 | ||
|
|
2147cb87ac | ||
|
|
00ed0f5402 | ||
|
|
870f79f6cd | ||
|
|
da879213fc | ||
|
|
db66974bd6 | ||
|
|
e3d5ae1d65 | ||
|
|
42f5975f6b | ||
|
|
1045593cc9 | ||
|
|
3443b80ff7 | ||
|
|
9fe6b3457a | ||
|
|
0a26380f23 | ||
|
|
b06bc5b3b7 | ||
|
|
a4c988012d | ||
|
|
a200701e4c | ||
|
|
e8f604792c | ||
|
|
c8b0666ef9 | ||
|
|
13aa72b150 | ||
|
|
6694074b18 | ||
|
|
63aa32c636 | ||
|
|
5fbab870c3 | ||
|
|
4a34e248e0 | ||
|
|
2c45165e53 | ||
|
|
3f029ac45b | ||
|
|
a4cf76d5ba | ||
|
|
3044000cf8 | ||
|
|
ab1ef5cfd8 | ||
|
|
16b91a283a | ||
|
|
e9fbdc21fa | ||
|
|
b429e662aa | ||
|
|
834ad1736e | ||
|
|
91021699d2 | ||
|
|
0f86aa12ab | ||
|
|
fb7bf6f308 | ||
|
|
5aa67aaa78 | ||
|
|
2e892e7305 | ||
|
|
6486a1689f | ||
|
|
5966535111 | ||
|
|
a2cf4bda99 | ||
|
|
7a93c8615d | ||
|
|
cf29f11cea | ||
|
|
23188a26d7 | ||
|
|
0480dc0140 | ||
|
|
cb14b29c78 | ||
|
|
bf68272de3 | ||
|
|
730f5f8cc9 | ||
|
|
4b6d328e3d | ||
|
|
cfde38be2d | ||
|
|
a2ea8e76fb | ||
|
|
e797d8a1c2 | ||
|
|
b58c157c87 | ||
|
|
58f746a285 | ||
|
|
a6bba42a49 | ||
|
|
519d6868b2 | ||
|
|
5322120097 | ||
|
|
2c88c86480 | ||
|
|
55f32fd45b | ||
|
|
f39f0b03d1 | ||
|
|
ef3605c8e3 | ||
|
|
3df20c4749 | ||
|
|
c63e87de45 | ||
|
|
1151e41846 | ||
|
|
09668d2500 | ||
|
|
773a24af2c | ||
|
|
b1f6409c8d | ||
|
|
ee8e535e58 | ||
|
|
d128f29bbc | ||
|
|
ff2f1a4955 | ||
|
|
b283e216a7 | ||
|
|
4328d568b3 | ||
|
|
8edc47703f | ||
|
|
92ce906163 | ||
|
|
6e141e360e | ||
|
|
d1aba87e13 | ||
|
|
723853079e | ||
|
|
cd0742c093 | ||
|
|
52d5de5aec | ||
|
|
4f8a5ae5db | ||
|
|
616f2463c7 | ||
|
|
d3b711a966 | ||
|
|
827fe34709 | ||
|
|
4b6c0242d5 | ||
|
|
c3cbc16084 | ||
|
|
36493bfc88 | ||
|
|
66ce93a3ff | ||
|
|
957bc76dbb | ||
|
|
1f5a28fb33 | ||
|
|
045c58ce66 | ||
|
|
e2dde7239f | ||
|
|
512ad93eea | ||
|
|
19759023a4 | ||
|
|
714d3399ce | ||
|
|
e1850e5282 | ||
|
|
a05c917b2c | ||
|
|
8f3a9c265c | ||
|
|
6f1a33b76e | ||
|
|
8f0451175f | ||
|
|
37a3a4f1c0 | ||
|
|
bd85746726 | ||
|
|
96265010bf | ||
|
|
4209951ce3 | ||
|
|
f1cbd95439 | ||
|
|
d63382c6d9 | ||
|
|
20697fb334 | ||
|
|
1090d1ca42 | ||
|
|
bec4acdf51 | ||
|
|
800b78bfd8 | ||
|
|
52b01b7bbe | ||
|
|
8b71764207 | ||
|
|
a5d7a75f32 | ||
|
|
8839bcb7aa | ||
|
|
bcaf71760d | ||
|
|
1d95204648 | ||
|
|
83bf2a808f | ||
|
|
7ffb0a01c6 | ||
|
|
0710113148 | ||
|
|
7c43e9a1af | ||
|
|
f9e768c378 | ||
|
|
2063dbd0b0 | ||
|
|
057683c72f | ||
|
|
4963a0e722 | ||
|
|
1d66d288bd | ||
|
|
b8f49157c3 | ||
|
|
d77a62ef6f | ||
|
|
c531150483 | ||
|
|
991f41c531 | ||
|
|
b5fb7dd2ec | ||
|
|
4fe8532971 | ||
|
|
4ae1e7d33e | ||
|
|
43fa4526a4 | ||
|
|
fc4b1da323 | ||
|
|
843755f4e4 | ||
|
|
80e02f7520 | ||
|
|
af8f52e589 | ||
|
|
bc3f48dec9 | ||
|
|
74ee832507 | ||
|
|
da1b2d09b1 | ||
|
|
99f8607211 | ||
|
|
ef293088e1 | ||
|
|
e08e72ccb0 | ||
|
|
b692440bab | ||
|
|
7061abc64b | ||
|
|
0dd5064abb | ||
|
|
a1aafff6ce | ||
|
|
1f88f154af | ||
|
|
3d1e0364c6 | ||
|
|
0f1b5431bb | ||
|
|
0369d3fa62 | ||
|
|
154e3a732a | ||
|
|
9c979db043 | ||
|
|
0af089db89 | ||
|
|
1335613860 | ||
|
|
cb86bfd8dc | ||
|
|
a0d32ae493 | ||
|
|
f7e56a6c40 | ||
|
|
56613c75f7 | ||
|
|
fb3c35c0a0 | ||
|
|
4b3dc0a59f | ||
|
|
7855615a7b | ||
|
|
ff6576f4da | ||
|
|
931fa9a9b0 | ||
|
|
77a70967f2 | ||
|
|
e5506d952c | ||
|
|
2c2dbd0761 | ||
|
|
e6f5ecd496 | ||
|
|
73cea2d83c | ||
|
|
835a576f44 | ||
|
|
0a090341cc | ||
|
|
453671abfb | ||
|
|
cfa7daa984 | ||
|
|
88f913f586 | ||
|
|
5b4aeb4923 | ||
|
|
19133a2913 | ||
|
|
293035b7c8 | ||
|
|
d06723de5c | ||
|
|
bc45d0c499 | ||
|
|
c320cccf6f | ||
|
|
e3aebbd145 | ||
|
|
e15d378e46 | ||
|
|
b6720d10fb | ||
|
|
83a2dbe8a1 | ||
|
|
5b8592a99d | ||
|
|
18a094c06c | ||
|
|
a319ff3dc0 | ||
|
|
0cb46eca1a | ||
|
|
d85c814cba | ||
|
|
f49e660f29 | ||
|
|
afa407e7d1 | ||
|
|
37e0f5ecea | ||
|
|
5000fdcfea | ||
|
|
2ec7489dbf | ||
|
|
05965cea6e | ||
|
|
279e22ccb3 | ||
|
|
6a6fc1ca8b | ||
|
|
b6b5426297 | ||
|
|
e332ddda74 | ||
|
|
2cd4cfb883 | ||
|
|
ef12d09d35 | ||
|
|
1e365a8a7c | ||
|
|
e9363b41fd | ||
|
|
5e99df137a | ||
|
|
c0d0b45e24 | ||
|
|
3340b4cdfa | ||
|
|
d4afcc3383 | ||
|
|
dad423eb04 | ||
|
|
7b275d7e3d | ||
|
|
5274ecb721 | ||
|
|
e45367a482 | ||
|
|
83532edaab | ||
|
|
793d28da6a | ||
|
|
a8e575f680 | ||
|
|
98b0b3f9dd | ||
|
|
2e6d9c296a | ||
|
|
a07dc96ef9 | ||
|
|
8ba097a68a | ||
|
|
b1dd990fea | ||
|
|
ba7864b910 | ||
|
|
5d8fa343cd | ||
|
|
3fc49c431b | ||
|
|
79b6e65ce3 | ||
|
|
9f457d0d76 | ||
|
|
aa2ff62db4 | ||
|
|
73fffca569 | ||
|
|
45589fc033 | ||
|
|
79b81ed932 | ||
|
|
d1242870df | ||
|
|
e0dbbc4bc0 | ||
|
|
bf89791817 | ||
|
|
e3197f6dc1 | ||
|
|
e6317aa898 | ||
|
|
c73dc326fd | ||
|
|
287e250357 | ||
|
|
9673a14420 | ||
|
|
3333fdc8d7 | ||
|
|
9fb4b8bb6e | ||
|
|
7b10ed13f4 | ||
|
|
c528bd797d | ||
|
|
264529705c | ||
|
|
4669e3dfc7 | ||
|
|
eff3798964 | ||
|
|
78c526c25b | ||
|
|
ec13415d1f | ||
|
|
96622184ae | ||
|
|
3742c1c862 | ||
|
|
a0c7757428 | ||
|
|
15f9f4906a | ||
|
|
d577cd9b21 | ||
|
|
cf610cbb87 | ||
|
|
1e1095204d | ||
|
|
3fb6a13a3a | ||
|
|
2826655fe2 | ||
|
|
0ccf450b28 | ||
|
|
d28b9460af | ||
|
|
3e1bdf98c2 | ||
|
|
3f87764230 | ||
|
|
25e8e2e9e1 | ||
|
|
3faf2ce9b9 | ||
|
|
cbe243fc9e | ||
|
|
a438f633be | ||
|
|
37ef67d7ac | ||
|
|
67d631b0f0 | ||
|
|
fc302ffa5f | ||
|
|
8c28556a94 | ||
|
|
45cc531eec | ||
|
|
5c9ad9286d | ||
|
|
ad1c9486d7 | ||
|
|
ad6a03b712 | ||
|
|
36bb8010bc | ||
|
|
2200da7a16 | ||
|
|
688c0e2e85 | ||
|
|
714345a65d | ||
|
|
34a1c7e408 | ||
|
|
6255221d6a | ||
|
|
58364de72a | ||
|
|
6d64df4ee4 | ||
|
|
7bac2f206b | ||
|
|
75e1a17a2c | ||
|
|
47b13384a8 | ||
|
|
77b9efa7d1 | ||
|
|
be5f3b18af | ||
|
|
d5d12a7ce5 | ||
|
|
d7726d7755 | ||
|
|
0cd0d37eff | ||
|
|
4521def103 | ||
|
|
5c70f0a758 | ||
|
|
c12c2c0416 | ||
|
|
db45c422e7 | ||
|
|
affd9a95c5 | ||
|
|
7baf25869a | ||
|
|
12096fb427 | ||
|
|
ef7136cb81 | ||
|
|
3c4baf0126 | ||
|
|
f0b87c62a5 | ||
|
|
a319435e91 | ||
|
|
5bd0e988e3 | ||
|
|
b2be669b9e | ||
|
|
51952b0485 | ||
|
|
2b0c5e7fac | ||
|
|
3e6cea1a6a | ||
|
|
1aec7c0999 | ||
|
|
5da98809a5 | ||
|
|
49695614b7 | ||
|
|
3fbbc104b7 | ||
|
|
2fe7c0b85e | ||
|
|
09d0e82216 | ||
|
|
d208fcea7d | ||
|
|
cc0674db34 | ||
|
|
1d5b84943d | ||
|
|
14fe992ca5 | ||
|
|
15232bddaf | ||
|
|
160ef25621 | ||
|
|
2afb8688a3 | ||
|
|
9d1af035ea | ||
|
|
fb7574d814 | ||
|
|
201a3cb9e3 | ||
|
|
cc735ee6a1 | ||
|
|
0165e14ea0 | ||
|
|
97d19605d5 | ||
|
|
bc490218f9 | ||
|
|
6dac05a21d | ||
|
|
fd3fff6322 | ||
|
|
edb64fff2e | ||
|
|
fe0e854e72 | ||
|
|
06c85fb203 | ||
|
|
69926c4ae1 | ||
|
|
ef44b0a412 | ||
|
|
8577ac1027 | ||
|
|
32da050106 | ||
|
|
526b74b3ef | ||
|
|
97ab328a9c | ||
|
|
21603eedcf | ||
|
|
7fbef273a1 | ||
|
|
9e19716504 | ||
|
|
b473642ab4 | ||
|
|
fba55f01a0 | ||
|
|
015e63ba66 | ||
|
|
d92e2407f3 | ||
|
|
a4f84fb8cd | ||
|
|
bfe88745ca | ||
|
|
0d334237ba | ||
|
|
fd5cff3fea | ||
|
|
af5b82e9fd | ||
|
|
d3561748c8 | ||
|
|
791a1d804b | ||
|
|
2442424e3b | ||
|
|
0ecedd2820 | ||
|
|
958d62ec0c | ||
|
|
400cfb2141 | ||
|
|
52b860dd8f | ||
|
|
4d57d8d576 | ||
|
|
9a098accd8 | ||
|
|
62f3b2522c | ||
|
|
9b48cd2037 | ||
|
|
69776d45d1 | ||
|
|
b8fb2660a4 | ||
|
|
941281298d | ||
|
|
8afc4511a6 | ||
|
|
f43ef325ae | ||
|
|
bbdc323204 | ||
|
|
27cbb70352 | ||
|
|
f5b10b516c | ||
|
|
5580308968 | ||
|
|
572901ec9d | ||
|
|
965239d215 | ||
|
|
ac1e5e991e | ||
|
|
66b7b127f9 | ||
|
|
0ed858b99c | ||
|
|
9b3e153a4d | ||
|
|
e525aef3d9 | ||
|
|
201b72c9c8 | ||
|
|
26b99f5f68 | ||
|
|
d3dc774492 | ||
|
|
1f7155a932 | ||
|
|
02729fe02b | ||
|
|
f5b98009dd | ||
|
|
cf0b66d852 | ||
|
|
84026afb92 | ||
|
|
4dea7d2a52 | ||
|
|
2df1b7dd61 | ||
|
|
89042113a5 | ||
|
|
48665ebcce | ||
|
|
103aaafff1 | ||
|
|
dff2217e80 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,12 +1,11 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: LucasGGamerM
|
github: LucasGGamerM
|
||||||
custom: ["https://liberapay.com/LucasGGamerM/donate", liberapay.com]
|
|
||||||
patreon: # mastodon
|
patreon: # mastodon
|
||||||
open_collective: # Replace with a single Open Collective username e.g., user1
|
open_collective: # Replace with a single Open Collective username e.g., user1
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username e.g., user1
|
liberapay: LucasGGamerM # Replace with a single Liberapay username e.g., user1
|
||||||
issuehunt: # Replace with a single IssueHunt username e.g., user1
|
issuehunt: # Replace with a single IssueHunt username e.g., user1
|
||||||
otechie: # Replace with a single Otechie username e.g., user1
|
otechie: # Replace with a single Otechie username e.g., user1
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -25,7 +25,7 @@ Does this issue also occur with the respective upstream release?
|
|||||||
> No / Yes
|
> No / Yes
|
||||||
|
|
||||||
> In case it does, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead.
|
> In case it does, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead.
|
||||||
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
|
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Moshidon, feel free to still create this issue!
|
||||||
|
|
||||||
**Screenshots and screen recordings**
|
**Screenshots and screen recordings**
|
||||||
|
|
||||||
|
|||||||
42
.github/workflows/nightly-builds.yml
vendored
42
.github/workflows/nightly-builds.yml
vendored
@@ -11,27 +11,27 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Appkit Repo
|
# - name: Checkout Appkit Repo
|
||||||
uses: actions/checkout@v3
|
# uses: actions/checkout@v3
|
||||||
with:
|
# with:
|
||||||
repository: grishka/appkit
|
# repository: grishka/appkit
|
||||||
|
#
|
||||||
- name: set up JDK 17
|
# - name: set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
# uses: actions/setup-java@v3
|
||||||
with:
|
# with:
|
||||||
java-version: '17'
|
# java-version: '17'
|
||||||
distribution: 'corretto'
|
# distribution: 'corretto'
|
||||||
cache: gradle
|
# cache: gradle
|
||||||
|
#
|
||||||
- name: Comment out signing config in appkits gradle file
|
# - name: Comment out signing config in appkits gradle file
|
||||||
run: |
|
# run: |
|
||||||
sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
# sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||||
|
#
|
||||||
- name: Grant execute permission for gradlew for Appkit
|
# - name: Grant execute permission for gradlew for Appkit
|
||||||
run: chmod +x gradlew
|
# run: chmod +x gradlew
|
||||||
|
#
|
||||||
- name: Compile appkit
|
# - name: Compile appkit
|
||||||
run: ./gradlew publishToMavenLocal
|
# run: ./gradlew publishToMavenLocal
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||||
|
|
||||||
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
||||||
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate (Currently broken)
|
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate
|
||||||
|
|
||||||
### You can also donate some Monero through this wallet address as well:
|
### You can also donate some Monero through this wallet address as well:
|
||||||
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j
|
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ android {
|
|||||||
archivesBaseName = "moshidon"
|
archivesBaseName = "moshidon"
|
||||||
applicationId "org.joinmastodon.android.moshinda"
|
applicationId "org.joinmastodon.android.moshinda"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
versionCode 103
|
versionCode 107
|
||||||
versionName "2.1.4+fork.103.moshinda"
|
versionName "2.3.0+fork.107.moshinda"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', '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', 'kab', 'ko-rKR', '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']
|
resourceConfigurations += ['ar-rSA', 'ar-rDZ', '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', 'kab', 'ko-rKR', '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']
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,28 @@ android {
|
|||||||
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
|
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// release{
|
||||||
|
// storeFile = file("keystore/release_keystore.jks")
|
||||||
|
// storePassword System.getenv("RELEASE_SIGNING_STORE_PASSWORD")
|
||||||
|
// if (storePassword == null) {
|
||||||
|
// Properties properties = new Properties()
|
||||||
|
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
// storePassword = properties.getProperty('RELEASE_SIGNING_STORE_PASSWORD')
|
||||||
|
// }
|
||||||
|
// keyAlias System.getenv("RELEASE_SIGNING_KEY_ALIAS")
|
||||||
|
// if (keyAlias == null) {
|
||||||
|
// Properties properties = new Properties()
|
||||||
|
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
// keyAlias = properties.getProperty('RELEASE_SIGNING_KEY_ALIAS')
|
||||||
|
// }
|
||||||
|
// keyPassword System.getenv("RELEASE_SIGNING_KEY_PASSWORD")
|
||||||
|
// if (keyPassword == null) {
|
||||||
|
// Properties properties = new Properties()
|
||||||
|
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
// keyPassword = properties.getProperty('RELEASE_SIGNING_KEY_PASSWORD')
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -80,8 +102,16 @@ android {
|
|||||||
shrinkResources true
|
shrinkResources true
|
||||||
versionNameSuffix '-play'
|
versionNameSuffix '-play'
|
||||||
}
|
}
|
||||||
githubRelease { initWith release }
|
githubRelease {
|
||||||
fdroidRelease { initWith release }
|
initWith release
|
||||||
|
versionNameSuffix '-github'
|
||||||
|
}
|
||||||
|
fdroidRelease {
|
||||||
|
initWith release
|
||||||
|
// The F-droid build system doesn't like this at all for some reason.
|
||||||
|
// versionNameSuffix '-fdroid'
|
||||||
|
// signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -117,7 +147,7 @@ dependencies {
|
|||||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||||
implementation 'me.grishka.litex:palette:1.0.0'
|
implementation 'me.grishka.litex:palette:1.0.0'
|
||||||
implementation 'me.grishka.appkit:appkit:1.2.14'
|
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||||
implementation 'com.google.code.gson:gson:2.9.0'
|
implementation 'com.google.code.gson:gson:2.9.0'
|
||||||
implementation 'org.jsoup:jsoup:1.14.3'
|
implementation 'org.jsoup:jsoup:1.14.3'
|
||||||
implementation 'com.squareup:otto:1.3.8'
|
implementation 'com.squareup:otto:1.3.8'
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.joinmastodon.android.utils;
|
|
||||||
|
|
||||||
import static org.joinmastodon.android.model.FilterAction.*;
|
|
||||||
import static org.joinmastodon.android.model.FilterContext.*;
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.model.LegacyFilter;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class StatusFilterPredicateTest {
|
|
||||||
|
|
||||||
private static final LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter();
|
|
||||||
private static final List<LegacyFilter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
|
||||||
|
|
||||||
private static final Status
|
|
||||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
|
||||||
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now()),
|
|
||||||
noAltText = Status.ofFake(null, "display me with a warning", Instant.now()),
|
|
||||||
withAltText = Status.ofFake(null, "display me with a warning", Instant.now());
|
|
||||||
|
|
||||||
static {
|
|
||||||
hideMeFilter.phrase = "hide me";
|
|
||||||
hideMeFilter.filterAction = HIDE;
|
|
||||||
hideMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
|
||||||
|
|
||||||
warnMeFilter.phrase = "warning";
|
|
||||||
warnMeFilter.filterAction = WARN;
|
|
||||||
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
|
||||||
|
|
||||||
noAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
|
|
||||||
withAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
|
|
||||||
for (Attachment mediaAttachment : withAltText.mediaAttachments) {
|
|
||||||
mediaAttachment.description = "Alt Text";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHide() {
|
|
||||||
assertFalse("should not pass because matching filter applies to given context",
|
|
||||||
new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHideRegardlessOfContext() {
|
|
||||||
assertTrue("filters without context should always pass",
|
|
||||||
new StatusFilterPredicate(allFilters, null).test(hideInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHideInDifferentContext() {
|
|
||||||
assertTrue("should pass because matching filter does not apply to given context",
|
|
||||||
new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHideWithWarningText() {
|
|
||||||
assertTrue("should pass because matching filter is for warnings",
|
|
||||||
new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWarn() {
|
|
||||||
assertFalse("should not pass because filter applies to given context",
|
|
||||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWarnRegardlessOfContext() {
|
|
||||||
assertTrue("filters without context should always pass",
|
|
||||||
new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWarnInDifferentContext() {
|
|
||||||
assertTrue("should pass because filter does not apply to given context",
|
|
||||||
new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWarnWithHideText() {
|
|
||||||
assertTrue("should pass because matching filter is for hiding",
|
|
||||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testAltTextFilterNoPass() {
|
|
||||||
assertFalse("should not pass because of no alt text",
|
|
||||||
new StatusFilterPredicate(allFilters, HOME).test(noAltText));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testAltTextFilterPass() {
|
|
||||||
assertTrue("should pass because of alt text",
|
|
||||||
new StatusFilterPredicate(allFilters, HOME).test(withAltText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -211,7 +211,13 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||||||
if(state==UpdateState.DOWNLOADING)
|
if(state==UpdateState.DOWNLOADING)
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
|
||||||
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||||
|
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED);
|
||||||
|
}else{
|
||||||
|
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||||
|
}
|
||||||
|
|
||||||
downloadID=dm.enqueue(
|
downloadID=dm.enqueue(
|
||||||
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
||||||
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
android:name=".MastodonApp"
|
android:name=".MastodonApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:label="@string/mo_app_name"
|
android:label="@string/mo_app_name"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -80,6 +82,15 @@
|
|||||||
<data android:mimeType="*/*"/>
|
<data android:mimeType="*/*"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".ChooseAccountForComposeActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
|
||||||
|
android:theme="@style/TransparentDialog">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.CHOOSER"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
|
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
|
||||||
|
|
||||||
@@ -105,13 +116,11 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="org.joinmastodon.android.utils.FileProvider"
|
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:name=".TweakedFileProvider"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true"
|
||||||
<meta-data
|
android:exported="false">
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths"/>
|
||||||
android:resource="@xml/file_paths" />
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -20,13 +20,16 @@ cachapa.xyz
|
|||||||
canary.fedinuke.example.com
|
canary.fedinuke.example.com
|
||||||
catgirl.life
|
catgirl.life
|
||||||
cawfee.club
|
cawfee.club
|
||||||
childlove.space
|
childlove.su
|
||||||
clew.lol
|
clew.lol
|
||||||
clubcyberia.co
|
clubcyberia.co
|
||||||
contrapointsfan.club
|
contrapointsfan.club
|
||||||
|
cottoncandy.cafe
|
||||||
|
crlf.ninja
|
||||||
crucible.world
|
crucible.world
|
||||||
cum.camp
|
cum.camp
|
||||||
cum.salon
|
cum.salon
|
||||||
|
cunnyborea.space
|
||||||
decayable.ink
|
decayable.ink
|
||||||
dembased.xyz
|
dembased.xyz
|
||||||
detroitriotcity.com
|
detroitriotcity.com
|
||||||
@@ -34,10 +37,12 @@ djsumdog.com
|
|||||||
eientei.org
|
eientei.org
|
||||||
eveningzoo.club
|
eveningzoo.club
|
||||||
fluf.club
|
fluf.club
|
||||||
|
foxgirl.lol
|
||||||
freak.university
|
freak.university
|
||||||
freeatlantis.com
|
freeatlantis.com
|
||||||
freespeechextremist.com
|
freespeechextremist.com
|
||||||
froth.zone
|
froth.zone
|
||||||
|
fsebugoutzone.org
|
||||||
gameliberty.club
|
gameliberty.club
|
||||||
gearlandia.haus
|
gearlandia.haus
|
||||||
genderheretics.xyz
|
genderheretics.xyz
|
||||||
@@ -49,6 +54,7 @@ goyim.app
|
|||||||
h5q.net
|
h5q.net
|
||||||
haeder.net
|
haeder.net
|
||||||
handholding.io
|
handholding.io
|
||||||
|
harpy.faith
|
||||||
hitchhiker.social
|
hitchhiker.social
|
||||||
iddqd.social
|
iddqd.social
|
||||||
kitsunemimi.club
|
kitsunemimi.club
|
||||||
@@ -56,15 +62,14 @@ kiwifarms.cc
|
|||||||
kurosawa.moe
|
kurosawa.moe
|
||||||
kyaruc.moe
|
kyaruc.moe
|
||||||
leafposter.club
|
leafposter.club
|
||||||
lewdieheaven.com
|
|
||||||
liberdon.com
|
liberdon.com
|
||||||
ligma.pro
|
ligma.pro
|
||||||
|
loli.church
|
||||||
lolicon.rocks
|
lolicon.rocks
|
||||||
lolison.network
|
lolison.network
|
||||||
lolison.top
|
lolison.top
|
||||||
lovingexpressions.net
|
lovingexpressions.net
|
||||||
makemysarcophagus.com
|
makemysarcophagus.com
|
||||||
marsey.moe
|
|
||||||
mastinator.com
|
mastinator.com
|
||||||
merovingian.club
|
merovingian.club
|
||||||
midwaytrades.com
|
midwaytrades.com
|
||||||
@@ -74,17 +79,21 @@ mouse.services
|
|||||||
mugicha.club
|
mugicha.club
|
||||||
narrativerry.xyz
|
narrativerry.xyz
|
||||||
natehiggers.online
|
natehiggers.online
|
||||||
|
nationalist.social
|
||||||
needs.vodka
|
needs.vodka
|
||||||
neenster.org
|
neenster.org
|
||||||
nicecrew.digital
|
nicecrew.digital
|
||||||
|
nightshift.social
|
||||||
nnia.space
|
nnia.space
|
||||||
noagendasocial.com
|
noagendasocial.com
|
||||||
noagendasocial.nl
|
noagendasocial.nl
|
||||||
noagendatube.com
|
noagendatube.com
|
||||||
|
noauthority.social
|
||||||
nobodyhasthe.biz
|
nobodyhasthe.biz
|
||||||
norwoodzero.net
|
norwoodzero.net
|
||||||
nyanide.com
|
nyanide.com
|
||||||
onionfarms.org
|
onionfarms.org
|
||||||
|
parcero.bond
|
||||||
pawlicker.com
|
pawlicker.com
|
||||||
pawoo.net
|
pawoo.net
|
||||||
pedo.school
|
pedo.school
|
||||||
@@ -129,9 +138,11 @@ sonichu.com
|
|||||||
spinster.xyz
|
spinster.xyz
|
||||||
springbo.cc
|
springbo.cc
|
||||||
strelizia.net
|
strelizia.net
|
||||||
|
taihou.website
|
||||||
tastingtraffic.net
|
tastingtraffic.net
|
||||||
teci.world
|
teci.world
|
||||||
theapex.social
|
theapex.social
|
||||||
|
theblab.org
|
||||||
thechimp.zone
|
thechimp.zone
|
||||||
thenobody.club
|
thenobody.club
|
||||||
thepostearthdestination.com
|
thepostearthdestination.com
|
||||||
@@ -139,9 +150,11 @@ tkammer.de
|
|||||||
trumpislovetrumpis.life
|
trumpislovetrumpis.life
|
||||||
truthsocial.co.in
|
truthsocial.co.in
|
||||||
usualsuspects.lol
|
usualsuspects.lol
|
||||||
|
vampiremaid.cafe
|
||||||
varishangout.net
|
varishangout.net
|
||||||
vtuberfan.social
|
vtuberfan.social
|
||||||
wolfgirl.bar
|
wolfgirl.bar
|
||||||
xn--p1abe3d.xn--80asehdb
|
xn--p1abe3d.xn--80asehdb
|
||||||
yggdrasil.social
|
yggdrasil.social
|
||||||
youjo.love
|
youjo.love
|
||||||
|
zhub.link
|
||||||
@@ -88,8 +88,13 @@ public class AudioPlayerService extends Service{
|
|||||||
nm=getSystemService(NotificationManager.class);
|
nm=getSystemService(NotificationManager.class);
|
||||||
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
|
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
|
||||||
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
|
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;
|
instance=this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
|
|
||||||
|
public class ChooseAccountForComposeActivity extends FragmentStackActivity{
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||||
|
UiUtils.setUserPreferredTheme(this);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (savedInstanceState == null && Objects.equals(getIntent().getAction(), Intent.ACTION_CHOOSER)) {
|
||||||
|
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||||
|
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||||
|
if (sessions.isEmpty()){
|
||||||
|
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
} else if (sessions.size() > 1) {
|
||||||
|
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, R.drawable.ic_fluent_compose_28_regular,
|
||||||
|
R.string.choose_account, null, false);
|
||||||
|
sheet.setOnClick((accountId, open) -> {
|
||||||
|
openComposeFragment(accountId);
|
||||||
|
});
|
||||||
|
sheet.show();
|
||||||
|
} else if (sessions.size() == 1) {
|
||||||
|
openComposeFragment(sessions.get(0).getID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openComposeFragment(String accountID){
|
||||||
|
getWindow().setBackgroundDrawable(null);
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Fragment fragment=new ComposeFragment();
|
||||||
|
fragment.setArguments(args);
|
||||||
|
showFragmentClearingBackStack(fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.jsoup.internal.StringUtil;
|
import org.jsoup.internal.StringUtil;
|
||||||
|
|
||||||
@@ -42,7 +42,11 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||||||
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
} else if (isOpenable || sessions.size() > 1) {
|
} else if (isOpenable || sessions.size() > 1) {
|
||||||
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
|
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, R.drawable.ic_fluent_share_28_regular,
|
||||||
|
isOpenable
|
||||||
|
? R.string.sk_external_share_or_open_title
|
||||||
|
: R.string.sk_external_share_title,
|
||||||
|
null, isOpenable);
|
||||||
sheet.setOnClick((accountId, open) -> {
|
sheet.setOnClick((accountId, open) -> {
|
||||||
if (open && text.isPresent()) {
|
if (open && text.isPresent()) {
|
||||||
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
|
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
|
||||||
|
|||||||
@@ -0,0 +1,841 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
|
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||||
|
import static org.xmlpull.v1.XmlPullParser.START_TAG;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.content.res.XmlResourceParser;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing
|
||||||
|
* of files associated with an app by creating a <code>content://</code> {@link Uri} for a file
|
||||||
|
* instead of a <code>file:///</code> {@link Uri}.
|
||||||
|
* <p>
|
||||||
|
* A content URI allows you to grant read and write access using
|
||||||
|
* temporary access permissions. When you create an {@link Intent} containing
|
||||||
|
* a content URI, in order to send the content URI
|
||||||
|
* to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add
|
||||||
|
* permissions. These permissions are available to the client app for as long as the stack for
|
||||||
|
* a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a
|
||||||
|
* {@link android.app.Service}, the permissions are available as long as the
|
||||||
|
* {@link android.app.Service} is running.
|
||||||
|
* <p>
|
||||||
|
* In comparison, to control access to a <code>file:///</code> {@link Uri} you have to modify the
|
||||||
|
* file system permissions of the underlying file. The permissions you provide become available to
|
||||||
|
* <em>any</em> app, and remain in effect until you change them. This level of access is
|
||||||
|
* fundamentally insecure.
|
||||||
|
* <p>
|
||||||
|
* The increased level of file access security offered by a content URI
|
||||||
|
* makes FileProvider a key part of Android's security infrastructure.
|
||||||
|
* <p>
|
||||||
|
* This overview of FileProvider includes the following topics:
|
||||||
|
* </p>
|
||||||
|
* <ol>
|
||||||
|
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
|
||||||
|
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
|
||||||
|
* <li><a href="#GetUri">Retrieving the Content URI for a File</li>
|
||||||
|
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
|
||||||
|
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
|
||||||
|
* </ol>
|
||||||
|
* <h3 id="ProviderDefinition">Defining a FileProvider</h3>
|
||||||
|
* <p>
|
||||||
|
* Since the default functionality of FileProvider includes content URI generation for files, you
|
||||||
|
* don't need to define a subclass in code. Instead, you can include a FileProvider in your app
|
||||||
|
* by specifying it entirely in XML. To specify the FileProvider component itself, add a
|
||||||
|
* <code><a href="{@docRoot}guide/topics/manifest/provider-element.html"><provider></a></code>
|
||||||
|
* element to your app manifest. Set the <code>android:name</code> attribute to
|
||||||
|
* <code>androidx.core.content.FileProvider</code>. Set the <code>android:authorities</code>
|
||||||
|
* attribute to a URI authority based on a domain you control; for example, if you control the
|
||||||
|
* domain <code>mydomain.com</code> you should use the authority
|
||||||
|
* <code>com.mydomain.fileprovider</code>. Set the <code>android:exported</code> attribute to
|
||||||
|
* <code>false</code>; the FileProvider does not need to be public. Set the
|
||||||
|
* <a href="{@docRoot}guide/topics/manifest/provider-element.html#gprmsn"
|
||||||
|
* >android:grantUriPermissions</a> attribute to <code>true</code>, to allow you
|
||||||
|
* to grant temporary access to files. For example:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<manifest>
|
||||||
|
* ...
|
||||||
|
* <application>
|
||||||
|
* ...
|
||||||
|
* <provider
|
||||||
|
* android:name="androidx.core.content.FileProvider"
|
||||||
|
* android:authorities="com.mydomain.fileprovider"
|
||||||
|
* android:exported="false"
|
||||||
|
* android:grantUriPermissions="true">
|
||||||
|
* ...
|
||||||
|
* </provider>
|
||||||
|
* ...
|
||||||
|
* </application>
|
||||||
|
*</manifest></pre>
|
||||||
|
* <p>
|
||||||
|
* If you want to override any of the default behavior of FileProvider methods, extend
|
||||||
|
* the FileProvider class and use the fully-qualified class name in the <code>android:name</code>
|
||||||
|
* attribute of the <code><provider></code> element.
|
||||||
|
* <h3 id="SpecifyFiles">Specifying Available Files</h3>
|
||||||
|
* A FileProvider can only generate a content URI for files in directories that you specify
|
||||||
|
* beforehand. To specify a directory, specify the its storage area and path in XML, using child
|
||||||
|
* elements of the <code><paths></code> element.
|
||||||
|
* For example, the following <code>paths</code> element tells FileProvider that you intend to
|
||||||
|
* request content URIs for the <code>images/</code> subdirectory of your private file area.
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
* <files-path name="my_images" path="images/"/>
|
||||||
|
* ...
|
||||||
|
*</paths>
|
||||||
|
*</pre>
|
||||||
|
* <p>
|
||||||
|
* The <code><paths></code> element must contain one or more of the following child elements:
|
||||||
|
* </p>
|
||||||
|
* <dl>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<files-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the <code>files/</code> subdirectory of your app's internal storage
|
||||||
|
* area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
|
||||||
|
* Context.getFilesDir()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre>
|
||||||
|
*<cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* <dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the cache subdirectory of your app's internal storage area. The root path
|
||||||
|
* of this subdirectory is the same as the value returned by {@link Context#getCacheDir()
|
||||||
|
* getCacheDir()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of the external storage area. The root path of this subdirectory
|
||||||
|
* is the same as the value returned by
|
||||||
|
* {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-files-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of your app's external storage area. The root path of this
|
||||||
|
* subdirectory is the same as the value returned by
|
||||||
|
* {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of your app's external cache area. The root path of this
|
||||||
|
* subdirectory is the same as the value returned by
|
||||||
|
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-media-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of your app's external media area. The root path of this
|
||||||
|
* subdirectory is the same as the value returned by the first result of
|
||||||
|
* {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
|
||||||
|
* <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
|
||||||
|
* </dd>
|
||||||
|
* </dl>
|
||||||
|
* <p>
|
||||||
|
* These child elements all use the same attributes:
|
||||||
|
* </p>
|
||||||
|
* <dl>
|
||||||
|
* <dt>
|
||||||
|
* <code>name="<i>name</i>"</code>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* A URI path segment. To enforce security, this value hides the name of the subdirectory
|
||||||
|
* you're sharing. The subdirectory name for this value is contained in the
|
||||||
|
* <code>path</code> attribute.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <code>path="<i>path</i>"</code>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* The subdirectory you're sharing. While the <code>name</code> attribute is a URI path
|
||||||
|
* segment, the <code>path</code> value is an actual subdirectory name. Notice that the
|
||||||
|
* value refers to a <b>subdirectory</b>, not an individual file or files. You can't
|
||||||
|
* share a single file by its file name, nor can you specify a subset of files using
|
||||||
|
* wildcards.
|
||||||
|
* </dd>
|
||||||
|
* </dl>
|
||||||
|
* <p>
|
||||||
|
* You must specify a child element of <code><paths></code> for each directory that contains
|
||||||
|
* files for which you want content URIs. For example, these XML elements specify two directories:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
* <files-path name="my_images" path="images/"/>
|
||||||
|
* <files-path name="my_docs" path="docs/"/>
|
||||||
|
*</paths>
|
||||||
|
*</pre>
|
||||||
|
* <p>
|
||||||
|
* Put the <code><paths></code> element and its children in an XML file in your project.
|
||||||
|
* For example, you can add them to a new file called <code>res/xml/file_paths.xml</code>.
|
||||||
|
* To link this file to the FileProvider, add a
|
||||||
|
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html"><meta-data></a> element
|
||||||
|
* as a child of the <code><provider></code> element that defines the FileProvider. Set the
|
||||||
|
* <code><meta-data></code> element's "android:name" attribute to
|
||||||
|
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
|
||||||
|
* to <code>@xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
|
||||||
|
* extension). For example:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<provider
|
||||||
|
* android:name="androidx.core.content.FileProvider"
|
||||||
|
* android:authorities="com.mydomain.fileprovider"
|
||||||
|
* android:exported="false"
|
||||||
|
* android:grantUriPermissions="true">
|
||||||
|
* <meta-data
|
||||||
|
* android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
* android:resource="@xml/file_paths" />
|
||||||
|
*</provider>
|
||||||
|
*</pre>
|
||||||
|
* <h3 id="GetUri">Generating the Content URI for a File</h3>
|
||||||
|
* <p>
|
||||||
|
* To share a file with another app using a content URI, your app has to generate the content URI.
|
||||||
|
* To generate the content URI, create a new {@link File} for the file, then pass the {@link File}
|
||||||
|
* to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI
|
||||||
|
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an
|
||||||
|
* {@link Intent}. The client app that receives the content URI can open the file
|
||||||
|
* and access its contents by calling
|
||||||
|
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
|
||||||
|
* ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}.
|
||||||
|
* <p>
|
||||||
|
* For example, suppose your app is offering files to other apps with a FileProvider that has the
|
||||||
|
* authority <code>com.mydomain.fileprovider</code>. To get a content URI for the file
|
||||||
|
* <code>default_image.jpg</code> in the <code>images/</code> subdirectory of your internal storage
|
||||||
|
* add the following code:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*File imagePath = new File(Context.getFilesDir(), "images");
|
||||||
|
*File newFile = new File(imagePath, "default_image.jpg");
|
||||||
|
*Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
|
||||||
|
*</pre>
|
||||||
|
* As a result of the previous snippet,
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI
|
||||||
|
* <code>content://com.mydomain.fileprovider/my_images/default_image.jpg</code>.
|
||||||
|
* <h3 id="Permissions">Granting Temporary Permissions to a URI</h3>
|
||||||
|
* To grant an access permission to a content URI returned from
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following:
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* Call the method
|
||||||
|
* {@link Context#grantUriPermission(String, Uri, int)
|
||||||
|
* Context.grantUriPermission(package, Uri, mode_flags)} for the <code>content://</code>
|
||||||
|
* {@link Uri}, using the desired mode flags. This grants temporary access permission for the
|
||||||
|
* content URI to the specified package, according to the value of the
|
||||||
|
* the <code>mode_flags</code> parameter, which you can set to
|
||||||
|
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
|
||||||
|
* or both. The permission remains in effect until you revoke it by calling
|
||||||
|
* {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
|
||||||
|
* reboots.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either
|
||||||
|
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or
|
||||||
|
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Finally, send the {@link Intent} to
|
||||||
|
* another app. Most often, you do this by calling
|
||||||
|
* {@link android.app.Activity#setResult(int, Intent) setResult()}.
|
||||||
|
* <p>
|
||||||
|
* Permissions granted in an {@link Intent} remain in effect while the stack of the receiving
|
||||||
|
* {@link android.app.Activity} is active. When the stack finishes, the permissions are
|
||||||
|
* automatically removed. Permissions granted to one {@link android.app.Activity} in a client
|
||||||
|
* app are automatically extended to other components of that app.
|
||||||
|
* </p>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* <h3 id="ServeUri">Serving a Content URI to Another App</h3>
|
||||||
|
* <p>
|
||||||
|
* There are a variety of ways to serve the content URI for a file to a client app. One common way
|
||||||
|
* is for the client app to start your app by calling
|
||||||
|
* {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()},
|
||||||
|
* which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app.
|
||||||
|
* In response, your app can immediately return a content URI to the client app or present a user
|
||||||
|
* interface that allows the user to pick a file. In the latter case, once the user picks the file
|
||||||
|
* your app can return its content URI. In both cases, your app returns the content URI in an
|
||||||
|
* {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* You can also put the content URI in a {@link android.content.ClipData} object and then add the
|
||||||
|
* object to an {@link Intent} you send to a client app. To do this, call
|
||||||
|
* {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can
|
||||||
|
* add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own
|
||||||
|
* content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent}
|
||||||
|
* to set temporary access permissions, the same permissions are applied to all of the content
|
||||||
|
* URIs.
|
||||||
|
* </p>
|
||||||
|
* <p class="note">
|
||||||
|
* <strong>Note:</strong> The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is
|
||||||
|
* only available in platform version 16 (Android 4.1) and later. If you want to maintain
|
||||||
|
* compatibility with previous versions, you should send one content URI at a time in the
|
||||||
|
* {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling
|
||||||
|
* {@link Intent#setData setData()}.
|
||||||
|
* </p>
|
||||||
|
* <h3 id="">More Information</h3>
|
||||||
|
* <p>
|
||||||
|
* To learn more about FileProvider, see the Android training class
|
||||||
|
* <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files Securely with URIs</a>.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class FileProvider extends ContentProvider {
|
||||||
|
private static final String[] COLUMNS = {
|
||||||
|
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||||
|
|
||||||
|
private static final String
|
||||||
|
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
|
||||||
|
|
||||||
|
private static final String TAG_ROOT_PATH = "root-path";
|
||||||
|
private static final String TAG_FILES_PATH = "files-path";
|
||||||
|
private static final String TAG_CACHE_PATH = "cache-path";
|
||||||
|
private static final String TAG_EXTERNAL = "external-path";
|
||||||
|
private static final String TAG_EXTERNAL_FILES = "external-files-path";
|
||||||
|
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
|
||||||
|
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
|
||||||
|
|
||||||
|
private static final String ATTR_NAME = "name";
|
||||||
|
private static final String ATTR_PATH = "path";
|
||||||
|
|
||||||
|
private static final File DEVICE_ROOT = new File("/");
|
||||||
|
|
||||||
|
@GuardedBy("sCache")
|
||||||
|
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
|
||||||
|
|
||||||
|
private PathStrategy mStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default FileProvider implementation does not need to be initialized. If you want to
|
||||||
|
* override this method, you must provide your own subclass of FileProvider.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After the FileProvider is instantiated, this method is called to provide the system with
|
||||||
|
* information about the provider.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context} for the current component.
|
||||||
|
* @param info A {@link ProviderInfo} for the new provider.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
|
||||||
|
super.attachInfo(context, info);
|
||||||
|
|
||||||
|
// Sanity check our security
|
||||||
|
if (info.exported) {
|
||||||
|
throw new SecurityException("Provider must not be exported");
|
||||||
|
}
|
||||||
|
if (!info.grantUriPermissions) {
|
||||||
|
throw new SecurityException("Provider must grant uri permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
mStrategy = getPathStrategy(context, info.authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a content URI for a given {@link File}. Specific temporary
|
||||||
|
* permissions for the content URI can be set with
|
||||||
|
* {@link Context#grantUriPermission(String, Uri, int)}, or added
|
||||||
|
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
|
||||||
|
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
|
||||||
|
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
|
||||||
|
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
|
||||||
|
* <code>content</code> {@link Uri} for file paths defined in their <code><paths></code>
|
||||||
|
* meta-data element. See the Class Overview for more information.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context} for the current component.
|
||||||
|
* @param authority The authority of a {@link FileProvider} defined in a
|
||||||
|
* {@code <provider>} element in your app's manifest.
|
||||||
|
* @param file A {@link File} pointing to the filename for which you want a
|
||||||
|
* <code>content</code> {@link Uri}.
|
||||||
|
* @return A content URI for the file.
|
||||||
|
* @throws IllegalArgumentException When the given {@link File} is outside
|
||||||
|
* the paths supported by the provider.
|
||||||
|
*/
|
||||||
|
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
|
||||||
|
@NonNull File file) {
|
||||||
|
final PathStrategy strategy = getPathStrategy(context, authority);
|
||||||
|
return strategy.getUriForFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a content URI returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
|
||||||
|
* managed by the FileProvider.
|
||||||
|
* FileProvider reports the column names defined in {@link OpenableColumns}:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
|
||||||
|
* <li>{@link OpenableColumns#SIZE}</li>
|
||||||
|
* </ul>
|
||||||
|
* For more information, see
|
||||||
|
* {@link ContentProvider#query(Uri, String[], String, String[], String)
|
||||||
|
* ContentProvider.query()}.
|
||||||
|
*
|
||||||
|
* @param uri A content URI returned by {@link #getUriForFile}.
|
||||||
|
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
|
||||||
|
* included.
|
||||||
|
* @param selection Selection criteria to apply. If null then all data that matches the content
|
||||||
|
* URI is returned.
|
||||||
|
* @param selectionArgs An array of {@link String}, containing arguments to bind to
|
||||||
|
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
|
||||||
|
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
|
||||||
|
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
|
||||||
|
* values are bound to <i>selection</i> as {@link String} values.
|
||||||
|
* @param sortOrder A {@link String} containing the column name(s) on which to sort
|
||||||
|
* the resulting {@link Cursor}.
|
||||||
|
* @return A {@link Cursor} containing the results of the query.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs,
|
||||||
|
@Nullable String sortOrder) {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = COLUMNS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] cols = new String[projection.length];
|
||||||
|
Object[] values = new Object[projection.length];
|
||||||
|
int i = 0;
|
||||||
|
for (String col : projection) {
|
||||||
|
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||||
|
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||||
|
values[i++] = file.getName();
|
||||||
|
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||||
|
cols[i] = OpenableColumns.SIZE;
|
||||||
|
values[i++] = file.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cols = copyOf(cols, i);
|
||||||
|
values = copyOf(values, i);
|
||||||
|
|
||||||
|
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
||||||
|
cursor.addRow(values);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type of a content URI returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
*
|
||||||
|
* @param uri A content URI returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
* @return If the associated file has an extension, the MIME type associated with that
|
||||||
|
* extension; otherwise <code>application/octet-stream</code>.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri) {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
|
||||||
|
final int lastDot = file.getName().lastIndexOf('.');
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
final String extension = file.getName().substring(lastDot + 1);
|
||||||
|
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
if (mime != null) {
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, this method throws an {@link UnsupportedOperationException}. You must
|
||||||
|
* subclass FileProvider if you want to provide different functionality.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||||
|
throw new UnsupportedOperationException("No external inserts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, this method throws an {@link UnsupportedOperationException}. You must
|
||||||
|
* subclass FileProvider if you want to provide different functionality.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs) {
|
||||||
|
throw new UnsupportedOperationException("No external updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the file associated with the specified content URI, as
|
||||||
|
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
|
||||||
|
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
|
||||||
|
*
|
||||||
|
* @param uri A content URI for a file, as returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
* @param selection Ignored. Set to {@code null}.
|
||||||
|
* @param selectionArgs Ignored. Set to {@code null}.
|
||||||
|
* @return 1 if the delete succeeds; otherwise, 0.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int delete(@NonNull Uri uri, @Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs) {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
return file.delete() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, FileProvider automatically returns the
|
||||||
|
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
|
||||||
|
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
|
||||||
|
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
|
||||||
|
* ContentResolver.openFileDescriptor}.
|
||||||
|
*
|
||||||
|
* To override this method, you must provide your own subclass of FileProvider.
|
||||||
|
*
|
||||||
|
* @param uri A content URI associated with a file, as returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
|
||||||
|
* write access, or "rwt" for read and write access that truncates any existing file.
|
||||||
|
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
|
||||||
|
throws FileNotFoundException {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
final int fileMode = modeToMode(mode);
|
||||||
|
return ParcelFileDescriptor.open(file, fileMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@link PathStrategy} for given authority, either by parsing or
|
||||||
|
* returning from cache.
|
||||||
|
*/
|
||||||
|
private static PathStrategy getPathStrategy(Context context, String authority) {
|
||||||
|
PathStrategy strat;
|
||||||
|
synchronized (sCache) {
|
||||||
|
strat = sCache.get(authority);
|
||||||
|
if (strat == null) {
|
||||||
|
try {
|
||||||
|
strat = parsePathStrategy(context, authority);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||||
|
}
|
||||||
|
sCache.put(authority, strat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and return {@link PathStrategy} for given authority as defined in
|
||||||
|
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
|
||||||
|
*
|
||||||
|
* @see #getPathStrategy(Context, String)
|
||||||
|
*/
|
||||||
|
private static PathStrategy parsePathStrategy(Context context, String authority)
|
||||||
|
throws IOException, XmlPullParserException {
|
||||||
|
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
||||||
|
|
||||||
|
final ProviderInfo info = context.getPackageManager()
|
||||||
|
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
|
||||||
|
if (info == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Couldn't find meta-data for provider with authority " + authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
final XmlResourceParser in = info.loadXmlMetaData(
|
||||||
|
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
|
||||||
|
if (in == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
|
||||||
|
}
|
||||||
|
|
||||||
|
int type;
|
||||||
|
while ((type = in.next()) != END_DOCUMENT) {
|
||||||
|
if (type == START_TAG) {
|
||||||
|
final String tag = in.getName();
|
||||||
|
|
||||||
|
final String name = in.getAttributeValue(null, ATTR_NAME);
|
||||||
|
String path = in.getAttributeValue(null, ATTR_PATH);
|
||||||
|
|
||||||
|
File target = null;
|
||||||
|
if (TAG_ROOT_PATH.equals(tag)) {
|
||||||
|
target = DEVICE_ROOT;
|
||||||
|
} else if (TAG_FILES_PATH.equals(tag)) {
|
||||||
|
target = context.getFilesDir();
|
||||||
|
} else if (TAG_CACHE_PATH.equals(tag)) {
|
||||||
|
target = context.getCacheDir();
|
||||||
|
} else if (TAG_EXTERNAL.equals(tag)) {
|
||||||
|
target = Environment.getExternalStorageDirectory();
|
||||||
|
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
|
||||||
|
File[] externalFilesDirs = context.getExternalFilesDirs(null);
|
||||||
|
if (externalFilesDirs.length > 0) {
|
||||||
|
target = externalFilesDirs[0];
|
||||||
|
}
|
||||||
|
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
|
||||||
|
File[] externalCacheDirs = context.getExternalCacheDirs();
|
||||||
|
if (externalCacheDirs.length > 0) {
|
||||||
|
target = externalCacheDirs[0];
|
||||||
|
}
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||||
|
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
|
||||||
|
File[] externalMediaDirs = context.getExternalMediaDirs();
|
||||||
|
if (externalMediaDirs.length > 0) {
|
||||||
|
target = externalMediaDirs[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target != null) {
|
||||||
|
strat.addRoot(name, buildPath(target, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy for mapping between {@link File} and {@link Uri}.
|
||||||
|
* <p>
|
||||||
|
* Strategies must be symmetric so that mapping a {@link File} to a
|
||||||
|
* {@link Uri} and then back to a {@link File} points at the original
|
||||||
|
* target.
|
||||||
|
* <p>
|
||||||
|
* Strategies must remain consistent across app launches, and not rely on
|
||||||
|
* dynamic state. This ensures that any generated {@link Uri} can still be
|
||||||
|
* resolved if your process is killed and later restarted.
|
||||||
|
*
|
||||||
|
* @see SimplePathStrategy
|
||||||
|
*/
|
||||||
|
interface PathStrategy {
|
||||||
|
/**
|
||||||
|
* Return a {@link Uri} that represents the given {@link File}.
|
||||||
|
*/
|
||||||
|
Uri getUriForFile(File file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link File} that represents the given {@link Uri}.
|
||||||
|
*/
|
||||||
|
File getFileForUri(Uri uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy that provides access to files living under a narrow whitelist of
|
||||||
|
* filesystem roots. It will throw {@link SecurityException} if callers try
|
||||||
|
* accessing files outside the configured roots.
|
||||||
|
* <p>
|
||||||
|
* For example, if configured with
|
||||||
|
* {@code addRoot("myfiles", context.getFilesDir())}, then
|
||||||
|
* {@code context.getFileStreamPath("foo.txt")} would map to
|
||||||
|
* {@code content://myauthority/myfiles/foo.txt}.
|
||||||
|
*/
|
||||||
|
static class SimplePathStrategy implements PathStrategy {
|
||||||
|
private final String mAuthority;
|
||||||
|
private final HashMap<String, File> mRoots = new HashMap<String, File>();
|
||||||
|
|
||||||
|
SimplePathStrategy(String authority) {
|
||||||
|
mAuthority = authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a mapping from a name to a filesystem root. The provider only offers
|
||||||
|
* access to files that live under configured roots.
|
||||||
|
*/
|
||||||
|
void addRoot(String name, File root) {
|
||||||
|
if (TextUtils.isEmpty(name)) {
|
||||||
|
throw new IllegalArgumentException("Name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve to canonical path to keep path checking fast
|
||||||
|
root = root.getCanonicalFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to resolve canonical path for " + root, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mRoots.put(name, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getUriForFile(File file) {
|
||||||
|
String path;
|
||||||
|
try {
|
||||||
|
path = file.getCanonicalPath();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most-specific root path
|
||||||
|
Map.Entry<String, File> mostSpecific = null;
|
||||||
|
for (Map.Entry<String, File> root : mRoots.entrySet()) {
|
||||||
|
final String rootPath = root.getValue().getPath();
|
||||||
|
if (path.startsWith(rootPath) && (mostSpecific == null
|
||||||
|
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
|
||||||
|
mostSpecific = root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostSpecific == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to find configured root that contains " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start at first char of path under root
|
||||||
|
final String rootPath = mostSpecific.getValue().getPath();
|
||||||
|
if (rootPath.endsWith("/")) {
|
||||||
|
path = path.substring(rootPath.length());
|
||||||
|
} else {
|
||||||
|
path = path.substring(rootPath.length() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the tag and path separately
|
||||||
|
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
|
||||||
|
return new Uri.Builder().scheme("content")
|
||||||
|
.authority(mAuthority).encodedPath(path).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getFileForUri(Uri uri) {
|
||||||
|
String path = uri.getEncodedPath();
|
||||||
|
|
||||||
|
final int splitIndex = path.indexOf('/', 1);
|
||||||
|
final String tag = Uri.decode(path.substring(1, splitIndex));
|
||||||
|
path = Uri.decode(path.substring(splitIndex + 1));
|
||||||
|
|
||||||
|
final File root = mRoots.get(tag);
|
||||||
|
if (root == null) {
|
||||||
|
throw new IllegalArgumentException("Unable to find configured root for " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(root, path);
|
||||||
|
try {
|
||||||
|
file = file.getCanonicalFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.getPath().startsWith(root.getPath())) {
|
||||||
|
throw new SecurityException("Resolved path jumped beyond configured root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from ContentResolver.java
|
||||||
|
*/
|
||||||
|
private static int modeToMode(String mode) {
|
||||||
|
int modeBits;
|
||||||
|
if ("r".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||||
|
} else if ("wa".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_APPEND;
|
||||||
|
} else if ("rw".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE;
|
||||||
|
} else if ("rwt".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid mode: " + mode);
|
||||||
|
}
|
||||||
|
return modeBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File buildPath(File base, String... segments) {
|
||||||
|
File cur = base;
|
||||||
|
for (String segment : segments) {
|
||||||
|
if (segment != null) {
|
||||||
|
cur = new File(cur, segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] copyOf(String[] original, int newLength) {
|
||||||
|
final String[] result = new String[newLength];
|
||||||
|
System.arraycopy(original, 0, result, 0, newLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object[] copyOf(Object[] original, int newLength) {
|
||||||
|
final Object[] result = new Object[newLength];
|
||||||
|
System.arraycopy(original, 0, result, 0, newLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,9 @@ import java.util.HashSet;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
public class GlobalUserPreferences{
|
public class GlobalUserPreferences{
|
||||||
private static final String TAG="GlobalUserPreferences";
|
private static final String TAG="GlobalUserPreferences";
|
||||||
|
|
||||||
@@ -54,7 +57,6 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean spectatorMode;
|
public static boolean spectatorMode;
|
||||||
public static boolean autoHideFab;
|
public static boolean autoHideFab;
|
||||||
public static boolean allowRemoteLoading;
|
public static boolean allowRemoteLoading;
|
||||||
public static boolean forwardReportDefault;
|
|
||||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||||
public static boolean disableM3PillActiveIndicator;
|
public static boolean disableM3PillActiveIndicator;
|
||||||
public static boolean showNavigationLabels;
|
public static boolean showNavigationLabels;
|
||||||
@@ -75,15 +77,20 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean hapticFeedback;
|
public static boolean hapticFeedback;
|
||||||
public static boolean replyLineAboveHeader;
|
public static boolean replyLineAboveHeader;
|
||||||
public static boolean swapBookmarkWithBoostAction;
|
public static boolean swapBookmarkWithBoostAction;
|
||||||
public static boolean loadRemoteAccountFollowers;
|
|
||||||
public static boolean mentionRebloggerAutomatically;
|
public static boolean mentionRebloggerAutomatically;
|
||||||
public static boolean showPostsWithoutAlt;
|
public static boolean showPostsWithoutAlt;
|
||||||
public static boolean showMediaPreview;
|
public static boolean showMediaPreview;
|
||||||
|
public static boolean removeTrackingParams;
|
||||||
|
|
||||||
public static SharedPreferences getPrefs(){
|
public static SharedPreferences getPrefs(){
|
||||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences getPreReplyPrefs(){
|
||||||
|
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static <T> T fromJson(String json, Type type, T orElse){
|
public static <T> T fromJson(String json, Type type, T orElse){
|
||||||
if(json==null) return orElse;
|
if(json==null) return orElse;
|
||||||
try{
|
try{
|
||||||
@@ -129,7 +136,6 @@ public class GlobalUserPreferences{
|
|||||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||||
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
|
||||||
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
|
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
|
||||||
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
|
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
|
||||||
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
|
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
|
||||||
@@ -152,10 +158,10 @@ public class GlobalUserPreferences{
|
|||||||
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
||||||
hapticFeedback=prefs.getBoolean("hapticFeedback", true);
|
hapticFeedback=prefs.getBoolean("hapticFeedback", true);
|
||||||
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
|
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
|
||||||
loadRemoteAccountFollowers=prefs.getBoolean("loadRemoteAccountFollowers", true);
|
|
||||||
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
|
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
|
||||||
showPostsWithoutAlt=prefs.getBoolean("showPostsWithoutAlt", true);
|
showPostsWithoutAlt=prefs.getBoolean("showPostsWithoutAlt", true);
|
||||||
showMediaPreview=prefs.getBoolean("showMediaPreview", true);
|
showMediaPreview=prefs.getBoolean("showMediaPreview", true);
|
||||||
|
removeTrackingParams=prefs.getBoolean("removeTrackingParams", true);
|
||||||
|
|
||||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||||
|
|
||||||
@@ -205,7 +211,6 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("autoHideFab", autoHideFab)
|
.putBoolean("autoHideFab", autoHideFab)
|
||||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||||
.putBoolean("forwardReportDefault", forwardReportDefault)
|
|
||||||
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
|
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
|
||||||
.putBoolean("showNavigationLabels", showNavigationLabels)
|
.putBoolean("showNavigationLabels", showNavigationLabels)
|
||||||
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
|
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
|
||||||
@@ -224,7 +229,6 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||||
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
|
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
|
||||||
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
|
|
||||||
.putBoolean("hapticFeedback", hapticFeedback)
|
.putBoolean("hapticFeedback", hapticFeedback)
|
||||||
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
|
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
|
||||||
.putBoolean("showDividers", showDividers)
|
.putBoolean("showDividers", showDividers)
|
||||||
@@ -232,16 +236,46 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||||
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
|
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
|
||||||
.putBoolean("showMediaPreview", showMediaPreview)
|
.putBoolean("showMediaPreview", showMediaPreview)
|
||||||
|
.putBoolean("removeTrackingParams", removeTrackingParams)
|
||||||
|
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||||
|
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
|
||||||
|
return true;
|
||||||
|
if(account==null)
|
||||||
|
return false;
|
||||||
|
String accountKey=account.acct;
|
||||||
|
if(!accountKey.contains("@"))
|
||||||
|
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||||
|
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||||
|
String key;
|
||||||
|
if(account==null){
|
||||||
|
key="opt_out_"+type;
|
||||||
|
}else{
|
||||||
|
String accountKey=account.acct;
|
||||||
|
if(!accountKey.contains("@"))
|
||||||
|
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||||
|
key="opt_out_"+type+"_"+accountKey.toLowerCase();
|
||||||
|
}
|
||||||
|
getPreReplyPrefs().edit().putBoolean(key, true).apply();
|
||||||
|
}
|
||||||
|
|
||||||
public enum ThemePreference{
|
public enum ThemePreference{
|
||||||
AUTO,
|
AUTO,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
DARK
|
DARK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PreReplySheetType{
|
||||||
|
OLD_POST,
|
||||||
|
NON_MUTUAL
|
||||||
|
}
|
||||||
|
|
||||||
public enum AutoRevealMode {
|
public enum AutoRevealMode {
|
||||||
NEVER,
|
NEVER,
|
||||||
THREADS,
|
THREADS,
|
||||||
@@ -306,5 +340,4 @@ public class GlobalUserPreferences{
|
|||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.BadParcelableException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
@@ -110,8 +111,6 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
showFragmentClearingBackStack(fragment);
|
showFragmentClearingBackStack(fragment);
|
||||||
}
|
}
|
||||||
}else if(intent.getBooleanExtra("compose", false)){
|
|
||||||
showCompose();
|
|
||||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||||
handleURL(intent.getData(), null);
|
handleURL(intent.getData(), null);
|
||||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||||
@@ -131,11 +130,11 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
session=AccountSessionManager.get(accountID);
|
session=AccountSessionManager.get(accountID);
|
||||||
if(session==null || !session.activated)
|
if(session==null || !session.activated)
|
||||||
return;
|
return;
|
||||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
|
||||||
new GetSearchResults(q, null, true, null, 0, 0)
|
new GetSearchResults(q, type, true, null, 0, 0)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults result){
|
public void onSuccess(SearchResults result){
|
||||||
@@ -186,17 +185,6 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
showFragment(fragment);
|
showFragment(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showCompose(){
|
|
||||||
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
|
|
||||||
if(session==null || !session.activated)
|
|
||||||
return;
|
|
||||||
ComposeFragment compose=new ComposeFragment();
|
|
||||||
Bundle composeArgs=new Bundle();
|
|
||||||
composeArgs.putString("account", session.getID());
|
|
||||||
compose.setArguments(composeArgs);
|
|
||||||
showFragment(compose);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeRequestNotificationsPermission(){
|
private void maybeRequestNotificationsPermission(){
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
|
||||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
||||||
@@ -334,10 +322,14 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
if(fromNotification && hasNotification){
|
if(fromNotification && hasNotification){
|
||||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
// Parcelables might not be compatible across app versions so this protects against possible crashes
|
||||||
showFragmentForNotification(notification, session.getID());
|
// when a notification was received, then the app was updated, and then the user opened the notification
|
||||||
} else if (intent.getBooleanExtra("compose", false)){
|
try{
|
||||||
showCompose();
|
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||||
|
showFragmentForNotification(notification, session.getID());
|
||||||
|
}catch(BadParcelableException x){
|
||||||
|
Log.w(TAG, x);
|
||||||
|
}
|
||||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||||
handleURL(intent.getData(), null);
|
handleURL(intent.getData(), null);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class MastodonApp extends Application{
|
|||||||
params.diskCacheSize=100*1024*1024;
|
params.diskCacheSize=100*1024*1024;
|
||||||
params.maxMemoryCacheSize=Integer.MAX_VALUE;
|
params.maxMemoryCacheSize=Integer.MAX_VALUE;
|
||||||
ImageCache.setParams(params);
|
ImageCache.setParams(params);
|
||||||
NetworkUtils.setUserAgent("MastodonAndroid/"+BuildConfig.VERSION_NAME);
|
NetworkUtils.setUserAgent("MoshidonAndroid/"+BuildConfig.VERSION_NAME);
|
||||||
|
|
||||||
PushSubscriptionManager.tryRegisterFCM();
|
PushSubscriptionManager.tryRegisterFCM();
|
||||||
GlobalUserPreferences.load();
|
GlobalUserPreferences.load();
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
}
|
}
|
||||||
String accountID=account.getID();
|
String accountID=account.getID();
|
||||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||||
new GetNotificationByID(pn.notificationId+"")
|
new GetNotificationByID(pn.notificationId)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(org.joinmastodon.android.model.Notification result){
|
public void onSuccess(org.joinmastodon.android.model.Notification result){
|
||||||
@@ -133,7 +133,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
|
|
||||||
if(intent.hasExtra("notification")){
|
if(intent.hasExtra("notification")){
|
||||||
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||||
String statusID=notification.status.id;
|
|
||||||
|
String statusID = null;
|
||||||
|
if(notification != null && notification.status != null)
|
||||||
|
statusID=notification.status.id;
|
||||||
|
|
||||||
if (statusID != null) {
|
if (statusID != null) {
|
||||||
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
||||||
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
||||||
@@ -154,9 +158,9 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyUnifiedPush(Context context, String accountID, org.joinmastodon.android.model.Notification notification) {
|
public void notifyUnifiedPush(Context context, AccountSession account, org.joinmastodon.android.model.Notification notification) {
|
||||||
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
|
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
|
||||||
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
|
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, account, notification), account.getID(), notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public class TweakedFileProvider extends FileProvider{
|
||||||
|
private static final String TAG="TweakedFileProvider";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri){
|
||||||
|
Log.d(TAG, "getType() called with: uri = ["+uri+"]");
|
||||||
|
if(uri.getPathSegments().get(0).equals("image_cache")){
|
||||||
|
Log.i(TAG, "getType: HERE!");
|
||||||
|
return "image/jpeg"; // might as well be a png but image decoding APIs don't care, needs to be image/* though
|
||||||
|
}
|
||||||
|
return super.getType(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||||
|
Log.d(TAG, "query() called with: uri = ["+uri+"], projection = ["+Arrays.toString(projection)+"], selection = ["+selection+"], selectionArgs = ["+Arrays.toString(selectionArgs)+"], sortOrder = ["+sortOrder+"]");
|
||||||
|
return super.query(uri, projection, selection, selectionArgs, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||||
|
Log.d(TAG, "openFile() called with: uri = ["+uri+"], mode = ["+mode+"]");
|
||||||
|
return super.openFile(uri, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||||||
result.items
|
result.items
|
||||||
.stream()
|
.stream()
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
|
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, account, value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,21 +9,32 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.SearchResult;
|
import org.joinmastodon.android.model.SearchResult;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -44,6 +55,7 @@ public class CacheController{
|
|||||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||||
private boolean loadingNotifications;
|
private boolean loadingNotifications;
|
||||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||||
|
private List<FollowList> lists;
|
||||||
|
|
||||||
private static final int POST_FLAG_GAP_AFTER=1;
|
private static final int POST_FLAG_GAP_AFTER=1;
|
||||||
|
|
||||||
@@ -348,6 +360,99 @@ public class CacheController{
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reloadLists(Callback<List<FollowList>> callback){
|
||||||
|
new GetLists()
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> result){
|
||||||
|
result.sort(Comparator.comparing(l->l.title));
|
||||||
|
lists=result;
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onSuccess(result);
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onError(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FollowList> loadListsFromFile(){
|
||||||
|
File file=getListsFile();
|
||||||
|
if(!file.exists())
|
||||||
|
return null;
|
||||||
|
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||||
|
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||||
|
}catch(Exception x){
|
||||||
|
Log.w(TAG, "failed to read lists from cache file", x);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeListsToFile(){
|
||||||
|
databaseThread.postRunnable(()->{
|
||||||
|
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||||
|
MastodonAPIController.gson.toJson(lists, out);
|
||||||
|
}catch(IOException x){
|
||||||
|
Log.w(TAG, "failed to write lists to cache file", x);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getLists(Callback<List<FollowList>> callback){
|
||||||
|
if(lists!=null){
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onSuccess(lists);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
databaseThread.postRunnable(()->{
|
||||||
|
List<FollowList> lists=loadListsFromFile();
|
||||||
|
if(lists!=null){
|
||||||
|
this.lists=lists;
|
||||||
|
if(callback!=null)
|
||||||
|
uiHandler.post(()->callback.onSuccess(lists));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reloadLists(callback);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getListsFile(){
|
||||||
|
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addList(FollowList list){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
lists.add(list);
|
||||||
|
lists.sort(Comparator.comparing(l->l.title));
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteList(String id){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
lists.removeIf(l->l.id.equals(id));
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateList(FollowList list){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
for(int i=0;i<lists.size();i++){
|
||||||
|
if(lists.get(i).id.equals(list.id)){
|
||||||
|
lists.set(i, list);
|
||||||
|
lists.sort(Comparator.comparing(l->l.title));
|
||||||
|
writeListsToFile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class DatabaseHelper extends SQLiteOpenHelper{
|
private class DatabaseHelper extends SQLiteOpenHelper{
|
||||||
|
|
||||||
public DatabaseHelper(){
|
public DatabaseHelper(){
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ public class MastodonAPIController{
|
|||||||
.create();
|
.create();
|
||||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private AccountSession session;
|
private AccountSession session;
|
||||||
@@ -89,7 +91,11 @@ public class MastodonAPIController{
|
|||||||
final boolean isBad = host == null || badDomains.stream().anyMatch(h -> h.equalsIgnoreCase(host) || host.toLowerCase().endsWith("." + h));
|
final boolean isBad = host == null || badDomains.stream().anyMatch(h -> h.equalsIgnoreCase(host) || host.toLowerCase().endsWith("." + h));
|
||||||
thread.postRunnable(()->{
|
thread.postRunnable(()->{
|
||||||
try{
|
try{
|
||||||
// if (isBad) throw new IllegalArgumentException();
|
if(isBad){
|
||||||
|
Log.i(TAG, "submitRequest: refusing to connect to bad domain: " + host);
|
||||||
|
throw new IllegalArgumentException("Failed to connect to domain");
|
||||||
|
}
|
||||||
|
|
||||||
if(req.canceled)
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
Request.Builder builder=new Request.Builder()
|
Request.Builder builder=new Request.Builder()
|
||||||
@@ -122,15 +128,15 @@ public class MastodonAPIController{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
|
||||||
|
|
||||||
call.enqueue(new Callback(){
|
call.enqueue(new Callback(){
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||||
if(call.isCanceled())
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
@@ -139,10 +145,10 @@ public class MastodonAPIController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||||
if(call.isCanceled())
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
@@ -153,7 +159,7 @@ public class MastodonAPIController{
|
|||||||
try{
|
try{
|
||||||
if(BuildConfig.DEBUG){
|
if(BuildConfig.DEBUG){
|
||||||
JsonElement respJson=JsonParser.parseReader(reader);
|
JsonElement respJson=JsonParser.parseReader(reader);
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
Log.d(TAG, logTag(session)+"response body: "+respJson);
|
||||||
if(req.respTypeToken!=null)
|
if(req.respTypeToken!=null)
|
||||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||||
else if(req.respClass!=null)
|
else if(req.respClass!=null)
|
||||||
@@ -175,7 +181,7 @@ public class MastodonAPIController{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,19 +190,19 @@ public class MastodonAPIController{
|
|||||||
req.validateAndPostprocessResponse(respObj, response);
|
req.validateAndPostprocessResponse(respObj, response);
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
|
||||||
|
|
||||||
req.onSuccess(respObj);
|
req.onSuccess(respObj);
|
||||||
}else{
|
}else{
|
||||||
try{
|
try{
|
||||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
Log.w(TAG, logTag(session)+response+" received error: "+error);
|
||||||
if(error.has("details")){
|
if(error.has("details")){
|
||||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||||
@@ -231,7 +237,7 @@ public class MastodonAPIController{
|
|||||||
});
|
});
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
|
||||||
req.onError(x.getLocalizedMessage(), 0, x);
|
req.onError(x.getLocalizedMessage(), 0, x);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -244,4 +250,8 @@ public class MastodonAPIController{
|
|||||||
public static OkHttpClient getHttpClient(){
|
public static OkHttpClient getHttpClient(){
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String logTag(AccountSession session){
|
||||||
|
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
|
if(requestBody instanceof RequestBody rb)
|
||||||
|
return rb;
|
||||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
|
import org.joinmastodon.android.model.BaseModel;
|
||||||
|
|
||||||
|
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
|
||||||
|
public CheckInviteLink(String path){
|
||||||
|
super(HttpMethod.GET, path, Response.class);
|
||||||
|
addHeader("Accept", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPathPrefix(){
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response extends BaseModel{
|
||||||
|
@RequiredField
|
||||||
|
public String inviteCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
|
||||||
|
public GetAccountLists(String id){
|
||||||
|
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
|
||||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
|
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
|
||||||
super(HttpMethod.POST, "/accounts", Token.class);
|
super(HttpMethod.POST, "/accounts", Token.class);
|
||||||
setRequestBody(new Body(username, email, password, locale, reason, timezone));
|
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Body{
|
private static class Body{
|
||||||
public String username, email, password, locale, reason, timeZone;
|
public String username, email, password, locale, reason, timeZone, inviteCode;
|
||||||
public boolean agreement=true;
|
public boolean agreement=true;
|
||||||
|
|
||||||
public Body(String username, String email, String password, String locale, String reason, String timeZone){
|
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
|
||||||
this.username=username;
|
this.username=username;
|
||||||
this.email=email;
|
this.email=email;
|
||||||
this.password=password;
|
this.password=password;
|
||||||
this.locale=locale;
|
this.locale=locale;
|
||||||
this.reason=reason;
|
this.reason=reason;
|
||||||
this.timeZone=timeZone;
|
this.timeZone=timeZone;
|
||||||
|
this.inviteCode=inviteCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
|
||||||
|
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
|
||||||
|
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
|
||||||
|
addQueryParameter("q", q);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
if(offset>0)
|
||||||
|
addQueryParameter("offset", offset+"");
|
||||||
|
if(resolve)
|
||||||
|
addQueryParameter("resolve", "true");
|
||||||
|
if(following)
|
||||||
|
addQueryParameter("following", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,21 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
|
|
||||||
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
|
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
|
||||||
public SetAccountMuted(String id, boolean muted, long duration){
|
public SetAccountMuted(String id, boolean muted, long duration, boolean muteNotifications){
|
||||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
|
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
|
||||||
setRequestBody(new Request(duration));
|
if(muted)
|
||||||
|
setRequestBody(new Request(duration, muteNotifications));
|
||||||
|
else{
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Request{
|
private static class Request{
|
||||||
public long duration;
|
public long duration;
|
||||||
public Request(long duration){
|
public boolean muteNotifications;
|
||||||
|
public Request(long duration, boolean muteNotifications){
|
||||||
this.duration=duration;
|
this.duration=duration;
|
||||||
|
this.muteNotifications=muteNotifications;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
private Uri avatar, cover;
|
private Uri avatar, cover;
|
||||||
private File avatarFile, coverFile;
|
private File avatarFile, coverFile;
|
||||||
private List<AccountField> fields;
|
private List<AccountField> fields;
|
||||||
|
private Boolean discoverable, indexable;
|
||||||
|
|
||||||
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
||||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||||
@@ -41,6 +42,12 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
this.fields=fields;
|
this.fields=fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
|
||||||
|
this.discoverable=discoverable;
|
||||||
|
this.indexable=indexable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||||
@@ -58,15 +65,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
}else if(coverFile!=null){
|
}else if(coverFile!=null){
|
||||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||||
}
|
}
|
||||||
if(fields.isEmpty()){
|
if(fields!=null){
|
||||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
if(fields.isEmpty()){
|
||||||
}else{
|
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||||
int i=0;
|
}else{
|
||||||
for(AccountField field:fields){
|
int i=0;
|
||||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
for(AccountField field:fields){
|
||||||
i++;
|
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(discoverable!=null)
|
||||||
|
bldr.addFormDataPart("discoverable", discoverable.toString());
|
||||||
|
if(indexable!=null)
|
||||||
|
bldr.addFormDataPart("indexable", indexable.toString());
|
||||||
|
|
||||||
return bldr.build();
|
return bldr.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
|
|||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
|
@Keep
|
||||||
class KeywordAttribute{
|
class KeywordAttribute{
|
||||||
public String id;
|
public String id;
|
||||||
@SerializedName("_destroy")
|
@SerializedName("_destroy")
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class AddAccountsToList extends MastodonAPIRequest<Object> {
|
import java.nio.charset.StandardCharsets;
|
||||||
public AddAccountsToList(String listId, List<String> accountIds){
|
import java.util.Collection;
|
||||||
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
|
|
||||||
Request req = new Request();
|
|
||||||
req.accountIds = accountIds;
|
|
||||||
setRequestBody(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Request{
|
import okhttp3.FormBody;
|
||||||
public List<String> accountIds;
|
|
||||||
}
|
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
|
||||||
|
public AddAccountsToList(String listID, Collection<String> accountIDs){
|
||||||
|
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
|
||||||
|
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||||
|
for(String id:accountIDs){
|
||||||
|
builder.add("account_ids[]", id);
|
||||||
|
}
|
||||||
|
setRequestBody(builder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class CreateList extends MastodonAPIRequest<ListTimeline> {
|
public class CreateList extends MastodonAPIRequest<FollowList>{
|
||||||
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
super(HttpMethod.POST, "/lists", FollowList.class);
|
||||||
Request req = new Request();
|
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||||
req.title = title;
|
|
||||||
req.exclusive = exclusive;
|
|
||||||
req.repliesPolicy = repliesPolicy;
|
|
||||||
setRequestBody(req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Request {
|
private static class Request{
|
||||||
public String title;
|
public String title;
|
||||||
|
public FollowList.RepliesPolicy repliesPolicy;
|
||||||
public boolean exclusive;
|
public boolean exclusive;
|
||||||
public ListTimeline.RepliesPolicy repliesPolicy;
|
|
||||||
|
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
|
this.title=title;
|
||||||
|
this.repliesPolicy=repliesPolicy;
|
||||||
|
this.exclusive=exclusive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
|
||||||
|
|
||||||
public class DeleteList extends MastodonAPIRequest<Object> {
|
public class DeleteList extends ResultlessMastodonAPIRequest{
|
||||||
public DeleteList(String id) {
|
public DeleteList(String id){
|
||||||
super(HttpMethod.DELETE, "/lists/" + id, Object.class);
|
super(HttpMethod.DELETE, "/lists/"+id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class GetList extends MastodonAPIRequest<ListTimeline> {
|
public class GetList extends MastodonAPIRequest<FollowList> {
|
||||||
public GetList(String id) {
|
public GetList(String id) {
|
||||||
super(HttpMethod.GET, "/lists/" + id, ListTimeline.class);
|
super(HttpMethod.GET, "/lists/" + id, FollowList.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
public class GetListAccounts extends HeaderPaginationRequest<Account>{
|
||||||
|
public GetListAccounts(String listID, String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/lists/"+listID+"/accounts", new TypeToken<>(){});
|
||||||
|
if(!TextUtils.isEmpty(maxID))
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
addQueryParameter("limit", String.valueOf(limit));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ package org.joinmastodon.android.api.requests.lists;
|
|||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
|
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
|
||||||
public GetLists() {
|
public GetLists() {
|
||||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
|
import java.nio.charset.StandardCharsets;
|
||||||
public RemoveAccountsFromList(String listId, List<String> accountIds){
|
import java.util.Collection;
|
||||||
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
|
|
||||||
Request req = new Request();
|
|
||||||
req.accountIds = accountIds;
|
|
||||||
setRequestBody(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Request{
|
import okhttp3.FormBody;
|
||||||
public List<String> accountIds;
|
|
||||||
}
|
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
|
||||||
|
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
|
||||||
|
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
|
||||||
|
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||||
|
for(String id:accountIDs){
|
||||||
|
builder.add("account_ids[]", id);
|
||||||
|
}
|
||||||
|
setRequestBody(builder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
|
public class UpdateList extends MastodonAPIRequest<FollowList>{
|
||||||
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
|
||||||
CreateList.Request req = new CreateList.Request();
|
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||||
req.title = title;
|
}
|
||||||
req.exclusive = exclusive;
|
|
||||||
req.repliesPolicy = repliesPolicy;
|
private static class Request{
|
||||||
setRequestBody(req);
|
public String title;
|
||||||
|
public FollowList.RepliesPolicy repliesPolicy;
|
||||||
|
public boolean exclusive;
|
||||||
|
|
||||||
|
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
|
this.title=title;
|
||||||
|
this.repliesPolicy=repliesPolicy;
|
||||||
|
this.exclusive=exclusive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
|||||||
s.visibility=StatusPrivacy.PUBLIC;
|
s.visibility=StatusPrivacy.PUBLIC;
|
||||||
s.mentions=Collections.emptyList();
|
s.mentions=Collections.emptyList();
|
||||||
s.tags=Collections.emptyList();
|
s.tags=Collections.emptyList();
|
||||||
if (s.poll != null)
|
if(s.poll!=null){
|
||||||
s.poll.id="fakeID"+i;
|
s.poll.id="fakeID"+i;
|
||||||
|
s.poll.emojis=Collections.emptyList();
|
||||||
|
s.poll.ownVotes=Collections.emptyList();
|
||||||
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.tags;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
|
|
||||||
|
public class GetFollowedTags extends HeaderPaginationRequest<Hashtag>{
|
||||||
|
public GetFollowedTags(String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
|
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID, String replyVisibility){
|
||||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||||
if(local)
|
if(local)
|
||||||
addQueryParameter("local", "true");
|
addQueryParameter("local", "true");
|
||||||
@@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
|||||||
addQueryParameter("remote", "true");
|
addQueryParameter("remote", "true");
|
||||||
if(!TextUtils.isEmpty(maxID))
|
if(!TextUtils.isEmpty(maxID))
|
||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(!TextUtils.isEmpty(minID))
|
||||||
|
addQueryParameter("min_id", minID);
|
||||||
|
if(!TextUtils.isEmpty(sinceID))
|
||||||
|
addQueryParameter("since_id", sinceID);
|
||||||
if(limit>0)
|
if(limit>0)
|
||||||
addQueryParameter("limit", limit+"");
|
addQueryParameter("limit", limit+"");
|
||||||
if(replyVisibility != null)
|
if(replyVisibility != null)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.timelines;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GetTrendingLinksTimeline extends MastodonAPIRequest<List<Status>>{
|
||||||
|
public GetTrendingLinksTimeline(@NonNull String url, String maxID, String minID, int limit){
|
||||||
|
super(HttpMethod.GET, "/timelines/link/", new TypeToken<>(){});
|
||||||
|
addQueryParameter("url", url);
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(minID!=null)
|
||||||
|
addQueryParameter("min_id", minID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", ""+limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ public class AccountLocalPreferences{
|
|||||||
public ShowEmojiReactions showEmojiReactions;
|
public ShowEmojiReactions showEmojiReactions;
|
||||||
public ColorPreference color;
|
public ColorPreference color;
|
||||||
public ArrayList<Emoji> recentCustomEmoji;
|
public ArrayList<Emoji> recentCustomEmoji;
|
||||||
|
public boolean preReplySheet;
|
||||||
|
|
||||||
private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
|
private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
|
||||||
private final static Type timelinesType=new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
private final static Type timelinesType=new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
||||||
@@ -68,6 +69,7 @@ public class AccountLocalPreferences{
|
|||||||
revealCWs=prefs.getBoolean("revealCWs", false);
|
revealCWs=prefs.getBoolean("revealCWs", false);
|
||||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||||
|
// preReplySheet=prefs.getBoolean("preReplySheet", false);
|
||||||
|
|
||||||
// MEGALODON
|
// MEGALODON
|
||||||
showReplies=prefs.getBoolean("showReplies", true);
|
showReplies=prefs.getBoolean("showReplies", true);
|
||||||
@@ -112,6 +114,9 @@ public class AccountLocalPreferences{
|
|||||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||||
|
|
||||||
|
//TODO figure this stuff out
|
||||||
|
// .putBoolean("preReplySheet", preReplySheet)
|
||||||
|
|
||||||
// MEGALODON
|
// MEGALODON
|
||||||
.putBoolean("showReplies", showReplies)
|
.putBoolean("showReplies", showReplies)
|
||||||
.putBoolean("showBoosts", showBoosts)
|
.putBoolean("showBoosts", showBoosts)
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
|||||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.AltTextFilter;
|
||||||
import org.joinmastodon.android.model.Application;
|
import org.joinmastodon.android.model.Application;
|
||||||
import org.joinmastodon.android.model.FilterAction;
|
import org.joinmastodon.android.model.FilterAction;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.FilterResult;
|
import org.joinmastodon.android.model.FilterResult;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.LegacyFilter;
|
import org.joinmastodon.android.model.LegacyFilter;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
@@ -34,7 +36,9 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
|||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -70,6 +74,7 @@ public class AccountSession{
|
|||||||
private transient SharedPreferences prefs;
|
private transient SharedPreferences prefs;
|
||||||
private transient boolean preferencesNeedSaving;
|
private transient boolean preferencesNeedSaving;
|
||||||
private transient AccountLocalPreferences localPreferences;
|
private transient AccountLocalPreferences localPreferences;
|
||||||
|
private transient List<FollowList> lists;
|
||||||
|
|
||||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||||
this.token=token;
|
this.token=token;
|
||||||
@@ -310,8 +315,11 @@ public class AccountSession{
|
|||||||
return true;
|
return true;
|
||||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||||
if(getLocalPreferences().serverSideFiltersSupported){
|
if(getLocalPreferences().serverSideFiltersSupported){
|
||||||
|
// Moshidon: this code path in CustomLocalTimelines makes the app crash, so this check is here
|
||||||
|
if (s.filtered == null)
|
||||||
|
return false;
|
||||||
for(FilterResult filter : s.filtered){
|
for(FilterResult filter : s.filtered){
|
||||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE && filter.filter.context.contains(context))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}else if(wordFilters!=null){
|
}else if(wordFilters!=null){
|
||||||
@@ -323,6 +331,21 @@ public class AccountSession{
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<FilterResult> getClientSideFilters(Status status) {
|
||||||
|
List<FilterResult> filters = new ArrayList<>();
|
||||||
|
|
||||||
|
// filter post that have no alt text
|
||||||
|
// it only applies when activated in the settings
|
||||||
|
AltTextFilter altTextFilter=new AltTextFilter(FilterAction.WARN, EnumSet.allOf(FilterContext.class));
|
||||||
|
if(altTextFilter.matches(status)){
|
||||||
|
FilterResult filterResult=new FilterResult();
|
||||||
|
filterResult.filter=altTextFilter;
|
||||||
|
filterResult.keywordMatches=List.of();
|
||||||
|
filters.add(filterResult);
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
public void updateAccountInfo(){
|
public void updateAccountInfo(){
|
||||||
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||||
}
|
}
|
||||||
@@ -343,4 +366,12 @@ public class AccountSession{
|
|||||||
.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
|
.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
|
||||||
.orElse("");
|
.orElse("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isNotificationsMentionsOnly(){
|
||||||
|
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||||
|
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.joinmastodon.android.api.session;
|
package org.joinmastodon.android.api.session;
|
||||||
|
|
||||||
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -17,7 +15,7 @@ import android.util.Log;
|
|||||||
|
|
||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.ChooseAccountForComposeActivity;
|
||||||
import org.joinmastodon.android.MainActivity;
|
import org.joinmastodon.android.MainActivity;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -96,6 +94,7 @@ public class AccountSessionManager{
|
|||||||
|
|
||||||
private AccountSessionManager(){
|
private AccountSessionManager(){
|
||||||
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
||||||
|
// This file should not be backed up, otherwise the app may start with accounts already logged in. See res/xml/backup_rules.xml
|
||||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||||
if(!file.exists())
|
if(!file.exists())
|
||||||
return;
|
return;
|
||||||
@@ -216,12 +215,17 @@ public class AccountSessionManager{
|
|||||||
public void removeAccount(String id){
|
public void removeAccount(String id){
|
||||||
AccountSession session=getAccount(id);
|
AccountSession session=getAccount(id);
|
||||||
session.getCacheController().closeDatabase();
|
session.getCacheController().closeDatabase();
|
||||||
|
session.getCacheController().getListsFile().delete();
|
||||||
MastodonApp.context.deleteDatabase(id+".db");
|
MastodonApp.context.deleteDatabase(id+".db");
|
||||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||||
MastodonApp.context.deleteSharedPreferences(id);
|
MastodonApp.context.deleteSharedPreferences(id);
|
||||||
}else{
|
}else{
|
||||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
String dataDir=MastodonApp.context.getApplicationInfo().dataDir;
|
||||||
|
if(dataDir!=null){
|
||||||
|
File prefsDir=new File(dataDir, "shared_prefs");
|
||||||
|
new File(prefsDir, id+".xml").delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessions.remove(id);
|
sessions.remove(id);
|
||||||
if(lastActiveAccountID.equals(id)){
|
if(lastActiveAccountID.equals(id)){
|
||||||
@@ -485,15 +489,19 @@ public class AccountSessionManager{
|
|||||||
if(Build.VERSION.SDK_INT<26)
|
if(Build.VERSION.SDK_INT<26)
|
||||||
return;
|
return;
|
||||||
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
|
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
|
||||||
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
|
|
||||||
|
Intent intent = new Intent(MastodonApp.context, ChooseAccountForComposeActivity.class)
|
||||||
|
.setAction(Intent.ACTION_CHOOSER)
|
||||||
|
.putExtra("compose", true);
|
||||||
|
|
||||||
|
// This was done so that the old shortcuts get updated to the new implementation.
|
||||||
|
if((sm.getDynamicShortcuts().isEmpty() || sm.getDynamicShortcuts().get(0).getIntent() != intent || BuildConfig.DEBUG ) && !sessions.isEmpty()){
|
||||||
// There are no shortcuts, but there are accounts. Add a compose shortcut.
|
// There are no shortcuts, but there are accounts. Add a compose shortcut.
|
||||||
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
||||||
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
||||||
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
|
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
|
||||||
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
|
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
|
||||||
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
|
.setIntent(intent)
|
||||||
.setAction(Intent.ACTION_MAIN)
|
|
||||||
.putExtra("compose", true))
|
|
||||||
.build();
|
.build();
|
||||||
sm.setDynamicShortcuts(Collections.singletonList(info));
|
sm.setDynamicShortcuts(Collections.singletonList(info));
|
||||||
}else if(sessions.isEmpty()){
|
}else if(sessions.isEmpty()){
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
public class AccountAddedToListEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final String listID;
|
||||||
|
public final Account account;
|
||||||
|
|
||||||
|
public AccountAddedToListEvent(String accountID, String listID, Account account){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.listID=listID;
|
||||||
|
this.account=account;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class AccountRemovedFromListEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final String listID;
|
||||||
|
public final String targetAccountID;
|
||||||
|
|
||||||
|
public AccountRemovedFromListEvent(String accountID, String listID, String targetAccountID){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.listID=listID;
|
||||||
|
this.targetAccountID=targetAccountID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class FinishListCreationFragmentEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final String listID;
|
||||||
|
|
||||||
|
public FinishListCreationFragmentEvent(String accountID, String listID){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.listID=listID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
|
public class ListCreatedEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final FollowList list;
|
||||||
|
|
||||||
|
public ListCreatedEvent(String accountID, FollowList list){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.list=list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.joinmastodon.android.events;
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
public class ListDeletedEvent {
|
public class ListDeletedEvent{
|
||||||
public final String id;
|
public final String accountID;
|
||||||
|
public final String listID;
|
||||||
|
|
||||||
public ListDeletedEvent(String id) {
|
public ListDeletedEvent(String accountID, String listID){
|
||||||
this.id = id;
|
this.accountID=accountID;
|
||||||
|
this.listID=listID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package org.joinmastodon.android.events;
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class ListUpdatedCreatedEvent {
|
public class ListUpdatedCreatedEvent {
|
||||||
public final String id;
|
public final String id;
|
||||||
public final String title;
|
public final String title;
|
||||||
public final ListTimeline.RepliesPolicy repliesPolicy;
|
public final FollowList.RepliesPolicy repliesPolicy;
|
||||||
public final boolean exclusive;
|
public final boolean exclusive;
|
||||||
|
|
||||||
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, FollowList.RepliesPolicy repliesPolicy) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.exclusive = exclusive;
|
this.exclusive = exclusive;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
|
public class ListUpdatedEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final FollowList list;
|
||||||
|
|
||||||
|
public ListUpdatedEvent(String accountID, FollowList list){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.list=list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||||
|
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||||
|
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
|
||||||
|
private Account account;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.add_user_to_list_title);
|
||||||
|
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> allLists){
|
||||||
|
if(getActivity()==null)
|
||||||
|
return;
|
||||||
|
loadAccountLists(allLists);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAccountLists(final List<FollowList> allLists){
|
||||||
|
currentRequest=new GetAccountLists(account.id)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> result){
|
||||||
|
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
|
||||||
|
onDataLoaded(allLists.stream()
|
||||||
|
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
|
||||||
|
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
|
||||||
|
.collect(Collectors.toList()), false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int indexOfItemsAdapter(){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter<?> getAdapter(){
|
||||||
|
TextView topText=new TextView(getActivity());
|
||||||
|
topText.setTextAppearance(R.style.m3_body_medium);
|
||||||
|
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||||
|
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
|
||||||
|
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
|
||||||
|
|
||||||
|
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
|
||||||
|
mergeAdapter.addAdapter(super.getAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onItemClick(CheckableListItem<FollowList> item){
|
||||||
|
boolean add=!item.checked;
|
||||||
|
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
|
||||||
|
req.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
item.checked=add;
|
||||||
|
rebindItem(item);
|
||||||
|
if(add){
|
||||||
|
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
|
||||||
|
}else{
|
||||||
|
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, false)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||||
|
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||||
|
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.APIRequest;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
|
||||||
|
protected FollowList followList;
|
||||||
|
protected AvatarPileListItem<Void> membersItem;
|
||||||
|
protected CheckableListItem<Void> exclusiveItem;
|
||||||
|
protected FloatingHintEditTextLayout titleEditLayout;
|
||||||
|
protected EditText titleEdit;
|
||||||
|
protected Spinner showRepliesSpinner;
|
||||||
|
private APIRequest<?> getMembersRequest;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||||
|
|
||||||
|
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
|
||||||
|
List<ListItem<Void>> items=new ArrayList<>();
|
||||||
|
if(followList!=null){
|
||||||
|
items.add(membersItem);
|
||||||
|
}
|
||||||
|
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
|
||||||
|
items.add(exclusiveItem);
|
||||||
|
onDataLoaded(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
if(getMembersRequest!=null)
|
||||||
|
getMembersRequest.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter<?> getAdapter(){
|
||||||
|
LinearLayout topView=new LinearLayout(getActivity());
|
||||||
|
topView.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
|
||||||
|
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||||
|
titleEdit.setHint(R.string.list_name);
|
||||||
|
titleEditLayout.updateHint();
|
||||||
|
if(followList!=null)
|
||||||
|
titleEdit.setText(followList.title);
|
||||||
|
topView.addView(titleEditLayout);
|
||||||
|
|
||||||
|
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
|
||||||
|
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
|
||||||
|
showRepliesLayout.setHint(R.string.list_show_replies_to);
|
||||||
|
topView.addView(showRepliesLayout);
|
||||||
|
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
|
||||||
|
getString(R.string.list_replies_no_one),
|
||||||
|
getString(R.string.list_replies_members),
|
||||||
|
getString(R.string.list_replies_anyone)
|
||||||
|
));
|
||||||
|
showRepliesSpinner.setAdapter(spinnerAdapter);
|
||||||
|
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
|
||||||
|
case FOLLOWED -> 2;
|
||||||
|
case LIST -> 1;
|
||||||
|
case NONE -> 0;
|
||||||
|
});
|
||||||
|
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
|
||||||
|
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
|
||||||
|
|
||||||
|
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||||
|
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
|
||||||
|
adapter.addAdapter(super.getAdapter());
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int indexOfItemsAdapter(){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void doDeleteList(){
|
||||||
|
new DeleteList(followList.id)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().deleteList(followList.id);
|
||||||
|
E.post(new ListDeletedEvent(accountID, followList.id));
|
||||||
|
Nav.finish(BaseEditListFragment.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
Activity activity=getActivity();
|
||||||
|
if(activity==null)
|
||||||
|
return;
|
||||||
|
error.showToast(activity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMembersClick(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void loadMembers(){
|
||||||
|
getMembersRequest=new GetListAccounts(followList.id, null, 3)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
getMembersRequest=null;
|
||||||
|
membersItem.avatars=new ArrayList<>();
|
||||||
|
for(int i=0;i<Math.min(3, result.size());i++){
|
||||||
|
Account acc=result.get(i);
|
||||||
|
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
|
||||||
|
}
|
||||||
|
rebindItem(membersItem);
|
||||||
|
imgLoader.updateImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
getMembersRequest=null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
|
||||||
|
return switch(showRepliesSpinner.getSelectedItemPosition()){
|
||||||
|
case 0 -> FollowList.RepliesPolicy.NONE;
|
||||||
|
case 1 -> FollowList.RepliesPolicy.LIST;
|
||||||
|
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import android.graphics.Rect;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
@@ -23,23 +24,26 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.CacheController;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||||
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
||||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||||
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.Poll;
|
import org.joinmastodon.android.model.Poll;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.Translation;
|
import org.joinmastodon.android.model.Translation;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.NonMutualPreReplySheet;
|
||||||
|
import org.joinmastodon.android.ui.OldPostPreReplySheet;
|
||||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
@@ -63,8 +67,9 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
|||||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -73,14 +78,9 @@ import java.util.Set;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||||
@@ -145,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
for(T s:items){
|
for(T s:items){
|
||||||
displayItems.addAll(buildDisplayItems(s));
|
displayItems.addAll(buildDisplayItems(s));
|
||||||
}
|
}
|
||||||
|
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -166,6 +167,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
if(notify)
|
if(notify)
|
||||||
adapter.notifyItemRangeInserted(0, offset);
|
adapter.notifyItemRangeInserted(0, offset);
|
||||||
|
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
||||||
final Status status=_status.getContentStatus();
|
final Status status=_status.getContentStatus();
|
||||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||||
private MediaAttachmentViewController transitioningHolder;
|
private MediaAttachmentViewController transitioningHolder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -288,6 +290,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public void photoViewerDismissed(){
|
public void photoViewerDismissed(){
|
||||||
currentPhotoViewer=null;
|
currentPhotoViewer=null;
|
||||||
|
gridHolder.itemView.setHasTransientState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -299,12 +302,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return gridHolder.getViewController(index);
|
return gridHolder.getViewController(index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
gridHolder.itemView.setHasTransientState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
|
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
|
||||||
final Status status=_status.getContentStatus();
|
final Status status=_status.getContentStatus();
|
||||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||||
private PreviewlessMediaAttachmentViewController transitioningHolder;
|
private PreviewlessMediaAttachmentViewController transitioningHolder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -574,6 +578,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a temporary measure to deal with the app crashing when the poll isn't updated.
|
||||||
|
// This is needed because of a possible id mismatch that screws with things
|
||||||
|
if(firstOptionIndex==-1 || footerIndex==-1){
|
||||||
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
if(status.id.equals(itemID)){
|
||||||
|
if(item instanceof SpoilerStatusDisplayItem){
|
||||||
|
spoilerItem=(SpoilerStatusDisplayItem) item;
|
||||||
|
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||||
|
firstOptionIndex=i;
|
||||||
|
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||||
|
footerIndex=i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(firstOptionIndex==-1 || footerIndex==-1)
|
if(firstOptionIndex==-1 || footerIndex==-1)
|
||||||
throw new IllegalStateException("Can't find all poll items in displayItems");
|
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||||
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||||
@@ -635,11 +658,30 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onPollViewResultsButtonClick(PollFooterStatusDisplayItem.Holder holder, boolean shown){
|
public void onPollViewResultsButtonClick(PollFooterStatusDisplayItem.Holder holder, boolean shown){
|
||||||
for(int i=0;i<list.getChildCount();i++){
|
int firstOptionIndex=-1, footerIndex=-1;
|
||||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item && item.getItemID().equals(holder.getItemID())){
|
int i=0;
|
||||||
item.showResults(shown);
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
if(item.parentID.equals(holder.getItemID())){
|
||||||
|
if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||||
|
firstOptionIndex=i;
|
||||||
|
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||||
|
footerIndex=i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if(firstOptionIndex==-1 || footerIndex==-1)
|
||||||
|
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||||
|
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||||
|
|
||||||
|
for(StatusDisplayItem item:pollItems){
|
||||||
|
if (item instanceof PollOptionStatusDisplayItem) {
|
||||||
|
((PollOptionStatusDisplayItem) item).isAnimating=true;
|
||||||
|
((PollOptionStatusDisplayItem) item).showResults=shown;
|
||||||
|
adapter.notifyItemRangeChanged(firstOptionIndex, pollItems.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void submitPollVote(String parentID, String pollID, List<Integer> choices){
|
protected void submitPollVote(String parentID, String pollID, List<Integer> choices){
|
||||||
@@ -667,6 +709,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
toggleSpoiler(status, isForQuote, holder.getItemID());
|
toggleSpoiler(status, isForQuote, holder.getItemID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateStatusWithQuote(DisplayItemsParent parent) {
|
||||||
|
Pair<Integer, Integer> items=findAllItemsOfParent(parent);
|
||||||
|
if (items==null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Only StatusListFragments/NotificationsListFragments can display status with quotes
|
||||||
|
assert (this instanceof StatusListFragment) || (this instanceof NotificationsListFragment);
|
||||||
|
List<StatusDisplayItem> oldItems = displayItems.subList(items.first, items.second+1);
|
||||||
|
List<StatusDisplayItem> newItems=this.buildDisplayItems((T) parent);
|
||||||
|
int prevSize=oldItems.size();
|
||||||
|
oldItems.clear();
|
||||||
|
displayItems.addAll(items.first, newItems);
|
||||||
|
|
||||||
|
// Update the cache
|
||||||
|
final CacheController cache=AccountSessionManager.get(accountID).getCacheController();
|
||||||
|
if (parent instanceof Status) {
|
||||||
|
cache.updateStatus((Status) parent);
|
||||||
|
} else if (parent instanceof Notification) {
|
||||||
|
cache.updateNotification((Notification) parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyItemRangeRemoved(items.first, prevSize);
|
||||||
|
adapter.notifyItemRangeInserted(items.first, newItems.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeStatus(DisplayItemsParent parent) {
|
||||||
|
Pair<Integer, Integer> items=findAllItemsOfParent(parent);
|
||||||
|
if (items==null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<StatusDisplayItem> statusDisplayItems = displayItems.subList(items.first, items.second+1);
|
||||||
|
int prevSize=statusDisplayItems.size();
|
||||||
|
statusDisplayItems.clear();
|
||||||
|
adapter.notifyItemRangeRemoved(items.first, prevSize);
|
||||||
|
}
|
||||||
|
|
||||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
||||||
Status status = holder.getItem().status;
|
Status status = holder.getItem().status;
|
||||||
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
|
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
|
||||||
@@ -702,6 +780,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||||
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
||||||
}else{
|
}else{
|
||||||
|
if(spoilers.size()>1 && !isForQuote && status.quote.spoilerRevealed)
|
||||||
|
toggleSpoiler(status.quote, true, itemID);
|
||||||
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
||||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||||
}
|
}
|
||||||
@@ -714,19 +794,33 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
list.invalidateItemDecorations();
|
list.invalidateItemDecorations();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable, boolean isForQuote) {
|
||||||
Status s=holder.getItem().status;
|
Status s=holder.getItem().status;
|
||||||
if(s.textExpandable!=expandable && list!=null) {
|
if(s.textExpandable!=expandable && list!=null) {
|
||||||
s.textExpandable=expandable;
|
s.textExpandable=expandable;
|
||||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
List<HeaderStatusDisplayItem.Holder> headers=findAllHoldersOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||||
if(header!=null) header.bindCollapseButton();
|
if(headers!=null && !headers.isEmpty()){
|
||||||
|
HeaderStatusDisplayItem.Holder header=headers.size() > 1 && isForQuote ? headers.get(1) : headers.get(0);
|
||||||
|
if(header!=null) header.bindCollapseButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onToggleExpanded(Status status, String itemID) {
|
public void onToggleExpanded(Status status, boolean isForQuote, String itemID) {
|
||||||
status.textExpanded = !status.textExpanded;
|
status.textExpanded = !status.textExpanded;
|
||||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
// TODO: simplify this to a single case
|
||||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
if(!isForQuote)
|
||||||
|
// using the adapter directly to update the item does not work for non-quoted texts
|
||||||
|
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||||
|
else{
|
||||||
|
List<TextStatusDisplayItem.Holder> textItems=findAllHoldersOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||||
|
TextStatusDisplayItem.Holder text=textItems.size()>1 ? textItems.get(1) : textItems.get(0);
|
||||||
|
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||||
|
}
|
||||||
|
List<HeaderStatusDisplayItem.Holder> headers=findAllHoldersOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||||
|
if (headers.isEmpty())
|
||||||
|
return;
|
||||||
|
HeaderStatusDisplayItem.Holder header=headers.size() > 1 && isForQuote ? headers.get(1) : headers.get(0);
|
||||||
if(header!=null) header.animateExpandToggle();
|
if(header!=null) header.animateExpandToggle();
|
||||||
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||||
}
|
}
|
||||||
@@ -734,12 +828,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
|
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
|
||||||
|
|
||||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||||
int startPos = warning.getAbsoluteAdapterPosition();
|
WarningFilteredStatusDisplayItem filterItem=findItemOfType(warning.getItemID(), WarningFilteredStatusDisplayItem.class);
|
||||||
|
int startPos=displayItems.indexOf(filterItem);
|
||||||
displayItems.remove(startPos);
|
displayItems.remove(startPos);
|
||||||
displayItems.addAll(startPos, warning.filteredItems);
|
displayItems.addAll(startPos, warning.filteredItems);
|
||||||
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
||||||
if (startPos == 0) scrollToTop();
|
if (startPos == 0) scrollToTop();
|
||||||
warning.getItem().status.filterRevealed = true;
|
warning.getItem().status.filterRevealed = true;
|
||||||
|
list.invalidateItemDecorations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -756,6 +852,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void loadRelationships(Set<String> ids){
|
protected void loadRelationships(Set<String> ids){
|
||||||
|
if(ids.isEmpty())
|
||||||
|
return;
|
||||||
|
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
|
||||||
if(ids.isEmpty())
|
if(ids.isEmpty())
|
||||||
return;
|
return;
|
||||||
// TODO somehow manage these and cancel outstanding requests on refresh
|
// TODO somehow manage these and cancel outstanding requests on refresh
|
||||||
@@ -849,6 +948,23 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected Pair<Integer, Integer> findAllItemsOfParent(DisplayItemsParent parent){
|
||||||
|
int startIndex=-1;
|
||||||
|
int endIndex=-1;
|
||||||
|
for(int i=0; i<displayItems.size(); i++){
|
||||||
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
if(item.parentID.equals(parent.getID())) {
|
||||||
|
startIndex= startIndex==-1 ? i : startIndex;
|
||||||
|
endIndex=i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(startIndex==-1 || endIndex==-1)
|
||||||
|
return null;
|
||||||
|
return Pair.create(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> List<H> findAllHoldersOfType(String id, Class<H> type){
|
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> List<H> findAllHoldersOfType(String id, Class<H> type){
|
||||||
ArrayList<H> holders=new ArrayList<>();
|
ArrayList<H> holders=new ArrayList<>();
|
||||||
for(int i=0;i<list.getChildCount();i++){
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
@@ -1047,6 +1163,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void maybeShowPreReplySheet(Status status, Runnable proceed){
|
||||||
|
Relationship rel=getRelationship(status.account.id);
|
||||||
|
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
|
||||||
|
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
|
||||||
|
new NonMutualPreReplySheet(getActivity(), notAgain->{
|
||||||
|
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
|
||||||
|
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))){
|
||||||
|
new OldPostPreReplySheet(getActivity(), notAgain->{
|
||||||
|
if(notAgain)
|
||||||
|
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
|
||||||
|
proceed.run();
|
||||||
|
}, status).show();
|
||||||
|
}else{
|
||||||
|
proceed.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDra
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorSet;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
@@ -62,6 +65,7 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.TweakedFileProvider;
|
||||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
||||||
@@ -75,7 +79,7 @@ import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
|||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.ContentType;
|
import org.joinmastodon.android.model.ContentType;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
@@ -95,7 +99,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
|
|||||||
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||||
import org.joinmastodon.android.utils.FileProvider;
|
import org.joinmastodon.android.utils.Tracking;
|
||||||
import org.joinmastodon.android.utils.TransferSpeedTracker;
|
import org.joinmastodon.android.utils.TransferSpeedTracker;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController;
|
import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController;
|
||||||
@@ -135,12 +139,14 @@ import java.util.stream.Collectors;
|
|||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID, CustomTransitionsFragment {
|
||||||
|
|
||||||
private static final int MEDIA_RESULT=717;
|
private static final int MEDIA_RESULT=717;
|
||||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||||
@@ -507,7 +513,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
int typeIndex=contentType.ordinal();
|
int typeIndex=contentType.ordinal();
|
||||||
contentTypePopup.getMenu().findItem(typeIndex).setChecked(true);
|
if (contentTypePopup.getMenu().findItem(typeIndex) != null)
|
||||||
|
contentTypePopup.getMenu().findItem(typeIndex).setChecked(true);
|
||||||
contentTypeBtn.setSelected(typeIndex != ContentType.UNSPECIFIED.ordinal() && typeIndex != ContentType.PLAIN.ordinal());
|
contentTypeBtn.setSelected(typeIndex != ContentType.UNSPECIFIED.ordinal() && typeIndex != ContentType.PLAIN.ordinal());
|
||||||
|
|
||||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||||
@@ -527,7 +534,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
public void onLaunchAccountSearch(){
|
public void onLaunchAccountSearch(){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
View autocompleteView=autocompleteViewController.getView();
|
View autocompleteView=autocompleteViewController.getView();
|
||||||
@@ -1169,6 +1176,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
private void actuallyPublish(boolean preview){
|
private void actuallyPublish(boolean preview){
|
||||||
String text=mainEditText.getText().toString();
|
String text=mainEditText.getText().toString();
|
||||||
|
if(GlobalUserPreferences.removeTrackingParams)
|
||||||
|
text=Tracking.cleanUrlsInText(text);
|
||||||
CreateStatus.Request req=new CreateStatus.Request();
|
CreateStatus.Request req=new CreateStatus.Request();
|
||||||
if("bottom".equals(postLang.encoding)){
|
if("bottom".equals(postLang.encoding)){
|
||||||
text=new StatusTextEncoder(Bottom::encode).encode(text);
|
text=new StatusTextEncoder(Bottom::encode).encode(text);
|
||||||
@@ -1293,7 +1302,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
.setPositiveButton(R.string.ok, (a, b)->{})
|
.setPositiveButton(R.string.ok, (a, b)->{})
|
||||||
.show();
|
.show();
|
||||||
handlePublishError(null);
|
handlePublishError(null);
|
||||||
publishButton.setEnabled(false);
|
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replyTo == null) updateRecentLanguages();
|
if (replyTo == null) updateRecentLanguages();
|
||||||
@@ -1320,7 +1329,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
result.preview=true;
|
result.preview=true;
|
||||||
wm.removeView(sendingOverlay);
|
wm.removeView(sendingOverlay);
|
||||||
sendingOverlay=null;
|
sendingOverlay=null;
|
||||||
publishButton.setEnabled(true);
|
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
|
||||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||||
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
@@ -1499,7 +1508,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private void openCamera() throws IOException {
|
private void openCamera() throws IOException {
|
||||||
if (getContext().checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
if (getContext().checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||||
File photoFile = File.createTempFile("img", ".jpg");
|
File photoFile = File.createTempFile("img", ".jpg");
|
||||||
photoUri = FileProvider.getUriForFile(getContext(), getContext().getPackageName() + ".fileprovider", photoFile);
|
photoUri = UiUtils.getFileProviderUri(getContext(), photoFile);
|
||||||
|
|
||||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||||
@@ -1657,7 +1666,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P) m.setGroupDividerEnabled(true);
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()) m.setGroupDividerEnabled(true);
|
||||||
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||||
@Override
|
@Override
|
||||||
public boolean onMenuItemClick(MenuItem item){
|
public boolean onMenuItemClick(MenuItem item){
|
||||||
@@ -1806,8 +1815,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sanitizeMediaDescription(String description){
|
||||||
|
if(description == null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Gboard android keyboard attaches this text whenever the user
|
||||||
|
// pastes something from the keyboard's suggestion bar.
|
||||||
|
// Due to different end user locales, the exact text may vary, but at
|
||||||
|
// least in version 13.4.08, all of the translations contained the
|
||||||
|
// string "Gboard".
|
||||||
|
if (description.contains("Gboard")){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||||
|
description = sanitizeMediaDescription(description);
|
||||||
return mediaViewController.addMediaAttachment(uri, description);
|
return mediaViewController.addMediaAttachment(uri, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1850,6 +1877,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
Editable e=mainEditText.getText();
|
Editable e=mainEditText.getText();
|
||||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||||
|
if(start==-1 || end==-1)
|
||||||
|
return;
|
||||||
e.replace(start, end, text+" ");
|
e.replace(start, end, text+" ");
|
||||||
finishAutocomplete();
|
finishAutocomplete();
|
||||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||||
@@ -1918,4 +1947,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
languageButton.setText(opt.language.getLanguageName());
|
languageButton.setText(opt.language.getLanguageName());
|
||||||
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
|
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Animator onCreateEnterTransition(View prev, View container){
|
||||||
|
AnimatorSet anim=new AnimatorSet();
|
||||||
|
if(getArguments().getBoolean("fromThreadFragment")){
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
anim.setDuration(300);
|
||||||
|
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Animator onCreateExitTransition(View prev, View container){
|
||||||
|
AnimatorSet anim=new AnimatorSet();
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
|
||||||
|
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
|
||||||
|
);
|
||||||
|
anim.setDuration(200);
|
||||||
|
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import android.graphics.Rect;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.media.MediaMetadataRetriever;
|
import android.media.MediaMetadataRetriever;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.style.BulletSpan;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -135,20 +132,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item){
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
if(item.getItemId()==R.id.help){
|
if(item.getItemId()==R.id.help){
|
||||||
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
|
|
||||||
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
|
|
||||||
for(BulletSpan span:spans){
|
|
||||||
BulletSpan betterSpan;
|
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
|
|
||||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
|
|
||||||
else
|
|
||||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
|
|
||||||
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
|
|
||||||
msg.removeSpan(span);
|
|
||||||
}
|
|
||||||
new M3AlertDialogBuilder(themeWrapper)
|
new M3AlertDialogBuilder(themeWrapper)
|
||||||
.setTitle(R.string.what_is_alt_text)
|
.setTitle(R.string.what_is_alt_text)
|
||||||
.setMessage(msg)
|
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
@@ -185,7 +171,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||||||
fakeAttachment.meta.width=width;
|
fakeAttachment.meta.width=width;
|
||||||
fakeAttachment.meta.height=height;
|
fakeAttachment.meta.height=height;
|
||||||
|
|
||||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
|
||||||
@Override
|
@Override
|
||||||
public void setPhotoViewVisibility(int index, boolean visible){
|
public void setPhotoViewVisibility(int index, boolean visible){
|
||||||
image.setAlpha(visible ? 1f : 0f);
|
image.setAlpha(visible ? 1f : 0f);
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
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.WindowInsets;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||||
|
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||||
|
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
|
||||||
|
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||||
|
import org.joinmastodon.android.ui.views.CurlyArrowEmptyView;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
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.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{
|
||||||
|
private FollowList followList;
|
||||||
|
private Button nextButton;
|
||||||
|
private View buttonBar;
|
||||||
|
private FragmentRootLinearLayout rootView;
|
||||||
|
private FrameLayout searchFragmentContainer;
|
||||||
|
private FrameLayout fragmentContentWrap;
|
||||||
|
private AddNewListMembersFragment searchFragment;
|
||||||
|
private WindowInsets lastInsets;
|
||||||
|
private boolean dismissingSearchFragment;
|
||||||
|
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.manage_list_members);
|
||||||
|
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
|
||||||
|
setLayout(R.layout.fragment_login);
|
||||||
|
setEmptyText(R.string.list_no_members);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
|
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||||
|
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
|
||||||
|
loadData();
|
||||||
|
}else{
|
||||||
|
onDataLoaded(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetListAccounts(followList.id, null, 0)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
for(Account acc:result)
|
||||||
|
accountIDsInList.add(acc.id);
|
||||||
|
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||||
|
View view=super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
FrameLayout wrapper=new FrameLayout(getActivity());
|
||||||
|
wrapper.addView(view);
|
||||||
|
rootView=(FragmentRootLinearLayout) view;
|
||||||
|
fragmentContentWrap=wrapper;
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
nextButton=view.findViewById(R.id.btn_next);
|
||||||
|
nextButton.setOnClickListener(this::onNextClick);
|
||||||
|
nextButton.setText(R.string.done);
|
||||||
|
buttonBar=view.findViewById(R.id.button_bar);
|
||||||
|
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
lastInsets=insets;
|
||||||
|
if(searchFragment!=null)
|
||||||
|
searchFragment.onApplyWindowInsets(insets);
|
||||||
|
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
|
||||||
|
rootView.dispatchApplyWindowInsets(insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<View> getViewsForElevationEffect(){
|
||||||
|
return List.of(getToolbar(), buttonBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
MenuItem item=menu.add(R.string.add_list_member);
|
||||||
|
item.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if(searchFragmentContainer!=null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
searchFragmentContainer=new FrameLayout(getActivity());
|
||||||
|
searchFragmentContainer.setId(R.id.search_fragment);
|
||||||
|
fragmentContentWrap.addView(searchFragmentContainer);
|
||||||
|
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
args.putBoolean("_can_go_back", true);
|
||||||
|
searchFragment=new AddNewListMembersFragment(this);
|
||||||
|
searchFragment.setArguments(args);
|
||||||
|
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
|
||||||
|
getChildFragmentManager().executePendingTransactions();
|
||||||
|
if(lastInsets!=null)
|
||||||
|
searchFragment.onApplyWindowInsets(lastInsets);
|
||||||
|
searchFragmentContainer.setTranslationX(V.dp(100));
|
||||||
|
searchFragmentContainer.setAlpha(0f);
|
||||||
|
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||||
|
rootView.setVisibility(View.GONE);
|
||||||
|
}).start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializeEmptyView(View contentView){
|
||||||
|
ViewStub emptyStub=contentView.findViewById(R.id.empty);
|
||||||
|
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
|
||||||
|
super.initializeEmptyView(contentView);
|
||||||
|
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
|
||||||
|
emptySecondary.setText(R.string.list_find_users);
|
||||||
|
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
|
||||||
|
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setStatusBarColor(int color){
|
||||||
|
rootView.setStatusBarColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setNavigationBarColor(int color){
|
||||||
|
rootView.setNavigationBarColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissSearchFragment(){
|
||||||
|
if(searchFragment==null || dismissingSearchFragment)
|
||||||
|
return;
|
||||||
|
dismissingSearchFragment=true;
|
||||||
|
rootView.setVisibility(View.VISIBLE);
|
||||||
|
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||||
|
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||||
|
getChildFragmentManager().executePendingTransactions();
|
||||||
|
fragmentContentWrap.removeView(searchFragmentContainer);
|
||||||
|
searchFragmentContainer=null;
|
||||||
|
searchFragment=null;
|
||||||
|
dismissingSearchFragment=false;
|
||||||
|
}).start();
|
||||||
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNextClick(View v){
|
||||||
|
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAccountToList(AccountViewModel account, Runnable onDone){
|
||||||
|
new AddAccountsToList(followList.id, Set.of(account.account.id))
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
accountIDsInList.add(account.account.id);
|
||||||
|
if(onDone!=null)
|
||||||
|
onDone.run();
|
||||||
|
int i=0;
|
||||||
|
for(AccountViewModel acc:data){
|
||||||
|
if(acc.account.id.equals(account.account.id)){
|
||||||
|
list.getAdapter().notifyItemChanged(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
int pos=data.size();
|
||||||
|
data.add(account);
|
||||||
|
list.getAdapter().notifyItemInserted(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
|
||||||
|
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
accountIDsInList.remove(account.account.id);
|
||||||
|
if(onDone!=null)
|
||||||
|
onDone.run();
|
||||||
|
int i=0;
|
||||||
|
for(AccountViewModel acc:data){
|
||||||
|
if(acc.account.id.equals(account.account.id)){
|
||||||
|
list.getAdapter().notifyItemChanged(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||||
|
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||||
|
holder.setOnLongClickListener(vh->false);
|
||||||
|
Button button=holder.getButton();
|
||||||
|
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||||
|
button.setMinimumWidth(0);
|
||||||
|
button.setMinWidth(0);
|
||||||
|
button.setOnClickListener(v->{
|
||||||
|
holder.setActionProgressVisible(true);
|
||||||
|
holder.itemView.setHasTransientState(true);
|
||||||
|
Runnable onDone=()->{
|
||||||
|
holder.setActionProgressVisible(false);
|
||||||
|
holder.itemView.setHasTransientState(false);
|
||||||
|
};
|
||||||
|
AccountViewModel account=holder.getItem();
|
||||||
|
if(isAccountInList(account)){
|
||||||
|
removeAccountAccountFromList(account, onDone);
|
||||||
|
}else{
|
||||||
|
addAccountToList(account, onDone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindViewHolder(AccountViewHolder holder){
|
||||||
|
Button button=holder.getButton();
|
||||||
|
int textRes, styleRes;
|
||||||
|
if(isAccountInList(holder.getItem())){
|
||||||
|
textRes=R.string.remove;
|
||||||
|
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||||
|
}else{
|
||||||
|
textRes=R.string.add;
|
||||||
|
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||||
|
}
|
||||||
|
button.setText(textRes);
|
||||||
|
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||||
|
button.setBackground(ta.getDrawable(0));
|
||||||
|
ta.recycle();
|
||||||
|
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||||
|
button.setTextColor(ta.getColorStateList(0));
|
||||||
|
ta.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base){
|
||||||
|
// TODO this
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
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.lists.CreateList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||||
|
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
|
||||||
|
public class CreateListFragment extends BaseEditListFragment{
|
||||||
|
private Button nextButton;
|
||||||
|
private View buttonBar;
|
||||||
|
private FollowList followList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.create_list);
|
||||||
|
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
|
||||||
|
setLayout(R.layout.fragment_login);
|
||||||
|
if(savedInstanceState!=null)
|
||||||
|
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getNavigationIconDrawableResource(){
|
||||||
|
return R.drawable.ic_baseline_arrow_drop_down_18;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wantsCustomNavigationIcon(){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
nextButton=view.findViewById(R.id.btn_next);
|
||||||
|
nextButton.setOnClickListener(this::onNextClick);
|
||||||
|
nextButton.setText(R.string.create);
|
||||||
|
buttonBar=view.findViewById(R.id.button_bar);
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<View> getViewsForElevationEffect(){
|
||||||
|
return List.of(getToolbar(), buttonBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState){
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNextClick(View v){
|
||||||
|
String title=titleEdit.getText().toString().trim();
|
||||||
|
if(TextUtils.isEmpty(title)){
|
||||||
|
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(followList==null){
|
||||||
|
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(FollowList result){
|
||||||
|
followList=result;
|
||||||
|
proceed(false);
|
||||||
|
E.post(new ListCreatedEvent(accountID, result));
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().addList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
|
||||||
|
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(FollowList result){
|
||||||
|
followList=result;
|
||||||
|
proceed(true);
|
||||||
|
E.post(new ListUpdatedEvent(accountID, result));
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}else{
|
||||||
|
proceed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void proceed(boolean needLoadMembers){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
args.putBoolean("needLoadMembers", needLoadMembers);
|
||||||
|
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
|
||||||
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
|
||||||
|
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
|
||||||
|
Nav.finish(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,14 @@ import android.view.MenuInflater;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
@@ -46,14 +44,14 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
|
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if(!result.isEmpty())
|
if(!result.isEmpty())
|
||||||
maxID=result.get(result.size()-1).id;
|
maxID=result.get(result.size()-1).id;
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList());
|
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||||
result.stream().forEach(status -> {
|
result.stream().forEach(status -> {
|
||||||
status.account.acct += "@"+domain;
|
status.account.acct += "@"+domain;
|
||||||
status.mentions.forEach(mention -> mention.id = null);
|
status.mentions.forEach(mention -> mention.id = null);
|
||||||
@@ -82,12 +80,15 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FilterContext getFilterContext() {
|
protected FilterContext getFilterContext() {
|
||||||
return null;
|
return FilterContext.PUBLIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uri getWebUri(Uri.Builder base) {
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
return Uri.parse(domain);
|
return new Uri.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.authority(domain)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
|
||||||
|
public class EditListFragment extends BaseEditListFragment{
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.edit_list);
|
||||||
|
loadMembers();
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
menu.add(R.string.delete_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.delete_list)
|
||||||
|
.setMessage(getString(R.string.delete_list_confirm, followList.title))
|
||||||
|
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
String newTitle=titleEdit.getText().toString();
|
||||||
|
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
|
||||||
|
boolean newExclusive=exclusiveItem.checked;
|
||||||
|
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
|
||||||
|
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(FollowList result){
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||||
|
E.post(new ListUpdatedEvent(accountID, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
// TODO handle errors somehow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,17 +41,14 @@ import org.joinmastodon.android.api.requests.lists.GetLists;
|
|||||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.model.CustomLocalTimeline;
|
import org.joinmastodon.android.model.CustomLocalTimeline;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -74,7 +71,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||||||
private Menu optionsMenu;
|
private Menu optionsMenu;
|
||||||
private boolean updated;
|
private boolean updated;
|
||||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
||||||
private final List<ListTimeline> listTimelines=new ArrayList<>();
|
private final List<FollowList> followLists =new ArrayList<>();
|
||||||
private final List<Hashtag> hashtags=new ArrayList<>();
|
private final List<Hashtag> hashtags=new ArrayList<>();
|
||||||
private MenuItem addHashtagItem;
|
private MenuItem addHashtagItem;
|
||||||
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
||||||
@@ -94,8 +91,8 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||||||
|
|
||||||
new GetLists().setCallback(new Callback<>(){
|
new GetLists().setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> result){
|
public void onSuccess(List<FollowList> result){
|
||||||
listTimelines.addAll(result);
|
followLists.addAll(result);
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +221,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||||||
makeBackItem(hashtagsMenu);
|
makeBackItem(hashtagsMenu);
|
||||||
|
|
||||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
||||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
|
followLists.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
|
||||||
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
|
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
|
||||||
|
|
||||||
@@ -399,7 +396,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||||||
tl.setTitle(name);
|
tl.setTitle(name);
|
||||||
if(item == null || item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
if(item == null || item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||||
tl.setTagOptions(
|
tl.setTagOptions(
|
||||||
mainHashtag,
|
TextUtils.isEmpty(mainHashtag) ? name : mainHashtag,
|
||||||
tagsAny.getChipValues(),
|
tagsAny.getChipValues(),
|
||||||
tagsAll.getChipValues(),
|
tagsAll.getChipValues(),
|
||||||
tagsNone.getChipValues(),
|
tagsNone.getChipValues(),
|
||||||
|
|||||||
@@ -297,8 +297,8 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
|||||||
cover.setImageDrawable(image);
|
cover.setImageDrawable(image);
|
||||||
}else{
|
}else{
|
||||||
item.emojiHelper.setImageDrawable(index-2, image);
|
item.emojiHelper.setImageDrawable(index-2, image);
|
||||||
name.invalidate();
|
name.setText(name.getText());
|
||||||
bio.invalidate();
|
bio.setText(bio.getText());
|
||||||
}
|
}
|
||||||
if(image instanceof Animatable a && !a.isRunning())
|
if(image instanceof Animatable a && !a.isRunning())
|
||||||
a.start();
|
a.start();
|
||||||
@@ -319,7 +319,18 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
|||||||
|
|
||||||
private void onFollowRequestButtonClick(View v) {
|
private void onFollowRequestButtonClick(View v) {
|
||||||
itemView.setHasTransientState(true);
|
itemView.setHasTransientState(true);
|
||||||
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
|
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, (Boolean visible) -> {
|
||||||
|
if(v==acceptButton){
|
||||||
|
acceptButton.setTextVisible(!visible);
|
||||||
|
acceptProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
acceptButton.setClickable(!visible);
|
||||||
|
}else{
|
||||||
|
rejectButton.setTextVisible(!visible);
|
||||||
|
rejectProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
rejectButton.setClickable(!visible);
|
||||||
|
}
|
||||||
|
itemView.setHasTransientState(false);
|
||||||
|
}, rel -> {
|
||||||
if(getContext()==null) return;
|
if(getContext()==null) return;
|
||||||
itemView.setHasTransientState(false);
|
itemView.setHasTransientState(false);
|
||||||
relationships.put(item.account.id, rel);
|
relationships.put(item.account.id, rel);
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import org.joinmastodon.android.model.FilterAction;
|
|||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.FilterKeyword;
|
import org.joinmastodon.android.model.FilterKeyword;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
@@ -63,6 +65,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
|||||||
private MenuItem followMenuItem, pinMenuItem, muteMenuItem;
|
private MenuItem followMenuItem, pinMenuItem, muteMenuItem;
|
||||||
private boolean followRequestRunning;
|
private boolean followRequestRunning;
|
||||||
private boolean toolbarContentVisible;
|
private boolean toolbarContentVisible;
|
||||||
|
private String maxID;
|
||||||
|
|
||||||
private List<String> any;
|
private List<String> any;
|
||||||
private List<String> all;
|
private List<String> all;
|
||||||
@@ -98,7 +101,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateMuteState(boolean newMute) {
|
private void updateMuteState(boolean newMute) {
|
||||||
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
|
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtagName));
|
||||||
muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +232,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uri getWebUri(Uri.Builder base) {
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtag).build();
|
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtagName).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import android.app.Fragment;
|
|||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.assist.AssistContent;
|
import android.app.assist.AssistContent;
|
||||||
import android.graphics.drawable.RippleDrawable;
|
import android.graphics.drawable.RippleDrawable;
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Outline;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.service.notification.StatusBarNotification;
|
import android.service.notification.StatusBarNotification;
|
||||||
@@ -37,7 +35,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
|
|||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TabBar;
|
import org.joinmastodon.android.ui.views.TabBar;
|
||||||
@@ -163,6 +161,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
|
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
|
||||||
notificationsBadge.setVisibility(View.GONE);
|
notificationsBadge.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
tabBar.selectTab(currentTab);
|
||||||
|
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
|
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
|
||||||
@@ -184,7 +184,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tabBar.selectTab(currentTab);
|
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -312,11 +311,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
discoverFragment.openSearch();
|
discoverFragment.openSearch();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(tab==R.id.tab_home){
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
|||||||
import org.joinmastodon.android.model.Announcement;
|
import org.joinmastodon.android.model.Announcement;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
@@ -95,7 +95,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
private ImageView collapsedChevron;
|
private ImageView collapsedChevron;
|
||||||
private TextView timelineTitle;
|
private TextView timelineTitle;
|
||||||
private PopupMenu switcherPopup;
|
private PopupMenu switcherPopup;
|
||||||
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
|
private final Map<Integer, FollowList> listItems = new HashMap<>();
|
||||||
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
||||||
private List<TimelineDefinition> timelinesList;
|
private List<TimelineDefinition> timelinesList;
|
||||||
private int count;
|
private int count;
|
||||||
@@ -270,7 +270,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
|
|
||||||
new GetLists().setCallback(new Callback<>() {
|
new GetLists().setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> lists) {
|
public void onSuccess(List<FollowList> lists) {
|
||||||
updateList(lists, listItems);
|
updateList(lists, listItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +408,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
addListsToOverflowMenu();
|
addListsToOverflowMenu();
|
||||||
addHashtagsToOverflowMenu();
|
addHashtagsToOverflowMenu();
|
||||||
|
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic())
|
||||||
m.setGroupDividerEnabled(true);
|
m.setGroupDividerEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +512,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
ListTimeline list;
|
FollowList list;
|
||||||
Hashtag hashtag;
|
Hashtag hashtag;
|
||||||
|
|
||||||
if (item.getItemId() == R.id.menu_back) {
|
if (item.getItemId() == R.id.menu_back) {
|
||||||
@@ -701,13 +701,13 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||||
handleListEvent(listItems, l -> l.id.equals(event.id), false, null);
|
handleListEvent(listItems, l -> l.id.equals(event.listID), false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||||
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
|
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
|
||||||
ListTimeline list = new ListTimeline();
|
FollowList list = new FollowList();
|
||||||
list.id = event.id;
|
list.id = event.id;
|
||||||
list.title = event.title;
|
list.title = event.title;
|
||||||
list.repliesPolicy = event.repliesPolicy;
|
list.repliesPolicy = event.repliesPolicy;
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||||
|
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||||
|
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||||
|
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
|
||||||
|
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.utils.ActionModeHelper;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
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.utils.V;
|
||||||
|
|
||||||
|
public class ListMembersFragment extends PaginatedAccountListFragment{
|
||||||
|
private static final int ADD_MEMBER_RESULT=600;
|
||||||
|
|
||||||
|
private ImageButton fab;
|
||||||
|
private FollowList followList;
|
||||||
|
private boolean inSelectionMode;
|
||||||
|
private Set<String> selectedAccounts=new HashSet<>();
|
||||||
|
private ActionMode actionMode;
|
||||||
|
private MenuItem deleteItem;
|
||||||
|
|
||||||
|
public ListMembersFragment(){
|
||||||
|
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||||
|
setTitle(R.string.list_members);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||||
|
return new GetListAccounts(followList.id, maxID, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MastodonAPIRequest loadRemoteInfo(){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCurrentInfo(){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteDomain(){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||||
|
super.onConfigureViewHolder(holder);
|
||||||
|
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
|
||||||
|
holder.setOnClickListener(this::onItemClick);
|
||||||
|
holder.setOnLongClickListener(this::onItemLongClick);
|
||||||
|
holder.getContextMenu().getMenu().add(0, R.id.remove_from_list, 0, R.string.remove_from_list);
|
||||||
|
holder.setOnCustomMenuItemSelectedListener(item->onItemMenuItemSelected(holder, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindViewHolder(AccountViewHolder holder){
|
||||||
|
super.onBindViewHolder(holder);
|
||||||
|
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
|
||||||
|
if(inSelectionMode){
|
||||||
|
holder.setChecked(selectedAccounts.contains(holder.getItem().account.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wantsLightStatusBar(){
|
||||||
|
if(actionMode!=null)
|
||||||
|
return UiUtils.isDarkTheme();
|
||||||
|
return super.wantsLightStatusBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
inflater.inflate(R.menu.selectable_list, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
int id=item.getItemId();
|
||||||
|
if(id==R.id.select){
|
||||||
|
enterSelectionMode();
|
||||||
|
}else if(id==R.id.select_all){
|
||||||
|
for(AccountViewModel a:(ArrayList<AccountViewModel>)data){
|
||||||
|
selectedAccounts.add(a.account.id);
|
||||||
|
}
|
||||||
|
enterSelectionMode();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
fab=view.findViewById(R.id.fab);
|
||||||
|
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
fab.setContentDescription(getString(R.string.add_list_member));
|
||||||
|
fab.setOnClickListener(v->onFabClick());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
super.onApplyWindowInsets(insets);
|
||||||
|
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||||
|
if(reqCode==ADD_MEMBER_RESULT && success){
|
||||||
|
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
|
||||||
|
addAccounts(List.of(acc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onAccountRemovedFromList(AccountRemovedFromListEvent ev){
|
||||||
|
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||||
|
removeAccountRows(Set.of(ev.targetAccountID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onAccountAddedToList(AccountAddedToListEvent ev){
|
||||||
|
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||||
|
data.add(new AccountViewModel(ev.account, accountID));
|
||||||
|
list.getAdapter().notifyItemInserted(data.size()-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFabClick(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onItemClick(AccountViewHolder holder){
|
||||||
|
if(inSelectionMode){
|
||||||
|
String id=holder.getItem().account.id;
|
||||||
|
if(selectedAccounts.contains(id)){
|
||||||
|
selectedAccounts.remove(id);
|
||||||
|
holder.setChecked(false);
|
||||||
|
}else{
|
||||||
|
selectedAccounts.add(id);
|
||||||
|
holder.setChecked(true);
|
||||||
|
}
|
||||||
|
updateActionModeTitle();
|
||||||
|
deleteItem.setEnabled(!selectedAccounts.isEmpty());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(holder.getItem().account));
|
||||||
|
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onItemLongClick(AccountViewHolder holder){
|
||||||
|
if(inSelectionMode)
|
||||||
|
return false;
|
||||||
|
selectedAccounts.add(holder.getItem().account.id);
|
||||||
|
enterSelectionMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onItemMenuItemSelected(AccountViewHolder holder, MenuItem item){
|
||||||
|
int id=item.getItemId();
|
||||||
|
if(id==R.id.remove_from_list){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.confirm_remove_list_member)
|
||||||
|
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateItemsForSelectionModeTransition(){
|
||||||
|
list.getAdapter().notifyItemRangeChanged(0, data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enterSelectionMode(){
|
||||||
|
inSelectionMode=true;
|
||||||
|
updateItemsForSelectionModeTransition();
|
||||||
|
V.setVisibilityAnimated(fab, View.INVISIBLE);
|
||||||
|
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||||
|
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
|
||||||
|
deleteItem=menu.findItem(R.id.delete);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.confirm_remove_list_members)
|
||||||
|
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode){
|
||||||
|
actionMode=null;
|
||||||
|
inSelectionMode=false;
|
||||||
|
selectedAccounts.clear();
|
||||||
|
updateItemsForSelectionModeTransition();
|
||||||
|
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateActionModeTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateActionModeTitle(){
|
||||||
|
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAccounts(Set<String> ids){
|
||||||
|
new RemoveAccountsFromList(followList.id, ids)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
if(inSelectionMode)
|
||||||
|
actionMode.finish();
|
||||||
|
removeAccountRows(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAccounts(Collection<Account> accounts){
|
||||||
|
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
for(Account acc:accounts){
|
||||||
|
data.add(new AccountViewModel(acc, accountID));
|
||||||
|
}
|
||||||
|
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAccountRows(Set<String> ids){
|
||||||
|
for(int i=data.size()-1;i>=0;i--){
|
||||||
|
if(ids.contains(((ArrayList<AccountViewModel>)data).get(i).account.id)){
|
||||||
|
data.remove(i);
|
||||||
|
list.getAdapter().notifyItemRemoved(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
|||||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
@@ -39,7 +39,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
private String listID;
|
private String listID;
|
||||||
private String listTitle;
|
private String listTitle;
|
||||||
@Nullable
|
@Nullable
|
||||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
private FollowList.RepliesPolicy repliesPolicy;
|
||||||
private boolean exclusive;
|
private boolean exclusive;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -54,19 +54,19 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
listID = args.getString("listID");
|
listID = args.getString("listID");
|
||||||
listTitle = args.getString("listTitle");
|
listTitle = args.getString("listTitle");
|
||||||
exclusive = args.getBoolean("listIsExclusive");
|
exclusive = args.getBoolean("listIsExclusive");
|
||||||
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
repliesPolicy = FollowList.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||||
|
|
||||||
setTitle(listTitle);
|
setTitle(listTitle);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
new GetList(listID).setCallback(new Callback<>() {
|
new GetList(listID).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ListTimeline listTimeline) {
|
public void onSuccess(FollowList followList) {
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
// TODO: save updated info
|
// TODO: save updated info
|
||||||
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
|
if (!followList.title.equals(listTitle)) setTitle(followList.title);
|
||||||
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
|
if (followList.repliesPolicy != null && !followList.repliesPolicy.equals(repliesPolicy)) {
|
||||||
repliesPolicy = listTimeline.repliesPolicy;
|
repliesPolicy = followList.repliesPolicy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +97,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
.setPositiveButton(R.string.save, (d, which) -> {
|
.setPositiveButton(R.string.save, (d, which) -> {
|
||||||
String newTitle = editor.getTitle().trim();
|
String newTitle = editor.getTitle().trim();
|
||||||
setTitle(newTitle);
|
setTitle(newTitle);
|
||||||
new UpdateList(listID, newTitle, editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
new UpdateList(listID, newTitle, editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ListTimeline list) {
|
public void onSuccess(FollowList list) {
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
setTitle(list.title);
|
setTitle(list.title);
|
||||||
listTitle = list.title;
|
listTitle = list.title;
|
||||||
@@ -119,7 +119,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
.show();
|
.show();
|
||||||
} else if (item.getItemId() == R.id.delete) {
|
} else if (item.getItemId() == R.id.delete) {
|
||||||
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
||||||
E.post(new ListDeletedEvent(listID));
|
E.post(new ListDeletedEvent(accountID, listID));
|
||||||
Nav.finish(this);
|
Nav.finish(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.views.ListEditor;
|
import org.joinmastodon.android.ui.views.ListEditor;
|
||||||
@@ -42,7 +43,7 @@ import me.grishka.appkit.api.SimpleCallback;
|
|||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
public class ListsFragment extends MastodonRecyclerFragment<FollowList> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private String profileAccountId;
|
private String profileAccountId;
|
||||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||||
@@ -97,9 +98,9 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||||
.setView(editor)
|
.setView(editor)
|
||||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||||
new CreateList(editor.getTitle(), editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
new CreateList(editor.getTitle(), editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ListTimeline list) {
|
public void onSuccess(FollowList list) {
|
||||||
data.add(0, list);
|
data.add(0, list);
|
||||||
adapter.notifyItemRangeInserted(0, 1);
|
adapter.notifyItemRangeInserted(0, 1);
|
||||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
|
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
|
||||||
@@ -120,10 +121,10 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
private void saveListMembership(String listId, boolean isMember) {
|
private void saveListMembership(String listId, boolean isMember) {
|
||||||
userInList.put(listId, isMember);
|
userInList.put(listId, isMember);
|
||||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
ResultlessMastodonAPIRequest req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||||
req.setCallback(new Callback<>() {
|
req.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Object o) {}
|
public void onSuccess(Void o) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
@@ -139,19 +140,19 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||||
.setCallback(new SimpleCallback<>(this) {
|
.setCallback(new SimpleCallback<>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> lists) {
|
public void onSuccess(List<FollowList> lists) {
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
for (FollowList l : lists) userInListBefore.put(l.id, true);
|
||||||
userInList.putAll(userInListBefore);
|
userInList.putAll(userInListBefore);
|
||||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||||
if (profileAccountId == null) return;
|
if (profileAccountId == null) return;
|
||||||
|
|
||||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> allLists) {
|
public void onSuccess(List<FollowList> allLists) {
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
List<ListTimeline> newLists = new ArrayList<>();
|
List<FollowList> newLists = new ArrayList<>();
|
||||||
for (ListTimeline l : allLists) {
|
for (FollowList l : allLists) {
|
||||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||||
if (!userInListBefore.containsKey(l.id)) {
|
if (!userInListBefore.containsKey(l.id)) {
|
||||||
userInListBefore.put(l.id, false);
|
userInListBefore.put(l.id, false);
|
||||||
@@ -169,8 +170,8 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||||
for (int i = 0; i < data.size(); i++) {
|
for (int i = 0; i < data.size(); i++) {
|
||||||
ListTimeline item = data.get(i);
|
FollowList item = data.get(i);
|
||||||
if (item.id.equals(event.id)) {
|
if (item.id.equals(event.listID)) {
|
||||||
data.remove(i);
|
data.remove(i);
|
||||||
adapter.notifyItemRemoved(i);
|
adapter.notifyItemRemoved(i);
|
||||||
break;
|
break;
|
||||||
@@ -181,7 +182,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||||
for (int i = 0; i < data.size(); i++) {
|
for (int i = 0; i < data.size(); i++) {
|
||||||
ListTimeline item = data.get(i);
|
FollowList item = data.get(i);
|
||||||
if (item.id.equals(event.id)) {
|
if (item.id.equals(event.id)) {
|
||||||
item.title = event.title;
|
item.title = event.title;
|
||||||
item.repliesPolicy = event.repliesPolicy;
|
item.repliesPolicy = event.repliesPolicy;
|
||||||
@@ -230,7 +231,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
private class ListViewHolder extends BindableViewHolder<FollowList> implements UsableRecyclerView.Clickable{
|
||||||
private final TextView title;
|
private final TextView title;
|
||||||
private final CheckBox listToggle;
|
private final CheckBox listToggle;
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(ListTimeline item) {
|
public void onBind(FollowList item) {
|
||||||
title.setText(item.title);
|
title.setText(item.title);
|
||||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
||||||
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
|
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||||
|
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||||
|
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class ManageFollowedHashtagsFragment extends BaseSettingsFragment<Hashtag> implements ListItemWithOptionsMenu.OptionsMenuListener<Hashtag>{
|
||||||
|
private String maxID;
|
||||||
|
|
||||||
|
public ManageFollowedHashtagsFragment(){
|
||||||
|
super(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.manage_hashtags);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetFollowedTags(offset>0 ? maxID : null, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||||
|
maxID=null;
|
||||||
|
if(result.nextPageUri!=null)
|
||||||
|
maxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
|
onDataLoaded(result.stream().map(t->{
|
||||||
|
int posts=t.getWeekPosts();
|
||||||
|
return new ListItemWithOptionsMenu<>(t.name, getResources().getQuantityString(R.plurals.x_posts_recently, posts, posts), ManageFollowedHashtagsFragment.this,
|
||||||
|
R.drawable.ic_fluent_tag_24_regular, ManageFollowedHashtagsFragment.this::onItemClick, t, false);
|
||||||
|
}).collect(Collectors.toList()), maxID!=null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<Hashtag> item, Menu menu){
|
||||||
|
menu.clear();
|
||||||
|
menu.add(getString(R.string.unfollow_user, "#"+item.parentObject.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListItemOptionSelected(ListItemWithOptionsMenu<Hashtag> item, MenuItem menuItem){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(getString(R.string.unfollow_confirmation, "#"+item.parentObject.name))
|
||||||
|
.setPositiveButton(R.string.unfollow, (dlg, which)->doUnfollow(item))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onItemClick(ListItemWithOptionsMenu<Hashtag> item){
|
||||||
|
UiUtils.openHashtagTimeline(getActivity(), accountID, item.parentObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doUnfollow(ListItemWithOptionsMenu<Hashtag> item){
|
||||||
|
new SetTagFollowed(item.parentObject.name, false)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Hashtag result){
|
||||||
|
int index=data.indexOf(item);
|
||||||
|
if(index==-1)
|
||||||
|
return;
|
||||||
|
data.remove(index);
|
||||||
|
list.getAdapter().notifyItemRemoved(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||||
|
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
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.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
|
||||||
|
private ImageButton fab;
|
||||||
|
|
||||||
|
public ManageListsFragment(){
|
||||||
|
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.manage_lists);
|
||||||
|
loadData();
|
||||||
|
setRefreshEnabled(true);
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
Callback<List<FollowList>> callback=new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> result){
|
||||||
|
onDataLoaded(result.stream().map(ManageListsFragment.this::makeItem).collect(Collectors.toList()), false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(refreshing){
|
||||||
|
AccountSessionManager.get(accountID)
|
||||||
|
.getCacheController()
|
||||||
|
.reloadLists(callback);
|
||||||
|
}else{
|
||||||
|
AccountSessionManager.get(accountID)
|
||||||
|
.getCacheController()
|
||||||
|
.getLists(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListItem<FollowList> makeItem(FollowList l){
|
||||||
|
return new ListItemWithOptionsMenu<>(l.title, null, ManageListsFragment.this, R.drawable.ic_list_alt_24px, ManageListsFragment.this::onListClick, l, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onListClick(ListItemWithOptionsMenu<FollowList> item){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||||
|
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<FollowList> item, Menu menu){
|
||||||
|
menu.add(0, R.id.edit, 0, R.string.edit_list);
|
||||||
|
menu.add(0, R.id.delete, 1, R.string.delete_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListItemOptionSelected(ListItemWithOptionsMenu<FollowList> item, MenuItem menuItem){
|
||||||
|
int id=menuItem.getItemId();
|
||||||
|
if(id==R.id.edit){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||||
|
Nav.go(getActivity(), EditListFragment.class, args);
|
||||||
|
}else if(id==R.id.delete){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.delete_list)
|
||||||
|
.setMessage(getString(R.string.delete_list_confirm, item.parentObject.title))
|
||||||
|
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList(item.parentObject))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
fab=view.findViewById(R.id.fab);
|
||||||
|
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
fab.setContentDescription(getString(R.string.create_list));
|
||||||
|
fab.setOnClickListener(v->onFabClick());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
super.onApplyWindowInsets(insets);
|
||||||
|
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doDeleteList(FollowList list){
|
||||||
|
new DeleteList(list.id)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
for(int i=0;i<data.size();i++){
|
||||||
|
if(data.get(i).parentObject==list){
|
||||||
|
data.remove(i);
|
||||||
|
itemsAdapter.notifyItemRemoved(i);
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().deleteList(list.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
Activity activity=getActivity();
|
||||||
|
if(activity==null)
|
||||||
|
return;
|
||||||
|
error.showToast(activity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onListUpdated(ListUpdatedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
for(ListItem<FollowList> item:data){
|
||||||
|
if(item.parentObject.id.equals(ev.list.id)){
|
||||||
|
item.parentObject=ev.list;
|
||||||
|
item.title=ev.list.title;
|
||||||
|
rebindItem(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onListDeleted(ListDeletedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
int i=0;
|
||||||
|
for(ListItem<FollowList> item:data){
|
||||||
|
if(item.parentObject.id.equals(ev.listID)){
|
||||||
|
data.remove(i);
|
||||||
|
itemsAdapter.notifyItemRemoved(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onListCreated(ListCreatedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
ListItem<FollowList> item=makeItem(ev.list);
|
||||||
|
data.add(item);
|
||||||
|
((List<ListItem<FollowList>>)data).sort(Comparator.comparing(l->l.parentObject.title));
|
||||||
|
itemsAdapter.notifyItemInserted(data.indexOf(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFabClick(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Nav.go(getActivity(), CreateListFragment.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import android.graphics.Rect;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
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.view.View;
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
@@ -14,6 +18,7 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
@@ -47,12 +52,13 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
|
||||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
|
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||||
private boolean onlyMentions;
|
private boolean onlyMentions;
|
||||||
private boolean onlyPosts;
|
|
||||||
private String maxID;
|
private String maxID;
|
||||||
private boolean reloadingFromCache;
|
private boolean reloadingFromCache;
|
||||||
private DiscoverInfoBannerHelper bannerHelper;
|
private DiscoverInfoBannerHelper bannerHelper;
|
||||||
|
private String unreadMarker, realUnreadMarker;
|
||||||
|
private MenuItem markAllReadItem;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean wantsComposeButton() {
|
protected boolean wantsComposeButton() {
|
||||||
@@ -63,13 +69,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
E.register(this);
|
E.register(this);
|
||||||
if(savedInstanceState!=null){
|
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
|
setHasOptionsMenu(true);
|
||||||
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
|
|
||||||
}
|
|
||||||
if (onlyPosts) {
|
|
||||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -81,14 +82,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity){
|
public void onAttach(Activity activity){
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
|
||||||
onlyPosts=getArguments().getBoolean("onlyPosts", false);
|
|
||||||
setTitle(R.string.notifications);
|
setTitle(R.string.notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||||
if(!onlyMentions && !onlyPosts){
|
if(!onlyMentions){
|
||||||
switch(n.type){
|
switch(n.type){
|
||||||
case MENTION -> {
|
case MENTION -> {
|
||||||
if(!getLocalPrefs().notificationFilters.mention)
|
if(!getLocalPrefs().notificationFilters.mention)
|
||||||
@@ -164,7 +163,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
AccountSessionManager.getInstance()
|
AccountSessionManager.getInstance()
|
||||||
.getAccount(accountID).getCacheController()
|
.getAccount(accountID).getCacheController()
|
||||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, false, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
@@ -186,7 +185,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRelationshipsLoaded(){
|
protected void onRelationshipsLoaded(){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
@@ -201,13 +200,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
protected void onShown(){
|
protected void onShown(){
|
||||||
super.onShown();
|
super.onShown();
|
||||||
if(!dataLoading){
|
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||||
if(onlyMentions){
|
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||||
refresh();
|
reloadingFromCache=true;
|
||||||
}else{
|
refresh();
|
||||||
reloadingFromCache=true;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,13 +264,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState){
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putBoolean("onlyMentions", onlyMentions);
|
|
||||||
outState.putBoolean("onlyPosts", onlyPosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Notification getNotificationByID(String id){
|
private Notification getNotificationByID(String id){
|
||||||
for(Notification n:data){
|
for(Notification n:data){
|
||||||
if(n.id.equals(id))
|
if(n.id.equals(id))
|
||||||
@@ -380,11 +369,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
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());
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if(item.getItemId()==R.id.mark_all_read){
|
||||||
|
markAsRead();
|
||||||
|
resetUnreadBackground();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markAsRead(){
|
||||||
|
if(data.isEmpty())
|
||||||
|
return;
|
||||||
|
String id=data.get(0).id;
|
||||||
|
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||||
|
new SaveMarkers(null, id).exec(accountID);
|
||||||
|
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||||
|
realUnreadMarker=id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void resetUnreadBackground(){
|
void resetUnreadBackground(){
|
||||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||||
nf.unreadMarker=nf.realUnreadMarker;
|
nf.unreadMarker=nf.realUnreadMarker;
|
||||||
@@ -396,13 +412,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
public void onRefresh(){
|
public void onRefresh(){
|
||||||
super.onRefresh();
|
super.onRefresh();
|
||||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||||
if (!onlyMentions && !onlyPosts) nf.markAsRead();
|
if (!onlyMentions) nf.markAsRead();
|
||||||
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||||
nf.unreadMarker=nf.realUnreadMarker=m;
|
nf.unreadMarker=nf.realUnreadMarker=m;
|
||||||
nf.updateMarkAllReadButton();
|
nf.updateMarkAllReadButton();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resetUnreadBackground();
|
resetUnreadBackground();
|
||||||
|
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||||
|
unreadMarker=realUnreadMarker=m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMarkAllReadButton(){
|
||||||
|
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -420,4 +443,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
? "/users/" + getSession().self.username + "/interactions"
|
? "/users/" + getSession().self.username + "/interactions"
|
||||||
: "/notifications").build();
|
: "/notifications").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canRefreshWithoutUpsettingUser(){
|
||||||
|
// TODO maybe reload notifications the same way we reload the home timelines, i.e. with gaps and stuff
|
||||||
|
if(data.size()<=itemsPerPage)
|
||||||
|
return true;
|
||||||
|
for(int i=list.getChildCount()-1;i>=0;i--){
|
||||||
|
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
|
||||||
|
String id=itemHolder.getItemID();
|
||||||
|
for(int j=0;j<data.size();j++){
|
||||||
|
if(data.get(j).id.equals(id))
|
||||||
|
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import android.annotation.SuppressLint;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.assist.AssistContent;
|
import android.app.assist.AssistContent;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
@@ -21,12 +23,9 @@ import android.graphics.drawable.LayerDrawable;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ImageSpan;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.transition.ChangeBounds;
|
import android.transition.ChangeBounds;
|
||||||
import android.transition.Fade;
|
import android.transition.Fade;
|
||||||
import android.transition.TransitionManager;
|
import android.transition.TransitionManager;
|
||||||
@@ -43,8 +42,6 @@ import android.view.ViewOutlineProvider;
|
|||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -72,11 +69,9 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
|||||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment;
|
import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.BlocksListFragment;
|
|
||||||
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
|
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
|
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment;
|
import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment;
|
||||||
import org.joinmastodon.android.fragments.account_list.MutesListFragment;
|
|
||||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||||
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
|
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
@@ -90,6 +85,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
|||||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||||
|
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||||
@@ -143,7 +139,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private ImageView avatar;
|
private ImageView avatar;
|
||||||
private CoverImageView cover;
|
private CoverImageView cover;
|
||||||
private View avatarBorder;
|
private View avatarBorder;
|
||||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
|
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
|
||||||
private ImageView lockIcon, botIcon;
|
private ImageView lockIcon, botIcon;
|
||||||
private ProgressBarButton actionButton, notifyButton;
|
private ProgressBarButton actionButton, notifyButton;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
@@ -154,7 +150,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private SwipeRefreshLayout refreshLayout;
|
private SwipeRefreshLayout refreshLayout;
|
||||||
private View followersBtn, followingBtn;
|
private View followersBtn, followingBtn;
|
||||||
private EditText nameEdit, bioEdit;
|
private EditText nameEdit, bioEdit;
|
||||||
private ProgressBar actionProgress, notifyProgress, noteSaveProgress;
|
private ProgressBar actionProgress, notifyProgress;
|
||||||
private FrameLayout[] tabViews;
|
private FrameLayout[] tabViews;
|
||||||
private TabLayoutMediator tabLayoutMediator;
|
private TabLayoutMediator tabLayoutMediator;
|
||||||
private TextView followsYouView;
|
private TextView followsYouView;
|
||||||
@@ -197,7 +193,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
|
|
||||||
// profile note
|
// profile note
|
||||||
private FrameLayout noteWrap;
|
private FrameLayout noteWrap;
|
||||||
private ImageButton noteSaveBtn;
|
|
||||||
private EditText noteEdit;
|
private EditText noteEdit;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -220,7 +215,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if(!isOwnProfile)
|
if(!isOwnProfile)
|
||||||
loadRelationship();
|
loadRelationship();
|
||||||
else if (isInstanceAkkoma()) {
|
else if (isInstanceAkkoma()) {
|
||||||
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
maxFields = (int) getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
profileAccountID=getArguments().getString("profileAccountID");
|
profileAccountID=getArguments().getString("profileAccountID");
|
||||||
@@ -250,6 +245,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
name=content.findViewById(R.id.name);
|
name=content.findViewById(R.id.name);
|
||||||
usernameWrap=content.findViewById(R.id.username_wrap);
|
usernameWrap=content.findViewById(R.id.username_wrap);
|
||||||
username=content.findViewById(R.id.username);
|
username=content.findViewById(R.id.username);
|
||||||
|
usernameDomain=content.findViewById(R.id.username_domain);
|
||||||
lockIcon=content.findViewById(R.id.lock_icon);
|
lockIcon=content.findViewById(R.id.lock_icon);
|
||||||
botIcon=content.findViewById(R.id.bot_icon);
|
botIcon=content.findViewById(R.id.bot_icon);
|
||||||
bio=content.findViewById(R.id.bio);
|
bio=content.findViewById(R.id.bio);
|
||||||
@@ -271,7 +267,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
|
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
|
||||||
actionProgress=content.findViewById(R.id.action_progress);
|
actionProgress=content.findViewById(R.id.action_progress);
|
||||||
notifyProgress=content.findViewById(R.id.notify_progress);
|
notifyProgress=content.findViewById(R.id.notify_progress);
|
||||||
noteSaveProgress=content.findViewById(R.id.note_save_progress);
|
|
||||||
fab=content.findViewById(R.id.fab);
|
fab=content.findViewById(R.id.fab);
|
||||||
followsYouView=content.findViewById(R.id.follows_you);
|
followsYouView=content.findViewById(R.id.follows_you);
|
||||||
countersLayout=content.findViewById(R.id.profile_counters);
|
countersLayout=content.findViewById(R.id.profile_counters);
|
||||||
@@ -288,46 +283,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
|
|
||||||
noteEdit=content.findViewById(R.id.note_edit);
|
noteEdit=content.findViewById(R.id.note_edit);
|
||||||
noteWrap=content.findViewById(R.id.note_edit_wrap);
|
noteWrap=content.findViewById(R.id.note_edit_wrap);
|
||||||
noteSaveBtn=content.findViewById(R.id.note_save_btn);
|
|
||||||
|
|
||||||
noteSaveBtn.setOnClickListener((v->{
|
|
||||||
savePrivateNote(noteEdit.getText().toString());
|
|
||||||
InputMethodManager imm=(InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
|
|
||||||
noteEdit.clearFocus();
|
|
||||||
noteSaveBtn.clearFocus();
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
|
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
|
||||||
if(hasFocus){
|
if(hasFocus){
|
||||||
hideFab();
|
hideFab();
|
||||||
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
|
|
||||||
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||||
}else if(!noteSaveBtn.hasFocus()){
|
}else{
|
||||||
showFab();
|
showFab();
|
||||||
hideNoteSaveBtnIfNotDirty();
|
savePrivateNote(noteEdit.getText().toString());
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
noteEdit.addTextChangedListener(new TextWatcher(){
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
|
||||||
if(relationship!=null && noteSaveBtn.getVisibility()!=View.VISIBLE && !s.toString().equals(relationship.note))
|
|
||||||
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s){}
|
|
||||||
});
|
|
||||||
|
|
||||||
noteSaveBtn.setOnFocusChangeListener((v, hasFocus)->{
|
|
||||||
if(!hasFocus && !noteEdit.hasFocus()){
|
|
||||||
showFab();
|
|
||||||
hideNoteSaveBtnIfNotDirty();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,28 +398,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||||
|
|
||||||
content.findViewById(R.id.username_wrap).setOnClickListener(v->{
|
content.findViewById(R.id.username_wrap).setOnClickListener(v->{
|
||||||
try {
|
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
|
||||||
new GetInstance()
|
|
||||||
.setCallback(new Callback<>(){
|
|
||||||
@Override
|
|
||||||
public void onSuccess(Instance result){
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putParcelable("instance", Parcels.wrap(result));
|
|
||||||
args.putString("account", accountID);
|
|
||||||
Nav.go(getActivity(), SettingsServerFragment.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(ErrorResponse error){
|
|
||||||
error.showToast(getContext());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.wrapProgress((Activity) getContext(), R.string.loading, true)
|
|
||||||
.execRemote(Uri.parse(account.url).getHost());
|
|
||||||
} catch (NullPointerException ignored) {
|
|
||||||
// maybe the url was malformed?
|
|
||||||
Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
content.findViewById(R.id.username_wrap).setOnLongClickListener(v->{
|
content.findViewById(R.id.username_wrap).setOnLongClickListener(v->{
|
||||||
@@ -464,7 +406,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if(!usernameString.contains("@")){
|
if(!usernameString.contains("@")){
|
||||||
usernameString+="@"+domain;
|
usernameString+="@"+domain;
|
||||||
}
|
}
|
||||||
UiUtils.copyText(username, '@'+usernameString);
|
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+usernameString));
|
||||||
|
UiUtils.maybeShowTextCopiedToast(getActivity());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -492,13 +435,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||||
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||||
|
|
||||||
return sizeWrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideNoteSaveBtnIfNotDirty(){
|
// qrCodeButton.setOnClickListener(v->{
|
||||||
if(noteEdit.getText().toString().equals(relationship.note)){
|
// Bundle args=new Bundle();
|
||||||
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
|
// args.putString("account", accountID);
|
||||||
}
|
// args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||||
|
// ProfileQrCodeFragment qf=new ProfileQrCodeFragment();
|
||||||
|
// qf.setArguments(args);
|
||||||
|
// qf.show(getChildFragmentManager(), "qrDialog");
|
||||||
|
// });
|
||||||
|
|
||||||
|
return sizeWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPrivateNote(){
|
private void showPrivateNote(){
|
||||||
@@ -517,8 +464,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
V.setVisibilityAnimated(noteSaveProgress, View.VISIBLE);
|
|
||||||
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
|
|
||||||
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
|
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Relationship result) {
|
public void onSuccess(Relationship result) {
|
||||||
@@ -529,8 +474,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(getContext());
|
error.showToast(getContext());
|
||||||
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
|
|
||||||
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).exec(accountID);
|
||||||
}
|
}
|
||||||
@@ -693,6 +636,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
getChildFragmentManager().putFragment(outState, "pinnedPosts", pinnedPostsFragment);
|
getChildFragmentManager().putFragment(outState, "pinnedPosts", pinnedPostsFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHidden(){
|
||||||
|
if (relationship != null && !noteEdit.getText().toString().equals(relationship.note)){
|
||||||
|
savePrivateNote(noteEdit.getText().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig){
|
public void onConfigurationChanged(Configuration newConfig){
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
@@ -755,13 +705,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
// boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||||
|
|
||||||
String acct = ((isSelf || account.isRemote)
|
// String acct = ((isSelf || account.isRemote)
|
||||||
? account.getFullyQualifiedName()
|
// ? account.getFullyQualifiedName()
|
||||||
: account.acct);
|
// : account.acct);
|
||||||
|
|
||||||
username.setText('@'+acct);
|
username.setText("@"+account.username);
|
||||||
|
|
||||||
|
String domain=account.getDomain();
|
||||||
|
if(TextUtils.isEmpty(domain))
|
||||||
|
domain=AccountSessionManager.get(accountID).domain;
|
||||||
|
usernameDomain.setText(domain);
|
||||||
|
|
||||||
lockIcon.setVisibility(account.locked ? View.VISIBLE : View.GONE);
|
lockIcon.setVisibility(account.locked ? View.VISIBLE : View.GONE);
|
||||||
lockIcon.setImageTintList(ColorStateList.valueOf(username.getCurrentTextColor()));
|
lockIcon.setImageTintList(ColorStateList.valueOf(username.getCurrentTextColor()));
|
||||||
@@ -780,7 +735,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
||||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
|
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
|
||||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
||||||
|
|
||||||
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
|
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
|
||||||
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
|
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
|
||||||
if (account.followersCount < 0 || account.followingCount < 0)
|
if (account.followersCount < 0 || account.followingCount < 0)
|
||||||
@@ -812,7 +767,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
for(AccountField field:account.fields){
|
for(AccountField field:account.fields){
|
||||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||||
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
|
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
|
||||||
ssb=new SpannableStringBuilder(field.name);
|
ssb=new SpannableStringBuilder(field.name);
|
||||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||||
@@ -857,23 +812,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return;
|
return;
|
||||||
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
||||||
if(isOwnProfile){
|
if(isOwnProfile){
|
||||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.scheduled, R.id.bookmarks, R.id.favorites);
|
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.scheduled, R.id.bookmarks);
|
||||||
}else{
|
}else{
|
||||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags, R.id.favorites, R.id.scheduled);
|
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.edit_note);
|
||||||
}
|
}
|
||||||
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
||||||
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
|
menu.findItem(R.id.open_with_account).setVisible(hasMultipleAccounts);
|
||||||
openWithAccounts.setVisible(hasMultipleAccounts);
|
|
||||||
SubMenu accountsMenu=openWithAccounts.getSubMenu();
|
|
||||||
if(hasMultipleAccounts){
|
|
||||||
accountsMenu.clear();
|
|
||||||
UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL(
|
|
||||||
getActivity(), s.getID(), account.url, false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isOwnProfile) {
|
if(isOwnProfile) {
|
||||||
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
||||||
|
menu.findItem(R.id.favorites).setIcon(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular);
|
||||||
|
UiUtils.insetPopupMenuIcon(getContext(), menu.findItem(R.id.favorites));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,18 +851,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
}else{
|
}else{
|
||||||
blockDomain.setVisible(false);
|
blockDomain.setVisible(false);
|
||||||
}
|
}
|
||||||
menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty())
|
boolean canAddNote = noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty());
|
||||||
? R.string.sk_add_note : R.string.sk_delete_note);
|
menu.findItem(R.id.edit_note).setTitle(canAddNote ? R.string.sk_add_note : R.string.sk_delete_note);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item){
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
int id=item.getItemId();
|
int id=item.getItemId();
|
||||||
if(id==R.id.share){
|
if(id==R.id.share){
|
||||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
UiUtils.openSystemShareSheet(getActivity(), account);
|
||||||
intent.setType("text/plain");
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
|
||||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
|
||||||
}else if(id==R.id.mute){
|
}else if(id==R.id.mute){
|
||||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||||
}else if(id==R.id.block){
|
}else if(id==R.id.block){
|
||||||
@@ -994,8 +940,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||||
imm.showSoftInput(noteEdit, 0);
|
imm.showSoftInput(noteEdit, 0);
|
||||||
}, 100);
|
}, 100);
|
||||||
}else if(relationship.note.isEmpty()){
|
}else if(relationship.note.isEmpty() && noteEdit.getText().toString().isEmpty()){
|
||||||
hidePrivateNote();
|
hidePrivateNote();
|
||||||
|
noteEdit.clearFocus();
|
||||||
|
noteEdit.postDelayed(()->{
|
||||||
|
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||||
|
imm.hideSoftInputFromWindow(noteEdit.getWindowToken(), 0);
|
||||||
|
}, 100);
|
||||||
UiUtils.beginLayoutTransition(scrollableContent);
|
UiUtils.beginLayoutTransition(scrollableContent);
|
||||||
}else{
|
}else{
|
||||||
new M3AlertDialogBuilder(getActivity())
|
new M3AlertDialogBuilder(getActivity())
|
||||||
@@ -1005,6 +956,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
|
}else if(id==R.id.open_with_account){
|
||||||
|
UiUtils.pickAccount(getActivity(), accountID, R.string.sk_open_with_account, R.drawable.ic_fluent_person_swap_24_regular, session ->UiUtils.openURL(
|
||||||
|
getActivity(), session.getID(), account.url, false
|
||||||
|
), null);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1038,13 +993,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||||
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
||||||
noteSaveProgress.setIndeterminateTintList(noteEdit.getTextColors());
|
|
||||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||||
notifyButton.setSelected(relationship.notifying);
|
notifyButton.setSelected(relationship.notifying);
|
||||||
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
||||||
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
|
|
||||||
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
|
|
||||||
UiUtils.beginLayoutTransition(scrollableContent);
|
UiUtils.beginLayoutTransition(scrollableContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,7 +1351,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return;
|
return;
|
||||||
int radius=V.dp(25);
|
int radius=V.dp(25);
|
||||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() : account.avatar, ava), 0,
|
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() : account.avatar, ava), 0,
|
||||||
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
|
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1411,7 +1363,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if(drawable==null || drawable instanceof ColorDrawable)
|
if(drawable==null || drawable instanceof ColorDrawable)
|
||||||
return;
|
return;
|
||||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
|
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
|
||||||
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1635,8 +1587,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
public void setImage(int index, Drawable image){
|
public void setImage(int index, Drawable image){
|
||||||
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
|
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
|
||||||
span.setDrawable(image);
|
span.setDrawable(image);
|
||||||
title.invalidate();
|
title.setText(title.getText());
|
||||||
value.invalidate();
|
value.setText(value.getText());
|
||||||
|
toolbarTitleView.setText(toolbarTitleView.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
||||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, null,
|
return StatusDisplayItem.buildItems(this, s.toFormattedStatus(accountID), accountID, s, knownAccounts, null,
|
||||||
StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS |
|
StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS |
|
||||||
StatusDisplayItem.FLAG_NO_FOOTER |
|
StatusDisplayItem.FLAG_NO_FOOTER |
|
||||||
StatusDisplayItem.FLAG_NO_TRANSLATE);
|
StatusDisplayItem.FLAG_NO_TRANSLATE);
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
|
|||||||
|
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
|
||||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
|
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
|
||||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
||||||
@@ -20,6 +27,7 @@ import org.joinmastodon.android.model.Instance;
|
|||||||
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||||
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||||
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
||||||
@@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
private ProgressBar defaultServerProgress;
|
private ProgressBar defaultServerProgress;
|
||||||
private String chosenDefaultServer=DEFAULT_SERVER;
|
private String chosenDefaultServer=DEFAULT_SERVER;
|
||||||
private boolean loadingDefaultServer, loadedDefaultServer;
|
private boolean loadingDefaultServer, loadedDefaultServer;
|
||||||
|
private Uri currentInviteLink;
|
||||||
|
private ProgressDialog instanceLoadingProgress;
|
||||||
|
private String inviteCode;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
Bundle extras=new Bundle();
|
Bundle extras=new Bundle();
|
||||||
boolean isSignup=v.getId()==R.id.btn_get_started;
|
boolean isSignup=v.getId()==R.id.btn_get_started;
|
||||||
extras.putBoolean("signup", isSignup);
|
extras.putBoolean("signup", isSignup);
|
||||||
|
extras.putString("defaultServer", chosenDefaultServer);
|
||||||
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onJoinDefaultServerClick(View v){
|
private void onJoinDefaultServerClick(View v){
|
||||||
if(loadingDefaultServer)
|
if(loadingDefaultServer)
|
||||||
return;
|
return;
|
||||||
|
instanceLoadingProgress=new ProgressDialog(getActivity());
|
||||||
|
instanceLoadingProgress.setCancelable(false);
|
||||||
|
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
|
||||||
|
instanceLoadingProgress.show();
|
||||||
|
if(currentInviteLink!=null){
|
||||||
|
new CheckInviteLink(currentInviteLink.getPath())
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(CheckInviteLink.Response result){
|
||||||
|
inviteCode=result.inviteCode;
|
||||||
|
proceedWithServerDomain(currentInviteLink.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
if(getActivity()==null)
|
||||||
|
return;
|
||||||
|
instanceLoadingProgress.dismiss();
|
||||||
|
instanceLoadingProgress=null;
|
||||||
|
if(error instanceof MastodonErrorResponse mer){
|
||||||
|
switch(mer.httpStatus){
|
||||||
|
case 401 -> new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.expired_invite_link)
|
||||||
|
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
case 404 -> new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.invalid_invite_link)
|
||||||
|
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
default -> error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execNoAuth(currentInviteLink.getHost());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proceedWithServerDomain(chosenDefaultServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void proceedWithServerDomain(String domain){
|
||||||
new GetInstance()
|
new GetInstance()
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Instance result){
|
public void onSuccess(Instance result){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
return;
|
return;
|
||||||
if(!result.registrations){
|
instanceLoadingProgress.dismiss();
|
||||||
|
instanceLoadingProgress=null;
|
||||||
|
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
|
||||||
new M3AlertDialogBuilder(getActivity())
|
new M3AlertDialogBuilder(getActivity())
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(R.string.instance_signup_closed)
|
.setMessage(R.string.instance_signup_closed)
|
||||||
@@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
}
|
}
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putParcelable("instance", Parcels.wrap(result));
|
args.putParcelable("instance", Parcels.wrap(result));
|
||||||
|
if(inviteCode!=null)
|
||||||
|
args.putString("inviteCode", inviteCode);
|
||||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
return;
|
return;
|
||||||
|
instanceLoadingProgress.dismiss();
|
||||||
|
instanceLoadingProgress=null;
|
||||||
error.showToast(getActivity());
|
error.showToast(getActivity());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress(getActivity(), R.string.loading_instance, true)
|
.execNoAuth(domain);
|
||||||
.execNoAuth(chosenDefaultServer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLearnMoreClick(View v){
|
private void onLearnMoreClick(View v){
|
||||||
@@ -198,7 +258,18 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadAndChooseDefaultServer(){
|
private void loadAndChooseDefaultServer(){
|
||||||
loadingDefaultServer=true;
|
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
|
||||||
|
if(clipData!=null && clipData.getItemCount()>0){
|
||||||
|
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
|
||||||
|
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
|
||||||
|
currentInviteLink=Uri.parse(clipText.toString());
|
||||||
|
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
loadingDefaultServer=true;
|
||||||
|
defaultServerButton.setTextVisible(false);
|
||||||
|
defaultServerProgress.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
new GetCatalogDefaultInstances()
|
new GetCatalogDefaultInstances()
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
@@ -241,7 +312,7 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
chosenDefaultServer=domain;
|
chosenDefaultServer=domain;
|
||||||
loadingDefaultServer=false;
|
loadingDefaultServer=false;
|
||||||
loadedDefaultServer=true;
|
loadedDefaultServer=true;
|
||||||
if(defaultServerButton!=null && getActivity()!=null){
|
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
|
||||||
defaultServerButton.setTextVisible(true);
|
defaultServerButton.setTextVisible(true);
|
||||||
defaultServerProgress.setVisibility(View.GONE);
|
defaultServerProgress.setVisibility(View.GONE);
|
||||||
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
|
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package org.joinmastodon.android.fragments;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
@@ -13,7 +13,6 @@ import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
|
|||||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@@ -26,6 +25,8 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import name.fraser.neil.plaintext.diff_match_patch;
|
import name.fraser.neil.plaintext.diff_match_patch;
|
||||||
|
|
||||||
@@ -54,12 +55,47 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
|||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||||
|
if(result.size()<=1&& GlobalUserPreferences.allowRemoteLoading) {
|
||||||
|
//server send only a single edit, which is always the original status
|
||||||
|
//try to get the complete history from the remote server
|
||||||
|
loadRemoteData(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
onDataLoaded(result, false);
|
onDataLoaded(result, false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void loadRemoteData(List<Status> prevData){
|
||||||
|
String remoteURL = Uri.parse(url).getHost();
|
||||||
|
String[] parts=url.split("/");
|
||||||
|
|
||||||
|
if(parts.length==0||remoteURL==null) {
|
||||||
|
onDataLoaded(prevData, false);
|
||||||
|
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new GetStatusEditHistory(parts[parts.length-1])
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Status> result){
|
||||||
|
if(getActivity()==null) return;
|
||||||
|
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||||
|
onDataLoaded(result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse errorResponse){
|
||||||
|
//fallback to previously loaded data
|
||||||
|
onDataLoaded(prevData, false);
|
||||||
|
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execNoAuth(remoteURL);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
List<StatusDisplayItem> items=new ArrayList<>();
|
List<StatusDisplayItem> items=new ArrayList<>();
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
|||||||
flags |= StatusDisplayItem.FLAG_NO_TRANSLATE;
|
flags |= StatusDisplayItem.FLAG_NO_TRANSLATE;
|
||||||
if(!GlobalUserPreferences.showMediaPreview)
|
if(!GlobalUserPreferences.showMediaPreview)
|
||||||
flags |= StatusDisplayItem.FLAG_NO_MEDIA_PREVIEW;
|
flags |= StatusDisplayItem.FLAG_NO_MEDIA_PREVIEW;
|
||||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags);
|
/* MOSHIDON: we make the filterContext null in the main status in the thread fragment, so that the main status is never filtered (because you just clicked on it).
|
||||||
|
This also restores old behavior that got lost to time and changes in the filter system */
|
||||||
|
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, isMainThreadStatus ? null : getFilterContext(), isMainThreadStatus ? 0 : flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract FilterContext getFilterContext();
|
protected abstract FilterContext getFilterContext();
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ package org.joinmastodon.android.fragments;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
@@ -23,6 +31,7 @@ import org.joinmastodon.android.model.Account;
|
|||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.StatusContext;
|
import org.joinmastodon.android.model.StatusContext;
|
||||||
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
@@ -50,7 +59,13 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||||
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||||
@@ -58,10 +73,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||||
private StatusContext result;
|
private StatusContext result;
|
||||||
protected boolean contextInitiallyRendered, transitionFinished, preview;
|
protected boolean contextInitiallyRendered, transitionFinished, preview;
|
||||||
|
private FrameLayout replyContainer;
|
||||||
|
private LinearLayout replyButton;
|
||||||
|
private ImageView replyButtonAva;
|
||||||
|
private TextView replyButtonText;
|
||||||
|
private int lastBottomInset;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
setLayout(R.layout.fragment_thread);
|
||||||
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||||
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
|
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
|
||||||
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
|
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
|
||||||
@@ -143,10 +164,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||||
if(s.id.equals(mainStatus.id)) {
|
if(s.id.equals(mainStatus.id)) {
|
||||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
|
itemsToModify.add(itemsToModify.size()-1, new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -207,12 +228,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
s.spoilerRevealed = oldStatus.spoilerRevealed;
|
s.spoilerRevealed = oldStatus.spoilerRevealed;
|
||||||
s.sensitiveRevealed = oldStatus.sensitiveRevealed;
|
s.sensitiveRevealed = oldStatus.sensitiveRevealed;
|
||||||
s.filterRevealed = oldStatus.filterRevealed;
|
s.filterRevealed = oldStatus.filterRevealed;
|
||||||
|
s.textExpanded = oldStatus.textExpanded;
|
||||||
}
|
}
|
||||||
if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
|
if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
|
||||||
s.spoilerText != null &&
|
s.spoilerText != null){
|
||||||
s.spoilerText.equals(mainStatus.spoilerText)) {
|
if (s.spoilerText.equals(mainStatus.spoilerText) ||
|
||||||
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
|
(s.spoilerText.toLowerCase().startsWith("re: ") &&
|
||||||
s.spoilerRevealed = mainStatus.spoilerRevealed;
|
s.spoilerText.substring(4).equals(mainStatus.spoilerText))){
|
||||||
|
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
|
||||||
|
s.spoilerRevealed = mainStatus.spoilerRevealed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,6 +308,13 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
updatedStatus.filterRevealed = mainStatus.filterRevealed;
|
updatedStatus.filterRevealed = mainStatus.filterRevealed;
|
||||||
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
|
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
|
||||||
updatedStatus.sensitiveRevealed = mainStatus.sensitiveRevealed;
|
updatedStatus.sensitiveRevealed = mainStatus.sensitiveRevealed;
|
||||||
|
updatedStatus.textExpanded = mainStatus.textExpanded;
|
||||||
|
if(updatedStatus.quote!=null && mainStatus.quote!=null){
|
||||||
|
updatedStatus.quote.filterRevealed = mainStatus.quote.filterRevealed;
|
||||||
|
updatedStatus.quote.spoilerRevealed = mainStatus.quote.spoilerRevealed;
|
||||||
|
updatedStatus.quote.sensitiveRevealed = mainStatus.quote.sensitiveRevealed;
|
||||||
|
updatedStatus.quote.textExpanded = mainStatus.quote.textExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
// returning fired event object to facilitate testing
|
// returning fired event object to facilitate testing
|
||||||
Object event;
|
Object event;
|
||||||
@@ -387,6 +419,22 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
replyContainer=view.findViewById(R.id.reply_button_wrapper);
|
||||||
|
replyButton=replyContainer.findViewById(R.id.reply_button);
|
||||||
|
replyButtonText=replyButton.findViewById(R.id.reply_btn_text);
|
||||||
|
replyButtonAva=replyButton.findViewById(R.id.avatar);
|
||||||
|
replyButton.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||||
|
replyButton.setClipToOutline(true);
|
||||||
|
replyButtonText.setText(HtmlParser.parseCustomEmoji(getString(R.string.reply_to_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||||
|
UiUtils.loadCustomEmojiInTextView(replyButtonText);
|
||||||
|
replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
|
||||||
|
replyButtonAva.setClipToOutline(true);
|
||||||
|
replyButton.setOnClickListener(v->openReply(mainStatus, accountID));
|
||||||
|
replyButton.setOnLongClickListener(this::onReplyLongClick);
|
||||||
|
Account self=AccountSessionManager.get(accountID).self;
|
||||||
|
if(!TextUtils.isEmpty(self.avatar)){
|
||||||
|
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
|
||||||
|
}
|
||||||
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
|
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
|
||||||
showContent();
|
showContent();
|
||||||
if(!loaded)
|
if(!loaded)
|
||||||
@@ -526,4 +574,36 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
}
|
}
|
||||||
super.onErrorRetryClick();
|
super.onErrorRetryClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
lastBottomInset=insets.getSystemWindowInsetBottom();
|
||||||
|
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openReply(Status status, String accountID){
|
||||||
|
maybeShowPreReplySheet(status, ()->{
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("replyTo", Parcels.wrap(status));
|
||||||
|
args.putBoolean("fromThreadFragment", true);
|
||||||
|
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private boolean onReplyLongClick(View v) {
|
||||||
|
if(mainStatus.preview) return false;
|
||||||
|
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||||
|
UiUtils.pickAccount(v.getContext(), accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
|
||||||
|
String pickedAccountID = session.getID();
|
||||||
|
UiUtils.lookupStatus(v.getContext(), mainStatus, pickedAccountID, accountID, status -> {
|
||||||
|
if (status == null) return;
|
||||||
|
openReply(status, pickedAccountID);
|
||||||
|
});
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSnackbarOffset(){
|
||||||
|
return replyContainer.getHeight()-lastBottomInset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import android.text.TextUtils;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.SearchResults;
|
import org.joinmastodon.android.model.SearchResults;
|
||||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||||
@@ -14,13 +16,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
|||||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
public class AccountSearchFragment extends BaseAccountListFragment{
|
||||||
private String currentQuery;
|
protected String currentQuery;
|
||||||
private boolean resultDelivered;
|
private boolean resultDelivered;
|
||||||
private SearchViewHelper searchViewHelper;
|
private SearchViewHelper searchViewHelper;
|
||||||
|
|
||||||
@@ -29,12 +32,11 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setRefreshEnabled(false);
|
setRefreshEnabled(false);
|
||||||
setEmptyText("");
|
setEmptyText("");
|
||||||
dataLoaded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
|
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder());
|
||||||
searchViewHelper.setListeners(this::onQueryChanged, null);
|
searchViewHelper.setListeners(this::onQueryChanged, null);
|
||||||
searchViewHelper.addDivider(contentView);
|
searchViewHelper.addDivider(contentView);
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
@@ -52,13 +54,21 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
|||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults result){
|
public void onSuccess(SearchResults result){
|
||||||
setEmptyText(R.string.no_search_results);
|
AccountSearchFragment.this.onSuccess(result.accounts);
|
||||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void onSuccess(List<Account> result){
|
||||||
|
setEmptyText(R.string.no_search_results);
|
||||||
|
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getSearchViewPlaceholder(){
|
||||||
|
return getString(R.string.search_hint);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onUpdateToolbar(){
|
protected void onUpdateToolbar(){
|
||||||
super.onUpdateToolbar();
|
super.onUpdateToolbar();
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class AddListMembersFragment extends AccountSearchFragment{
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
dataLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
refreshing=true;
|
||||||
|
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Account> result){
|
||||||
|
AddListMembersFragment.this.onSuccess(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getSearchViewPlaceholder(){
|
||||||
|
return getString(R.string.search_among_people_you_follow);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
@SuppressLint("ValidFragment") // This shouldn't be part of any saved states anyway
|
||||||
|
public class AddNewListMembersFragment extends AccountSearchFragment{
|
||||||
|
private Listener listener;
|
||||||
|
private String maxID;
|
||||||
|
|
||||||
|
public AddNewListMembersFragment(Listener listener){
|
||||||
|
this.listener=listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
if(TextUtils.isEmpty(currentQuery)){
|
||||||
|
currentRequest=new GetAccountFollowing(AccountSessionManager.get(accountID).self.id, offset>0 ? maxID : null, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
setEmptyText("");
|
||||||
|
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
|
||||||
|
maxID=result.getNextPageMaxID();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}else{
|
||||||
|
refreshing=true;
|
||||||
|
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Account> result){
|
||||||
|
AddNewListMembersFragment.this.onSuccess(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getSearchViewPlaceholder(){
|
||||||
|
return getString(R.string.search_among_people_you_follow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||||
|
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||||
|
holder.setOnLongClickListener(vh->false);
|
||||||
|
Button button=holder.getButton();
|
||||||
|
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||||
|
button.setMinimumWidth(0);
|
||||||
|
button.setMinWidth(0);
|
||||||
|
button.setOnClickListener(v->{
|
||||||
|
holder.setActionProgressVisible(true);
|
||||||
|
holder.itemView.setHasTransientState(true);
|
||||||
|
Runnable onDone=()->{
|
||||||
|
holder.setActionProgressVisible(false);
|
||||||
|
holder.itemView.setHasTransientState(false);
|
||||||
|
onBindViewHolder(holder);
|
||||||
|
};
|
||||||
|
AccountViewModel account=holder.getItem();
|
||||||
|
if(listener.isAccountInList(account)){
|
||||||
|
listener.removeAccountAccountFromList(account, onDone);
|
||||||
|
}else{
|
||||||
|
listener.addAccountToList(account, onDone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindViewHolder(AccountViewHolder holder){
|
||||||
|
Button button=holder.getButton();
|
||||||
|
int textRes, styleRes;
|
||||||
|
if(listener.isAccountInList(holder.getItem())){
|
||||||
|
textRes=R.string.remove;
|
||||||
|
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||||
|
}else{
|
||||||
|
textRes=R.string.add;
|
||||||
|
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||||
|
}
|
||||||
|
button.setText(textRes);
|
||||||
|
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||||
|
button.setBackground(ta.getDrawable(0));
|
||||||
|
ta.recycle();
|
||||||
|
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||||
|
button.setTextColor(ta.getColorStateList(0));
|
||||||
|
ta.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener{
|
||||||
|
boolean isAccountInList(AccountViewModel account);
|
||||||
|
void addAccountToList(AccountViewModel account, Runnable onDone);
|
||||||
|
void removeAccountAccountFromList(AccountViewModel account, Runnable onDone);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
|||||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||||
protected String accountID;
|
protected String accountID;
|
||||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||||
|
protected int itemLayoutRes=R.layout.item_account_list;
|
||||||
|
|
||||||
public BaseAccountListFragment(){
|
public BaseAccountListFragment(){
|
||||||
super(40);
|
super(40);
|
||||||
@@ -74,6 +75,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
|||||||
|
|
||||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||||
|
if(ids.isEmpty())
|
||||||
|
return;
|
||||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||||
relationshipsRequests.add(req);
|
relationshipsRequests.add(req);
|
||||||
req.setCallback(new Callback<>(){
|
req.setCallback(new Callback<>(){
|
||||||
@@ -124,13 +127,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
|||||||
Toolbar toolbar=getToolbar();
|
Toolbar toolbar=getToolbar();
|
||||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||||
toolbar.setNavigationContentDescription(R.string.back);
|
toolbar.setNavigationContentDescription(R.string.back);
|
||||||
if(hasSubtitle()){
|
|
||||||
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
|
|
||||||
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
|
|
||||||
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
|
|
||||||
toolbar.setTitleTextColor(color);
|
|
||||||
toolbar.setSubtitleTextColor(color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +158,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||||
|
protected void onBindViewHolder(AccountViewHolder holder){}
|
||||||
|
|
||||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
public AccountsAdapter(){
|
public AccountsAdapter(){
|
||||||
@@ -171,7 +168,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
|
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
|
||||||
onConfigureViewHolder(holder);
|
onConfigureViewHolder(holder);
|
||||||
return holder;
|
return holder;
|
||||||
}
|
}
|
||||||
@@ -179,6 +176,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
|||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||||
holder.bind(data.get(position));
|
holder.bind(data.get(position));
|
||||||
|
BaseAccountListFragment.this.onBindViewHolder(holder);
|
||||||
super.onBindViewHolder(holder, position);
|
super.onBindViewHolder(holder, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,8 +279,8 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
|
|||||||
cover.setImageDrawable(image);
|
cover.setImageDrawable(image);
|
||||||
}else{
|
}else{
|
||||||
item.emojiHelper.setImageDrawable(index-2, image);
|
item.emojiHelper.setImageDrawable(index-2, image);
|
||||||
name.invalidate();
|
name.setText(name.getText());
|
||||||
bio.invalidate();
|
bio.setText(bio.getText());
|
||||||
}
|
}
|
||||||
if(image instanceof Animatable a && !a.isRunning())
|
if(image instanceof Animatable a && !a.isRunning())
|
||||||
a.start();
|
a.start();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
|
|||||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -35,7 +37,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
|||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop {
|
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent{
|
||||||
private static final int QUERY_RESULT=937;
|
private static final int QUERY_RESULT=937;
|
||||||
|
|
||||||
private TabLayout tabLayout;
|
private TabLayout tabLayout;
|
||||||
@@ -80,8 +82,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
for(int i=0;i<tabViews.length;i++){
|
for(int i=0;i<tabViews.length;i++){
|
||||||
FrameLayout tabView=new FrameLayout(getActivity());
|
FrameLayout tabView=new FrameLayout(getActivity());
|
||||||
tabView.setId(switch(i){
|
tabView.setId(switch(i){
|
||||||
case 0 -> R.id.discover_hashtags;
|
case 0 -> R.id.discover_posts;
|
||||||
case 1 -> R.id.discover_posts;
|
case 1 -> R.id.discover_hashtags;
|
||||||
case 2 -> R.id.discover_news;
|
case 2 -> R.id.discover_news;
|
||||||
case 3 -> R.id.discover_users;
|
case 3 -> R.id.discover_users;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||||
@@ -125,8 +127,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
accountsFragment.setArguments(args);
|
accountsFragment.setArguments(args);
|
||||||
|
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
|
||||||
.add(R.id.discover_posts, postsFragment)
|
.add(R.id.discover_posts, postsFragment)
|
||||||
|
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||||
.add(R.id.discover_news, newsFragment)
|
.add(R.id.discover_news, newsFragment)
|
||||||
.add(R.id.discover_users, accountsFragment)
|
.add(R.id.discover_users, accountsFragment)
|
||||||
.commit();
|
.commit();
|
||||||
@@ -136,8 +138,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
@Override
|
@Override
|
||||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||||
tab.setText(switch(position){
|
tab.setText(switch(position){
|
||||||
case 0 -> R.string.hashtags;
|
case 0 -> R.string.posts;
|
||||||
case 1 -> R.string.posts;
|
case 1 -> R.string.hashtags;
|
||||||
case 2 -> R.string.news;
|
case 2 -> R.string.news;
|
||||||
case 3 -> R.string.for_you;
|
case 3 -> R.string.for_you;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||||
@@ -258,8 +260,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
|
|
||||||
private Fragment getFragmentForPage(int page){
|
private Fragment getFragmentForPage(int page){
|
||||||
return switch(page){
|
return switch(page){
|
||||||
case 0 -> hashtagsFragment;
|
case 0 -> postsFragment;
|
||||||
case 1 -> postsFragment;
|
case 1 -> hashtagsFragment;
|
||||||
case 2 -> newsFragment;
|
case 2 -> newsFragment;
|
||||||
case 3 -> accountsFragment;
|
case 3 -> accountsFragment;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||||
@@ -291,6 +293,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProvideAssistContent(AssistContent assistContent) {
|
||||||
|
callFragmentToProvideAssistContent(searchActive
|
||||||
|
? searchFragment
|
||||||
|
: getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||||
|
}
|
||||||
|
|
||||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments.discover;
|
|||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -19,6 +20,8 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
|||||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -27,6 +30,7 @@ import java.util.stream.Collectors;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||||
@@ -40,7 +44,7 @@ import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop, IsOnTop{
|
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri{
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private DiscoverInfoBannerHelper bannerHelper;
|
private DiscoverInfoBannerHelper bannerHelper;
|
||||||
private MergeRecyclerAdapter mergeAdapter;
|
private MergeRecyclerAdapter mergeAdapter;
|
||||||
@@ -115,6 +119,16 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
|
|||||||
return isRecyclerViewOnTop(list);
|
return isRecyclerViewOnTop(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccountID() {
|
||||||
|
return accountID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
|
||||||
|
}
|
||||||
|
|
||||||
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
private final List<CardViewModel> data;
|
private final List<CardViewModel> data;
|
||||||
|
|
||||||
@@ -203,7 +217,16 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(){
|
public void onClick(){
|
||||||
UiUtils.launchWebBrowser(getActivity(), item.url);
|
//TODO: enable timeline for all servers once 4.3.0 is released
|
||||||
|
if(getInstance().isEmpty() ||
|
||||||
|
!getInstance().get().checkVersion(4,3,0)){
|
||||||
|
UiUtils.launchWebBrowser(getActivity(), item.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("trendingLink", Parcels.wrap(item));
|
||||||
|
Nav.go(getActivity(), DiscoverTrendingLinkTimelineFragment.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
|||||||
|
|
||||||
public class DiscoverPostsFragment extends StatusListFragment{
|
public class DiscoverPostsFragment extends StatusListFragment{
|
||||||
private DiscoverInfoBannerHelper bannerHelper;
|
private DiscoverInfoBannerHelper bannerHelper;
|
||||||
private int offset;
|
private int realOffset=0;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -26,26 +26,23 @@ public class DiscoverPostsFragment extends StatusListFragment{
|
|||||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
|
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int o, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
if(refreshing) offset=0;
|
currentRequest=new GetTrendingStatuses(offset==0 ? 0 : realOffset, count)
|
||||||
currentRequest=new GetTrendingStatuses(offset, count)
|
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
boolean empty=result.isEmpty();
|
realOffset+=result.size();
|
||||||
offset+=result.size();
|
|
||||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||||
onDataLoaded(result, !empty);
|
onDataLoaded(result, !result.isEmpty());
|
||||||
bannerHelper.onBannerBecameVisible();
|
bannerHelper.onBannerBecameVisible();
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected RecyclerView.Adapter<?> getAdapter(){
|
protected RecyclerView.Adapter getAdapter(){
|
||||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||||
bannerHelper.maybeAddBanner(list, adapter);
|
bannerHelper.maybeAddBanner(list, adapter);
|
||||||
adapter.addAdapter(super.getAdapter());
|
adapter.addAdapter(super.getAdapter());
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.timelines.GetTrendingLinksTimeline;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
|
import org.joinmastodon.android.fragments.HomeTabFragment;
|
||||||
|
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Card;
|
||||||
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
//TODO: replace this implementation when upstream implements their own design
|
||||||
|
public class DiscoverTrendingLinkTimelineFragment extends StatusListFragment{
|
||||||
|
private Card trendingLink;
|
||||||
|
private TextView headerTitle, headerSubtitle;
|
||||||
|
private Button openLinkButton;
|
||||||
|
private boolean toolbarContentVisible;
|
||||||
|
|
||||||
|
private Menu optionsMenu;
|
||||||
|
private MenuInflater optionsMenuInflater;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean wantsComposeButton() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Activity activity){
|
||||||
|
super.onAttach(activity);
|
||||||
|
trendingLink=Parcels.unwrap(getArguments().getParcelable("trendingLink"));
|
||||||
|
setTitle(trendingLink.title);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetTrendingLinksTimeline(trendingLink.url, getMaxID(), null, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Status> result){
|
||||||
|
if(getActivity()==null) return;
|
||||||
|
boolean more=applyMaxID(result);
|
||||||
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||||
|
onDataLoaded(result, more);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onShown(){
|
||||||
|
super.onShown();
|
||||||
|
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
fab=view.findViewById(R.id.fab);
|
||||||
|
fab.setOnClickListener(this::onFabClick);
|
||||||
|
|
||||||
|
if(getParentFragment() instanceof HomeTabFragment) return;
|
||||||
|
|
||||||
|
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||||
|
View topChild=recyclerView.getChildAt(0);
|
||||||
|
int firstChildPos=recyclerView.getChildAdapterPosition(topChild);
|
||||||
|
float newAlpha=firstChildPos>0 ? 1f : Math.min(1f, -topChild.getTop()/(float)headerTitle.getHeight());
|
||||||
|
toolbarTitleView.setAlpha(newAlpha);
|
||||||
|
boolean newToolbarVisibility=newAlpha>0.5f;
|
||||||
|
if(newToolbarVisibility!=toolbarContentVisible){
|
||||||
|
toolbarContentVisible=newToolbarVisibility;
|
||||||
|
createOptionsMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFabLongClick(View v) {
|
||||||
|
return UiUtils.pickAccountForCompose(getActivity(), accountID, trendingLink.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFabClick(View v){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putString("prefilledText", trendingLink.url);
|
||||||
|
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSetFabBottomInset(int inset){
|
||||||
|
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FilterContext getFilterContext() {
|
||||||
|
return FilterContext.PUBLIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return base.path("/links").appendPath(trendingLink.url).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter getAdapter(){
|
||||||
|
View header=getActivity().getLayoutInflater().inflate(R.layout.header_trending_link_timeline, list, false);
|
||||||
|
headerTitle=header.findViewById(R.id.title);
|
||||||
|
headerSubtitle=header.findViewById(R.id.subtitle);
|
||||||
|
openLinkButton=header.findViewById(R.id.profile_action_btn);
|
||||||
|
|
||||||
|
headerTitle.setText(trendingLink.title);
|
||||||
|
openLinkButton.setVisibility(View.GONE);
|
||||||
|
openLinkButton.setOnClickListener(v->{
|
||||||
|
if(trendingLink==null)
|
||||||
|
return;
|
||||||
|
openLink();
|
||||||
|
});
|
||||||
|
updateHeader();
|
||||||
|
|
||||||
|
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
if(!(getParentFragment() instanceof HomeTabFragment)){
|
||||||
|
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(header));
|
||||||
|
}
|
||||||
|
mergeAdapter.addAdapter(super.getAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getMainAdapterOffset(){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createOptionsMenu(){
|
||||||
|
optionsMenu.clear();
|
||||||
|
optionsMenuInflater.inflate(R.menu.trending_links_timeline, optionsMenu);
|
||||||
|
MenuItem openLinkMenuItem=optionsMenu.findItem(R.id.open_link);
|
||||||
|
openLinkMenuItem.setVisible(toolbarContentVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
inflater.inflate(R.menu.trending_links_timeline, menu);
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
optionsMenu=menu;
|
||||||
|
optionsMenuInflater=inflater;
|
||||||
|
createOptionsMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if (super.onOptionsItemSelected(item)) return true;
|
||||||
|
if (item.getItemId() == R.id.open_link && trendingLink!=null) {
|
||||||
|
openLink();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onUpdateToolbar(){
|
||||||
|
super.onUpdateToolbar();
|
||||||
|
toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f);
|
||||||
|
createOptionsMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHeader(){
|
||||||
|
if(trendingLink==null || getActivity()==null)
|
||||||
|
return;
|
||||||
|
//TODO: update to show mastodon account once fully implemented upstream
|
||||||
|
headerSubtitle.setText(getContext().getString(R.string.article_by_author, TextUtils.isEmpty(trendingLink.authorName)? trendingLink.providerName : trendingLink.authorName));
|
||||||
|
openLinkButton.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openLink() {
|
||||||
|
UiUtils.launchWebBrowser(getActivity(), trendingLink.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ public class FederatedTimelineFragment extends StatusListFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetPublicTimeline(false, false, getMaxID(), count, getLocalPrefs().timelineReplyVisibility)
|
currentRequest=new GetPublicTimeline(false, false, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class LocalTimelineFragment extends StatusListFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetPublicTimeline(true, false, getMaxID(), count, getLocalPrefs().timelineReplyVisibility)
|
currentRequest=new GetPublicTimeline(true, false, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
|||||||
}*/
|
}*/
|
||||||
int offset=_offset;
|
int offset=_offset;
|
||||||
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
|
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<SearchResults>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults result){
|
public void onSuccess(SearchResults result){
|
||||||
ArrayList<SearchResult> results=new ArrayList<>();
|
ArrayList<SearchResult> results=new ArrayList<>();
|
||||||
@@ -164,7 +164,10 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
|||||||
}
|
}
|
||||||
prevDisplayItems=new ArrayList<>(displayItems);
|
prevDisplayItems=new ArrayList<>(displayItems);
|
||||||
unfilteredResults=results;
|
unfilteredResults=results;
|
||||||
|
boolean wasRefreshing=refreshing;
|
||||||
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
|
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
|
||||||
|
if(wasRefreshing)
|
||||||
|
list.scrollToPosition(0);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setTimeout(180000) // 3 minutes (searches can take a long time)
|
.setTimeout(180000) // 3 minutes (searches can take a long time)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.graphics.Outline;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.LayerDrawable;
|
import android.graphics.drawable.LayerDrawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -26,6 +27,7 @@ import android.widget.Toast;
|
|||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
|
import org.joinmastodon.android.MainActivity;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
@@ -434,7 +436,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onOpenURLClick(ListItem<?> item_){
|
private void onOpenURLClick(ListItem<?> item_){
|
||||||
UiUtils.openURL(getContext(), accountID, searchViewHelper.getQuery(), false);
|
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onGoToHashtagClick(ListItem<?> item_){
|
private void onGoToHashtagClick(ListItem<?> item_){
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -13,6 +14,7 @@ import org.joinmastodon.android.fragments.ScrollableToTop;
|
|||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.HashtagChartView;
|
import org.joinmastodon.android.ui.views.HashtagChartView;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
|||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop{
|
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri{
|
||||||
private String accountID;
|
private String accountID;
|
||||||
|
|
||||||
public TrendingHashtagsFragment(){
|
public TrendingHashtagsFragment(){
|
||||||
@@ -65,6 +67,16 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
|||||||
return isRecyclerViewOnTop(list);
|
return isRecyclerViewOnTop(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccountID() {
|
||||||
|
return accountID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
|
||||||
|
}
|
||||||
|
|
||||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
|||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
|||||||
protected void onButtonClick(){
|
protected void onButtonClick(){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putParcelable("instance", Parcels.wrap(instance));
|
args.putParcelable("instance", Parcels.wrap(instance));
|
||||||
|
if(getArguments().containsKey("inviteCode")){
|
||||||
|
args.putString("inviteCode", getArguments().getString("inviteCode"));
|
||||||
|
}
|
||||||
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
|
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -38,6 +37,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
@@ -48,7 +48,6 @@ import me.grishka.appkit.api.Callback;
|
|||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.V;
|
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
@@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
protected EditText searchEdit;
|
protected EditText searchEdit;
|
||||||
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
|
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||||
protected String currentSearchQuery;
|
protected String currentSearchQuery;
|
||||||
|
protected String currentSearchQueryButWithCasePreserved;
|
||||||
protected String loadingInstanceDomain;
|
protected String loadingInstanceDomain;
|
||||||
protected HashMap<String, Instance> instancesCache=new HashMap<>();
|
protected HashMap<String, Instance> instancesCache=new HashMap<>();
|
||||||
protected View buttonBar;
|
protected View buttonBar;
|
||||||
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||||
return true;
|
return true;
|
||||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||||
|
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
|
||||||
updateFilteredList();
|
updateFilteredList();
|
||||||
searchEdit.removeCallbacks(searchDebouncer);
|
searchEdit.removeCallbacks(searchDebouncer);
|
||||||
Instance instance=instancesCache.get(normalizeInstanceDomain(getCurrentSearchQuery()));
|
Instance instance=instancesCache.get(normalizeInstanceDomain(getCurrentSearchQuery()));
|
||||||
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
|
|
||||||
protected void onSearchChangedDebounced(){
|
protected void onSearchChangedDebounced(){
|
||||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||||
|
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
|
||||||
updateFilteredList();
|
updateFilteredList();
|
||||||
loadInstanceInfo(getCurrentSearchQuery(), false);
|
loadInstanceInfo(getCurrentSearchQuery(), false);
|
||||||
}
|
}
|
||||||
@@ -156,6 +158,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
|
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
|
||||||
|
loadInstanceInfo(_domain, isFromRedirect, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
|
||||||
if(TextUtils.isEmpty(_domain))
|
if(TextUtils.isEmpty(_domain))
|
||||||
return;
|
return;
|
||||||
String domain=normalizeInstanceDomain(_domain);
|
String domain=normalizeInstanceDomain(_domain);
|
||||||
@@ -180,7 +186,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
try{
|
try{
|
||||||
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
|
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
|
||||||
}catch(URISyntaxException x){
|
}catch(URISyntaxException x){
|
||||||
showInstanceInfoLoadError(domain, x);
|
if(onError!=null)
|
||||||
|
onError.accept(x);
|
||||||
|
else
|
||||||
|
showInstanceInfoLoadError(domain, x);
|
||||||
if(fakeInstance!=null){
|
if(fakeInstance!=null){
|
||||||
fakeInstance.description=getString(R.string.error);
|
fakeInstance.description=getString(R.string.error);
|
||||||
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||||
@@ -200,10 +209,11 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
loadingInstanceDomain=null;
|
loadingInstanceDomain=null;
|
||||||
result.uri=domain; // needed for instances that use domain redirection
|
result.uri=domain; // needed for instances that use domain redirection
|
||||||
instancesCache.put(domain, result);
|
instancesCache.put(domain, result);
|
||||||
|
if(instanceProgressDialog!=null || onError!=null)
|
||||||
|
proceedWithAuthOrSignup(result);
|
||||||
if(instanceProgressDialog!=null){
|
if(instanceProgressDialog!=null){
|
||||||
instanceProgressDialog.dismiss();
|
instanceProgressDialog.dismiss();
|
||||||
instanceProgressDialog=null;
|
instanceProgressDialog=null;
|
||||||
proceedWithAuthOrSignup(result);
|
|
||||||
}
|
}
|
||||||
if(Objects.equals(domain, getCurrentSearchQuery()) || Objects.equals(getCurrentSearchQuery(), redirects.get(domain)) || Objects.equals(getCurrentSearchQuery(), redirectsInverse.get(domain))){
|
if(Objects.equals(domain, getCurrentSearchQuery()) || Objects.equals(getCurrentSearchQuery(), redirects.get(domain)) || Objects.equals(getCurrentSearchQuery(), redirectsInverse.get(domain))){
|
||||||
boolean found=false;
|
boolean found=false;
|
||||||
@@ -230,11 +240,14 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
loadingInstanceRequest=null;
|
loadingInstanceRequest=null;
|
||||||
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
|
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
|
||||||
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
|
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadingInstanceDomain=null;
|
loadingInstanceDomain=null;
|
||||||
showInstanceInfoLoadError(domain, error);
|
if(onError!=null)
|
||||||
|
onError.accept(error);
|
||||||
|
else
|
||||||
|
showInstanceInfoLoadError(domain, error);
|
||||||
if(fakeInstance!=null && getActivity()!=null){
|
if(fakeInstance!=null && getActivity()!=null){
|
||||||
fakeInstance.description=getString(R.string.error);
|
fakeInstance.description=getString(R.string.error);
|
||||||
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||||
@@ -283,7 +296,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
|
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){
|
||||||
String url="https://"+domain+"/.well-known/host-meta";
|
String url="https://"+domain+"/.well-known/host-meta";
|
||||||
Request req=new Request.Builder()
|
Request req=new Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
@@ -297,7 +310,12 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
Activity a=getActivity();
|
Activity a=getActivity();
|
||||||
if(a==null)
|
if(a==null)
|
||||||
return;
|
return;
|
||||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
|
a.runOnUiThread(()->{
|
||||||
|
if(onError!=null)
|
||||||
|
onError.accept(e);
|
||||||
|
else
|
||||||
|
showInstanceInfoLoadError(domain, e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -309,7 +327,13 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
return;
|
return;
|
||||||
try(response){
|
try(response){
|
||||||
if(!response.isSuccessful()){
|
if(!response.isSuccessful()){
|
||||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
|
a.runOnUiThread(()->{
|
||||||
|
String err=response.code()+" "+response.message();
|
||||||
|
if(onError!=null)
|
||||||
|
onError.accept(err);
|
||||||
|
else
|
||||||
|
showInstanceInfoLoadError(domain, err);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
InputSource source=new InputSource(response.body().charStream());
|
InputSource source=new InputSource(response.body().charStream());
|
||||||
@@ -328,9 +352,19 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
|
a.runOnUiThread(()->{
|
||||||
|
if(onError!=null)
|
||||||
|
onError.accept(origError);
|
||||||
|
else
|
||||||
|
showInstanceInfoLoadError(domain, origError);
|
||||||
|
});
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
|
a.runOnUiThread(()->{
|
||||||
|
if(onError!=null)
|
||||||
|
onError.accept(x);
|
||||||
|
else
|
||||||
|
showInstanceInfoLoadError(domain, x);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package org.joinmastodon.android.fragments.onboarding;
|
package org.joinmastodon.android.fragments.onboarding;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -12,6 +17,8 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
import android.widget.HorizontalScrollView;
|
import android.widget.HorizontalScrollView;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -19,9 +26,12 @@ import android.widget.PopupMenu;
|
|||||||
import android.widget.RadioButton;
|
import android.widget.RadioButton;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
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.GetCatalogCategories;
|
||||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
@@ -29,6 +39,8 @@ import org.joinmastodon.android.model.catalog.CatalogCategory;
|
|||||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
|
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||||
@@ -40,7 +52,9 @@ import java.util.Collections;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
|||||||
private CatalogInstance.Region chosenRegion;
|
private CatalogInstance.Region chosenRegion;
|
||||||
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
|
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
|
||||||
|
|
||||||
|
private String inviteCode, inviteCodeHost;
|
||||||
|
private AlertDialog currentInviteLinkAlert;
|
||||||
|
|
||||||
public InstanceCatalogSignupFragment(){
|
public InstanceCatalogSignupFragment(){
|
||||||
super(R.layout.fragment_onboarding_common, 10);
|
super(R.layout.fragment_onboarding_common, 10);
|
||||||
}
|
}
|
||||||
@@ -317,7 +334,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
|||||||
focusThing=view.findViewById(R.id.focus_thing);
|
focusThing=view.findViewById(R.id.focus_thing);
|
||||||
focusThing.requestFocus();
|
focusThing.requestFocus();
|
||||||
|
|
||||||
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
|
view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick);
|
||||||
nextButton.setEnabled(chosenInstance!=null);
|
nextButton.setEnabled(chosenInstance!=null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,91 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void proceedWithAuthOrSignup(Instance instance){
|
protected void proceedWithAuthOrSignup(Instance instance){
|
||||||
|
if(currentInviteLinkAlert!=null){
|
||||||
|
currentInviteLinkAlert.dismiss();
|
||||||
|
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
|
||||||
|
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
|
||||||
|
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
|
||||||
|
new CheckInviteLink(inviteLink.getPath())
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(CheckInviteLink.Response result){
|
||||||
|
inviteCodeHost=inviteLink.getHost();
|
||||||
|
inviteCode=result.inviteCode;
|
||||||
|
proceedWithAuthOrSignup(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
if(getActivity()==null)
|
||||||
|
return;
|
||||||
|
if(error instanceof MastodonErrorResponse mer){
|
||||||
|
switch(mer.httpStatus){
|
||||||
|
case 401 -> new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.expired_invite_link)
|
||||||
|
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
case 404 -> new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.invalid_invite_link)
|
||||||
|
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
default -> error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading_instance, true)
|
||||||
|
.execNoAuth(inviteLink.getHost());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
if(!instance.registrations){
|
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
|
||||||
new M3AlertDialogBuilder(getActivity())
|
if(instance.invitesEnabled){
|
||||||
.setTitle(R.string.error)
|
showInviteLinkAlert(instance.uri);
|
||||||
.setMessage(R.string.instance_signup_closed)
|
}else{
|
||||||
.setPositiveButton(R.string.ok, null)
|
new M3AlertDialogBuilder(getActivity())
|
||||||
.show();
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(R.string.instance_signup_closed)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putParcelable("instance", Parcels.wrap(instance));
|
args.putParcelable("instance", Parcels.wrap(instance));
|
||||||
|
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
|
||||||
|
args.putString("inviteCode", inviteCode);
|
||||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPickRandomInstanceClick(View v){
|
private void onUseInviteClick(View v){
|
||||||
String lang=Locale.getDefault().getLanguage();
|
showInviteLinkAlert(null);
|
||||||
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
|
|
||||||
if(instances.isEmpty()){
|
|
||||||
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
if(instances.isEmpty()){
|
|
||||||
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
if(instances.isEmpty()){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chosenInstance=instances.get(new Random().nextInt(instances.size()));
|
|
||||||
onNextClick(v);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private String getEmojiForCategory(String category){
|
private void showInviteLinkAlert(String domain){
|
||||||
// return switch(category){
|
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||||
// case "all" -> "💬";
|
.setView(R.layout.alert_invite_link)
|
||||||
// case "academia" -> "📚";
|
.setPositiveButton(R.string.next, null)
|
||||||
// case "activism" -> "✊";
|
.setNegativeButton(R.string.cancel, null)
|
||||||
// case "food" -> "🍕";
|
.create();
|
||||||
// case "furry" -> "🦁";
|
|
||||||
// case "games" -> "🕹";
|
|
||||||
// case "general" -> "🐘";
|
|
||||||
// case "journalism" -> "📰";
|
|
||||||
// case "lgbt" -> "🏳️🌈";
|
|
||||||
// case "regional" -> "📍";
|
|
||||||
// case "art" -> "🎨";
|
|
||||||
// case "music" -> "🎼";
|
|
||||||
// case "tech" -> "📱";
|
|
||||||
// default -> "❓";
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
private int getEmojiForCategory(String category){
|
Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
return switch(category){
|
EditText edit=alert.findViewById(R.id.edit);
|
||||||
case "all" -> R.drawable.ic_category_all;
|
TextView supportingText=alert.findViewById(R.id.supporting_text);
|
||||||
case "academia" -> R.drawable.ic_category_academia;
|
TextView label=alert.findViewById(R.id.label);
|
||||||
case "activism" -> R.drawable.ic_category_activism;
|
TextView subtitle=alert.findViewById(R.id.subtitle);
|
||||||
case "food" -> R.drawable.ic_category_food;
|
ImageButton clear=alert.findViewById(R.id.clear);
|
||||||
case "furry" -> R.drawable.ic_category_furry;
|
clear.setVisibility(View.GONE);
|
||||||
case "games" -> R.drawable.ic_category_games;
|
|
||||||
case "general" -> R.drawable.ic_category_general;
|
|
||||||
case "journalism" -> R.drawable.ic_category_journalism;
|
|
||||||
case "lgbt" -> R.drawable.ic_category_lgbt;
|
|
||||||
case "regional" -> R.drawable.ic_category_regional;
|
|
||||||
case "art" -> R.drawable.ic_category_art;
|
|
||||||
case "music" -> R.drawable.ic_category_music;
|
|
||||||
case "tech" -> R.drawable.ic_category_tech;
|
|
||||||
default -> R.drawable.ic_category_unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getTitleForCategory(String category){
|
if(TextUtils.isEmpty(domain)){
|
||||||
return switch(category){
|
subtitle.setVisibility(View.GONE);
|
||||||
case "all" -> R.string.category_all;
|
}else{
|
||||||
case "academia" -> R.string.category_academia;
|
subtitle.setText(getString(R.string.need_invite_to_join_server, domain));
|
||||||
case "activism" -> R.string.category_activism;
|
}
|
||||||
case "food" -> R.string.category_food;
|
|
||||||
case "furry" -> R.string.category_furry;
|
Consumer<String> errorSetter=err->{
|
||||||
case "games" -> R.string.category_games;
|
supportingText.setText(err);
|
||||||
case "general" -> R.string.category_general;
|
int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
|
||||||
case "journalism" -> R.string.category_journalism;
|
supportingText.setTextColor(errorColor);
|
||||||
case "lgbt" -> R.string.category_lgbt;
|
label.setTextColor(errorColor);
|
||||||
case "regional" -> R.string.category_regional;
|
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error);
|
||||||
case "art" -> R.string.category_art;
|
|
||||||
case "music" -> R.string.category_music;
|
|
||||||
case "tech" -> R.string.category_tech;
|
|
||||||
default -> 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
next.setOnClickListener(_v->{
|
||||||
|
Uri inviteLink=Uri.parse(edit.getText().toString());
|
||||||
|
if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){
|
||||||
|
errorSetter.accept(getString(R.string.this_invite_is_invalid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UiUtils.showProgressForAlertButton(next, true);
|
||||||
|
new CheckInviteLink(inviteLink.getPath())
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(CheckInviteLink.Response result){
|
||||||
|
if(getActivity()==null || !alert.isShowing())
|
||||||
|
return;
|
||||||
|
|
||||||
|
String host=inviteLink.getHost();
|
||||||
|
inviteCode=result.inviteCode;
|
||||||
|
inviteCodeHost=host;
|
||||||
|
|
||||||
|
Instance instance=instancesCache.get(normalizeInstanceDomain(host));
|
||||||
|
if(instance==null){
|
||||||
|
loadInstanceInfo(host, false, err->{
|
||||||
|
String errorStr;
|
||||||
|
if(err instanceof String str){
|
||||||
|
errorStr=str;
|
||||||
|
}else if(err instanceof Throwable x){
|
||||||
|
errorStr=x.getMessage();
|
||||||
|
}else if(err instanceof MastodonErrorResponse mer){
|
||||||
|
errorStr=mer.error;
|
||||||
|
}else{
|
||||||
|
errorStr=getString(R.string.error);
|
||||||
|
}
|
||||||
|
errorSetter.accept(errorStr);
|
||||||
|
UiUtils.showProgressForAlertButton(next, false);
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
proceedWithAuthOrSignup(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
if(getActivity()==null || !alert.isShowing())
|
||||||
|
return;
|
||||||
|
UiUtils.showProgressForAlertButton(next, false);
|
||||||
|
if(error instanceof MastodonErrorResponse mer){
|
||||||
|
errorSetter.accept(switch(mer.httpStatus){
|
||||||
|
case 404 -> getString(R.string.this_invite_is_invalid);
|
||||||
|
case 401 -> getString(R.string.this_invite_has_expired);
|
||||||
|
default -> mer.error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execNoAuth(inviteLink.getHost());
|
||||||
|
});
|
||||||
|
next.setEnabled(false);
|
||||||
|
edit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||||
|
boolean wasEmpty=!next.isEnabled();
|
||||||
|
next.setEnabled(e.length()>0);
|
||||||
|
if(supportingText.length()>0){
|
||||||
|
supportingText.setText("");
|
||||||
|
int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant);
|
||||||
|
supportingText.setTextColor(regularColor);
|
||||||
|
label.setTextColor(regularColor);
|
||||||
|
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field);
|
||||||
|
}
|
||||||
|
if(wasEmpty!=(e.length()==0)){
|
||||||
|
int padEnd;
|
||||||
|
if(e.length()==0){
|
||||||
|
clear.setVisibility(View.GONE);
|
||||||
|
padEnd=V.dp(16);
|
||||||
|
}else{
|
||||||
|
clear.setVisibility(View.VISIBLE);
|
||||||
|
padEnd=V.dp(48);
|
||||||
|
}
|
||||||
|
edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
clear.setOnClickListener(_v->edit.setText(""));
|
||||||
|
|
||||||
|
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
|
||||||
|
if(clipData!=null && clipData.getItemCount()>0){
|
||||||
|
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
|
||||||
|
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
|
||||||
|
edit.setText(clipText);
|
||||||
|
supportingText.setText(R.string.invite_link_pasted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentInviteLinkAlert=alert;
|
||||||
|
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
|
||||||
|
alert.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -444,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
|||||||
filteredData.clear();
|
filteredData.clear();
|
||||||
if(searchQueryMode){
|
if(searchQueryMode){
|
||||||
if(!TextUtils.isEmpty(currentSearchQuery)){
|
if(!TextUtils.isEmpty(currentSearchQuery)){
|
||||||
|
String actualQuery;
|
||||||
|
if(currentSearchQuery.startsWith("https:")){
|
||||||
|
actualQuery=Uri.parse(currentSearchQuery).getHost();
|
||||||
|
}else{
|
||||||
|
actualQuery=currentSearchQuery;
|
||||||
|
}
|
||||||
for(CatalogInstance instance:data){
|
for(CatalogInstance instance:data){
|
||||||
if(instance.domain.contains(currentSearchQuery)){
|
if(instance.domain.contains(actualQuery)){
|
||||||
filteredData.add(instance);
|
filteredData.add(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAs
|
|||||||
protected void onButtonClick(){
|
protected void onButtonClick(){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putParcelable("instance", Parcels.wrap(instance));
|
args.putParcelable("instance", Parcels.wrap(instance));
|
||||||
|
if(getArguments().containsKey("inviteCode")){
|
||||||
|
args.putString("inviteCode", getArguments().getString("inviteCode"));
|
||||||
|
}
|
||||||
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
|
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||||
@@ -13,35 +14,38 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
|||||||
import org.joinmastodon.android.model.FollowSuggestion;
|
import org.joinmastodon.android.model.FollowSuggestion;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
|
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private View buttonBar;
|
private View buttonBar;
|
||||||
private ElevationOnScrollListener onScrollListener;
|
|
||||||
private int numRunningFollowRequests=0;
|
private int numRunningFollowRequests=0;
|
||||||
|
|
||||||
public OnboardingFollowSuggestionsFragment(){
|
public OnboardingFollowSuggestionsFragment(){
|
||||||
super(R.layout.fragment_onboarding_follow_suggestions, 40);
|
super(R.layout.fragment_onboarding_follow_suggestions, 40);
|
||||||
|
itemLayoutRes=R.layout.item_account_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
setTitle(R.string.popular_on_mastodon);
|
setTitle(R.string.onboarding_recommendations_title);
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
@@ -50,7 +54,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
|||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
buttonBar=view.findViewById(R.id.button_bar);
|
buttonBar=view.findViewById(R.id.button_bar);
|
||||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
|
||||||
|
|
||||||
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
|
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
|
||||||
// view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
|
// view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
|
||||||
@@ -59,9 +62,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
|||||||
@Override
|
@Override
|
||||||
protected void onUpdateToolbar(){
|
protected void onUpdateToolbar(){
|
||||||
super.onUpdateToolbar();
|
super.onUpdateToolbar();
|
||||||
if(onScrollListener!=null){
|
getToolbar().setContentInsetsRelative(V.dp(56), 0);
|
||||||
onScrollListener.setViews(buttonBar, getToolbar());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -70,7 +71,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
|||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<FollowSuggestion> result){
|
public void onSuccess(List<FollowSuggestion> result){
|
||||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
|
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
@@ -81,6 +82,20 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
|||||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter<?> getAdapter(){
|
||||||
|
// Unused in Moshidon
|
||||||
|
// TextView introText=new TextView(getActivity());
|
||||||
|
// introText.setTextAppearance(R.style.m3_body_large);
|
||||||
|
// introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||||
|
// introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
|
||||||
|
// introText.setText(R.string.onboarding_recommendations_intro);
|
||||||
|
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
// mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
|
||||||
|
mergeAdapter.addAdapter(super.getAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
private void onFollowAllClick(View v){
|
private void onFollowAllClick(View v){
|
||||||
if(!loaded || relationships.isEmpty())
|
if(!loaded || relationships.isEmpty())
|
||||||
return;
|
return;
|
||||||
@@ -156,6 +171,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
|||||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||||
super.onConfigureViewHolder(holder);
|
super.onConfigureViewHolder(holder);
|
||||||
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
|
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
|
||||||
|
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.view.WindowInsets;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -19,12 +20,17 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
|||||||
import org.joinmastodon.android.fragments.HomeFragment;
|
import org.joinmastodon.android.fragments.HomeFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.AccountField;
|
import org.joinmastodon.android.model.AccountField;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
|
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
@@ -35,7 +41,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
|
|
||||||
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
|
public class OnboardingProfileSetupFragment extends ToolbarFragment{
|
||||||
private Button btn;
|
private Button btn;
|
||||||
private View buttonBar;
|
private View buttonBar;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
@@ -43,9 +49,9 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
|||||||
private ScrollView scroller;
|
private ScrollView scroller;
|
||||||
private EditText nameEdit, bioEdit;
|
private EditText nameEdit, bioEdit;
|
||||||
private ImageView avaImage, coverImage;
|
private ImageView avaImage, coverImage;
|
||||||
private Button addRow;
|
|
||||||
private ReorderableLinearLayout profileFieldsLayout;
|
|
||||||
private Uri avatarUri, coverUri;
|
private Uri avatarUri, coverUri;
|
||||||
|
private LinearLayout scrollContent;
|
||||||
|
private CheckableListItem<Void> discoverableItem;
|
||||||
|
|
||||||
private static final int AVATAR_RESULT=348;
|
private static final int AVATAR_RESULT=348;
|
||||||
private static final int COVER_RESULT=183;
|
private static final int COVER_RESULT=183;
|
||||||
@@ -73,8 +79,6 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
|||||||
bioEdit=view.findViewById(R.id.bio);
|
bioEdit=view.findViewById(R.id.bio);
|
||||||
avaImage=view.findViewById(R.id.avatar);
|
avaImage=view.findViewById(R.id.avatar);
|
||||||
coverImage=view.findViewById(R.id.header);
|
coverImage=view.findViewById(R.id.header);
|
||||||
addRow=view.findViewById(R.id.add_row);
|
|
||||||
profileFieldsLayout=view.findViewById(R.id.profile_fields);
|
|
||||||
|
|
||||||
btn=view.findViewById(R.id.btn_next);
|
btn=view.findViewById(R.id.btn_next);
|
||||||
btn.setOnClickListener(v->onButtonClick());
|
btn.setOnClickListener(v->onButtonClick());
|
||||||
@@ -86,31 +90,20 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
|||||||
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
|
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
nameEdit.setText(account.displayName);
|
nameEdit.setText(account.displayName);
|
||||||
makeFieldsRow();
|
|
||||||
}else{
|
|
||||||
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
|
|
||||||
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
|
|
||||||
for(int i=0;i<fieldTitles.size();i++){
|
|
||||||
View row=makeFieldsRow();
|
|
||||||
EditText title=row.findViewById(R.id.title);
|
|
||||||
EditText content=row.findViewById(R.id.content);
|
|
||||||
title.setText(fieldTitles.get(i));
|
|
||||||
content.setText(fieldValues.get(i));
|
|
||||||
}
|
|
||||||
if(fieldTitles.size()==4)
|
|
||||||
addRow.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addRow.setOnClickListener(v->{
|
|
||||||
makeFieldsRow();
|
|
||||||
if(profileFieldsLayout.getChildCount()==4){
|
|
||||||
addRow.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
profileFieldsLayout.setDragListener(this);
|
|
||||||
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
|
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
|
||||||
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
|
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
|
||||||
|
|
||||||
|
scrollContent=view.findViewById(R.id.scrollable_content);
|
||||||
|
discoverableItem=new CheckableListItem<>(R.string.make_profile_discoverable, 0, CheckableListItem.Style.SWITCH_SEPARATED, true, R.drawable.ic_campaign_24px, item->showDiscoverabilityAlert());
|
||||||
|
GenericListItemsAdapter<Void> fakeAdapter=new GenericListItemsAdapter<>(List.of(discoverableItem));
|
||||||
|
ListItemViewHolder<?> holder=fakeAdapter.onCreateViewHolder(scrollContent, fakeAdapter.getItemViewType(0));
|
||||||
|
fakeAdapter.bindViewHolder(holder, 0);
|
||||||
|
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||||
|
holder.itemView.setOnClickListener(v->holder.onClick());
|
||||||
|
scrollContent.addView(holder.itemView);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,17 +122,8 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onButtonClick(){
|
protected void onButtonClick(){
|
||||||
ArrayList<AccountField> fields=new ArrayList<>();
|
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, null)
|
||||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
.setDiscoverableIndexable(discoverableItem.checked, discoverableItem.checked)
|
||||||
View row=profileFieldsLayout.getChildAt(i);
|
|
||||||
EditText title=row.findViewById(R.id.title);
|
|
||||||
EditText content=row.findViewById(R.id.content);
|
|
||||||
AccountField fld=new AccountField();
|
|
||||||
fld.name=title.getText().toString();
|
|
||||||
fld.value=content.getText().toString();
|
|
||||||
fields.add(fld);
|
|
||||||
}
|
|
||||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
|
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Account result){
|
public void onSuccess(Account result){
|
||||||
@@ -163,39 +147,6 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
|||||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||||
}
|
}
|
||||||
|
|
||||||
private View makeFieldsRow(){
|
|
||||||
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
|
|
||||||
profileFieldsLayout.addView(view);
|
|
||||||
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
|
||||||
profileFieldsLayout.startDragging(view);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
view.findViewById(R.id.delete).setOnClickListener(v->{
|
|
||||||
profileFieldsLayout.removeView(view);
|
|
||||||
if(addRow.getVisibility()==View.GONE)
|
|
||||||
addRow.setVisibility(View.VISIBLE);
|
|
||||||
});
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwapItems(int oldIndex, int newIndex){}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState){
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
|
|
||||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
|
||||||
View row=profileFieldsLayout.getChildAt(i);
|
|
||||||
EditText title=row.findViewById(R.id.title);
|
|
||||||
EditText content=row.findViewById(R.id.content);
|
|
||||||
fieldTitles.add(title.getText().toString());
|
|
||||||
fieldValues.add(content.getText().toString());
|
|
||||||
}
|
|
||||||
outState.putStringArrayList("fieldTitles", fieldTitles);
|
|
||||||
outState.putStringArrayList("fieldValues", fieldValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||||
if(resultCode!=Activity.RESULT_OK)
|
if(resultCode!=Activity.RESULT_OK)
|
||||||
@@ -215,4 +166,12 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
|||||||
img.setForeground(null);
|
img.setForeground(null);
|
||||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
|
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showDiscoverabilityAlert(){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.discoverability)
|
||||||
|
.setMessage(R.string.discoverability_help)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
package org.joinmastodon.android.fragments.onboarding;
|
package org.joinmastodon.android.fragments.onboarding;
|
||||||
|
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.LocaleList;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.Html;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.text.style.TypefaceSpan;
|
import android.text.style.TypefaceSpan;
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -46,10 +43,13 @@ import org.jsoup.select.NodeVisitor;
|
|||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -58,7 +58,6 @@ import me.grishka.appkit.api.APIRequest;
|
|||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||||
import me.grishka.appkit.utils.V;
|
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
|
|
||||||
public class SignupFragment extends ToolbarFragment{
|
public class SignupFragment extends ToolbarFragment{
|
||||||
@@ -79,6 +78,7 @@ public class SignupFragment extends ToolbarFragment{
|
|||||||
private ProgressDialog progressDialog;
|
private ProgressDialog progressDialog;
|
||||||
private HashSet<EditText> errorFields=new HashSet<>();
|
private HashSet<EditText> errorFields=new HashSet<>();
|
||||||
private ElevationOnScrollListener onScrollListener;
|
private ElevationOnScrollListener onScrollListener;
|
||||||
|
private Set<String> serverSupportedTimezones, serverSupportedLocales;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -87,6 +87,8 @@ public class SignupFragment extends ToolbarFragment{
|
|||||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||||
createAppAndGetToken();
|
createAppAndGetToken();
|
||||||
setTitle(R.string.signup_title);
|
setTitle(R.string.signup_title);
|
||||||
|
serverSupportedTimezones=Arrays.stream(getResources().getStringArray(R.array.server_supported_timezones)).collect(Collectors.toSet());
|
||||||
|
serverSupportedLocales=Arrays.stream(getResources().getStringArray(R.array.server_supported_locales)).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -190,7 +192,36 @@ public class SignupFragment extends ToolbarFragment{
|
|||||||
edit.setError(null);
|
edit.setError(null);
|
||||||
}
|
}
|
||||||
errorFields.clear();
|
errorFields.clear();
|
||||||
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString(), ZoneId.systemDefault().getId())
|
String locale=null;
|
||||||
|
String timezone=ZoneId.systemDefault().getId();
|
||||||
|
|
||||||
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||||
|
LocaleList localeList=getResources().getConfiguration().getLocales();
|
||||||
|
for(int i=0;i<localeList.size();i++){
|
||||||
|
Locale l=localeList.get(i);
|
||||||
|
if(serverSupportedLocales.contains(l.toLanguageTag())){
|
||||||
|
locale=l.toLanguageTag();
|
||||||
|
break;
|
||||||
|
}else if(serverSupportedLocales.contains(l.getLanguage())){
|
||||||
|
locale=l.getLanguage();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
Locale l=getResources().getConfiguration().locale;
|
||||||
|
if(serverSupportedLocales.contains(l.toLanguageTag())){
|
||||||
|
locale=l.toLanguageTag();
|
||||||
|
}else if(serverSupportedLocales.contains(l.getLanguage())){
|
||||||
|
locale=l.getLanguage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!serverSupportedTimezones.contains(timezone))
|
||||||
|
timezone=null;
|
||||||
|
|
||||||
|
String inviteCode=getArguments().getString("inviteCode");
|
||||||
|
|
||||||
|
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Token result){
|
public void onSuccess(Token result){
|
||||||
@@ -271,7 +302,7 @@ public class SignupFragment extends ToolbarFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void tail(Node node, int depth){
|
public void tail(Node node, int depth){
|
||||||
if(node instanceof Element){
|
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 LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user