Compare commits
1473 Commits
2.1.4+fork
...
2.3.0+fork
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d87d656002 | ||
|
|
9e9061e29c | ||
|
|
70d0ba88e4 | ||
|
|
9cb48e2f25 | ||
|
|
6f89dd7331 | ||
|
|
a59c20d239 | ||
|
|
3e36a72852 | ||
|
|
7801d28a23 | ||
|
|
c2e6c802a1 | ||
|
|
2dbfe88397 | ||
|
|
60f0a3d5ee | ||
|
|
86506f9ba4 | ||
|
|
7269788831 | ||
|
|
f7d0bda90f | ||
|
|
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 | ||
|
|
aab829ac4d | ||
|
|
d047379785 | ||
|
|
ff9587661e | ||
|
|
e6a4f81b78 | ||
|
|
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 | ||
|
|
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 | ||
|
|
6915d19fb4 | ||
|
|
ad2ef39ace | ||
|
|
3cff655e6f | ||
|
|
ed86a5a3e8 | ||
|
|
f329435f51 | ||
|
|
c8604ad68e | ||
|
|
6a6a80bcd7 | ||
|
|
62e4983f02 | ||
|
|
6dfd991e87 | ||
|
|
e205462bf4 | ||
|
|
03f341f6f8 | ||
|
|
b9b08c5ea7 | ||
|
|
2b5498ff5d | ||
|
|
84b058873d | ||
|
|
fcf5c0822e | ||
|
|
53c3da6a3d | ||
|
|
68371c9a0f | ||
|
|
e7295aac07 | ||
|
|
ae7f65954a | ||
|
|
350a73c3eb | ||
|
|
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 | ||
|
|
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 | ||
|
|
48f9aabaf7 | ||
|
|
14d353ae27 | ||
|
|
9a82846b84 | ||
|
|
a4c9bbadc4 | ||
|
|
fa70c55084 | ||
|
|
8d0a89fb06 | ||
|
|
3caf6cb94c | ||
|
|
9f29d72212 | ||
|
|
f4854061ea | ||
|
|
bf7607674e | ||
|
|
9786e324b7 | ||
|
|
a7fef67d48 | ||
|
|
30726cb364 | ||
|
|
e0ff1f6725 | ||
|
|
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 | ||
|
|
522763b33a | ||
|
|
3cae5cbadf | ||
|
|
ba7b2c8067 | ||
|
|
14901f5fac | ||
|
|
28bbf7c7be | ||
|
|
7315c6cd5c | ||
|
|
dda473f377 | ||
|
|
3b5e148ca9 | ||
|
|
f019497044 | ||
|
|
f1cdce38c4 | ||
|
|
4a329fc4c2 | ||
|
|
7b440d37b4 | ||
|
|
1cd8f5688d | ||
|
|
d02d285c1f | ||
|
|
011a49b180 | ||
|
|
59e4b13d82 | ||
|
|
72bfa12dec | ||
|
|
dbf0c1047f | ||
|
|
fb38b3563c | ||
|
|
601cfeffe1 | ||
|
|
e2dfef22a9 | ||
|
|
3293c95b84 | ||
|
|
33b9de5e5d | ||
|
|
2b5a9171dd | ||
|
|
2de2efd4be | ||
|
|
43dd8931ff | ||
|
|
f2c6dda26b | ||
|
|
ce7e7f63bb | ||
|
|
f0fe1f6cf9 | ||
|
|
a7350d82d6 | ||
|
|
36dede1f93 | ||
|
|
ed15daf9e9 | ||
|
|
c6052c841d | ||
|
|
3bf63e1090 | ||
|
|
3d48443a23 | ||
|
|
ce39c7ca8f | ||
|
|
b7723dcb98 | ||
|
|
ad0774f8a5 | ||
|
|
9172feb72b | ||
|
|
a297bd3281 | ||
|
|
e713a9cfc3 | ||
|
|
195395a22d | ||
|
|
7b6a62b047 | ||
|
|
ada1c9ff6d | ||
|
|
5a0a14ed56 | ||
|
|
cad3879646 | ||
|
|
46115e8f3b | ||
|
|
5d961991d4 | ||
|
|
e27536743f | ||
|
|
9f8d4a0f34 | ||
|
|
67b6a89fd9 | ||
|
|
a27ef27f8d | ||
|
|
b42862dd9b | ||
|
|
533032ca75 | ||
|
|
dabc4058ba | ||
|
|
6c468602c6 | ||
|
|
9c5d29a860 | ||
|
|
da5e2a6b50 | ||
|
|
a194569fd4 | ||
|
|
78a4ace9b2 | ||
|
|
9a664088cd | ||
|
|
1d2e6f880b | ||
|
|
2cd2918d53 | ||
|
|
9b49db6677 | ||
|
|
9f6c61e5c0 | ||
|
|
b6a2bb7881 | ||
|
|
62262010b9 | ||
|
|
72fe9a04a6 | ||
|
|
d8cf55ae21 | ||
|
|
dfb393b934 | ||
|
|
cd27716f6a | ||
|
|
469553b34e | ||
|
|
5d7c37262e | ||
|
|
3f3867473f | ||
|
|
b08cd1eb4b | ||
|
|
1f9ff8d341 | ||
|
|
528b362f64 | ||
|
|
1db10c5047 | ||
|
|
f295f5f4e7 | ||
|
|
08924bd9b0 | ||
|
|
5d432435a1 | ||
|
|
8bd76aa833 | ||
|
|
2147cb87ac | ||
|
|
00ed0f5402 | ||
|
|
870f79f6cd | ||
|
|
da879213fc | ||
|
|
db66974bd6 | ||
|
|
e3d5ae1d65 | ||
|
|
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 | ||
|
|
69a2fc2d9f | ||
|
|
2ef3e7eab7 | ||
|
|
58f746a285 | ||
|
|
a6bba42a49 | ||
|
|
519d6868b2 | ||
|
|
5322120097 | ||
|
|
2c88c86480 | ||
|
|
55f32fd45b | ||
|
|
f39f0b03d1 | ||
|
|
f7dfebcbea | ||
|
|
d8c73ea9af | ||
|
|
ff2f1a4955 | ||
|
|
79d5067c97 | ||
|
|
ed986b11af | ||
|
|
6c94eb50ea | ||
|
|
70b9d08830 | ||
|
|
b283e216a7 | ||
|
|
4328d568b3 | ||
|
|
8edc47703f | ||
|
|
92ce906163 | ||
|
|
6e141e360e | ||
|
|
d1aba87e13 | ||
|
|
723853079e | ||
|
|
cd0742c093 | ||
|
|
52d5de5aec | ||
|
|
4f8a5ae5db | ||
|
|
616f2463c7 | ||
|
|
d3b711a966 | ||
|
|
827fe34709 | ||
|
|
4b6c0242d5 | ||
|
|
c3cbc16084 | ||
|
|
36493bfc88 | ||
|
|
66ce93a3ff | ||
|
|
957bc76dbb | ||
|
|
1f5a28fb33 | ||
|
|
40711fbd28 | ||
|
|
045c58ce66 | ||
|
|
e2dde7239f | ||
|
|
512ad93eea | ||
|
|
19759023a4 | ||
|
|
714d3399ce | ||
|
|
e1850e5282 | ||
|
|
a05c917b2c | ||
|
|
8f3a9c265c | ||
|
|
6f1a33b76e | ||
|
|
8f0451175f | ||
|
|
37a3a4f1c0 | ||
|
|
bd85746726 | ||
|
|
96265010bf | ||
|
|
4209951ce3 | ||
|
|
f1cbd95439 | ||
|
|
d63382c6d9 | ||
|
|
20697fb334 | ||
|
|
1090d1ca42 | ||
|
|
1525ef8bdf | ||
|
|
f5e1326d9f | ||
|
|
43fe03c943 | ||
|
|
1ee96f3703 | ||
|
|
da5186c7b5 | ||
|
|
bec4acdf51 | ||
|
|
800b78bfd8 | ||
|
|
52b01b7bbe | ||
|
|
8b71764207 | ||
|
|
a5d7a75f32 | ||
|
|
8839bcb7aa | ||
|
|
bcaf71760d | ||
|
|
1d95204648 | ||
|
|
83bf2a808f | ||
|
|
dca68de77e | ||
|
|
0ef879ebd3 | ||
|
|
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 | ||
|
|
d96b9558b4 | ||
|
|
1ed3f00f82 | ||
|
|
327aef0271 | ||
|
|
5274ecb721 | ||
|
|
e45367a482 | ||
|
|
66f8cc5d18 | ||
|
|
36699e0ab1 | ||
|
|
1a382606d7 | ||
|
|
2522c9c428 | ||
|
|
d1ee12f248 | ||
|
|
d5f9c19fd3 | ||
|
|
dc700d9e37 | ||
|
|
6eb87149e2 | ||
|
|
79a0f8a891 | ||
|
|
26d41b006f | ||
|
|
60d9dc22d2 | ||
|
|
c0bf78fa7c | ||
|
|
f0c2bd0fe6 | ||
|
|
235cfd1e80 | ||
|
|
c96931ba72 | ||
|
|
2c654a28d6 | ||
|
|
72d3fc84f5 | ||
|
|
3b77604ae6 | ||
|
|
4e5f49b37d | ||
|
|
a89c075082 | ||
|
|
99e881bb95 | ||
|
|
4c585fc5d0 | ||
|
|
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 | ||
|
|
ae934b5167 | ||
|
|
3faf2ce9b9 | ||
|
|
cbe243fc9e | ||
|
|
a438f633be | ||
|
|
6b234209c6 | ||
|
|
786ce78b08 | ||
|
|
cde332684e | ||
|
|
37ef67d7ac | ||
|
|
d51e06b61f | ||
|
|
67d631b0f0 | ||
|
|
fc302ffa5f | ||
|
|
231d494955 | ||
|
|
df05fa9f3b | ||
|
|
a64f6c1625 | ||
|
|
cb7586f973 | ||
|
|
a078bb3597 | ||
|
|
8c28556a94 | ||
|
|
45cc531eec | ||
|
|
5c9ad9286d | ||
|
|
ad1c9486d7 | ||
|
|
ad6a03b712 | ||
|
|
36bb8010bc | ||
|
|
2200da7a16 | ||
|
|
688c0e2e85 | ||
|
|
e80348ac7d | ||
|
|
0c376d57e7 | ||
|
|
ee82772fee | ||
|
|
d5b6750abe | ||
|
|
70328217c6 | ||
|
|
733fb3f53a | ||
|
|
f82eb96a35 | ||
|
|
148952b96c | ||
|
|
52928e1577 | ||
|
|
cd13777c06 | ||
|
|
714345a65d | ||
|
|
b21e2acdb6 | ||
|
|
2b65aeb8b4 | ||
|
|
a8dcb11094 | ||
|
|
34a1c7e408 | ||
|
|
6255221d6a | ||
|
|
58364de72a | ||
|
|
aa42873274 | ||
|
|
8a5b36db96 | ||
|
|
0dcb9361ea | ||
|
|
6d64df4ee4 | ||
|
|
7bac2f206b | ||
|
|
75e1a17a2c | ||
|
|
47b13384a8 | ||
|
|
77b9efa7d1 | ||
|
|
be5f3b18af | ||
|
|
d5d12a7ce5 | ||
|
|
d7726d7755 | ||
|
|
0cd0d37eff | ||
|
|
c85af5502d | ||
|
|
06698d3c52 | ||
|
|
2818672cda | ||
|
|
24794f28aa | ||
|
|
50d1523210 | ||
|
|
4521def103 | ||
|
|
613cd2a1ea | ||
|
|
295cb74287 | ||
|
|
95dd3ff068 | ||
|
|
d6aeb753fc | ||
|
|
a085744038 | ||
|
|
26391a6f14 | ||
|
|
4cf734ce9a | ||
|
|
5c70f0a758 | ||
|
|
c12c2c0416 | ||
|
|
db45c422e7 | ||
|
|
affd9a95c5 | ||
|
|
7baf25869a | ||
|
|
12096fb427 | ||
|
|
ef7136cb81 | ||
|
|
3c4baf0126 | ||
|
|
f0b87c62a5 | ||
|
|
d27295739b | ||
|
|
99e8a5372b | ||
|
|
b1f7f3e351 | ||
|
|
6cfc1f3955 | ||
|
|
f1a5e3afae | ||
|
|
d8b932c9ad | ||
|
|
a319435e91 | ||
|
|
5bd0e988e3 | ||
|
|
b2be669b9e | ||
|
|
536bd11798 | ||
|
|
b74bd66cf2 | ||
|
|
5e92562c99 | ||
|
|
51952b0485 | ||
|
|
2b0c5e7fac | ||
|
|
3e6cea1a6a | ||
|
|
1aec7c0999 | ||
|
|
5da98809a5 | ||
|
|
49695614b7 | ||
|
|
3fbbc104b7 | ||
|
|
022e61b2d0 | ||
|
|
4a58fae111 | ||
|
|
2fe7c0b85e | ||
|
|
09d0e82216 | ||
|
|
d208fcea7d | ||
|
|
cc0674db34 | ||
|
|
47549fa88c | ||
|
|
be2b290300 | ||
|
|
6c9db5ce0d | ||
|
|
24677ea239 | ||
|
|
1d5b84943d | ||
|
|
14fe992ca5 | ||
|
|
15232bddaf | ||
|
|
160ef25621 | ||
|
|
2afb8688a3 | ||
|
|
9d1af035ea | ||
|
|
fb7574d814 | ||
|
|
201a3cb9e3 | ||
|
|
cc735ee6a1 | ||
|
|
0165e14ea0 | ||
|
|
97d19605d5 | ||
|
|
bc490218f9 | ||
|
|
6dac05a21d | ||
|
|
fd3fff6322 | ||
|
|
edb64fff2e | ||
|
|
fe0e854e72 | ||
|
|
288043cf49 | ||
|
|
fd5beba780 | ||
|
|
c8122aa65b | ||
|
|
ef1584de55 | ||
|
|
06c85fb203 | ||
|
|
69926c4ae1 | ||
|
|
ef44b0a412 | ||
|
|
8577ac1027 | ||
|
|
3075030b1c | ||
|
|
42c56401db | ||
|
|
a18a4383e5 | ||
|
|
375b5b3133 | ||
|
|
4cab916957 | ||
|
|
32da050106 | ||
|
|
526b74b3ef | ||
|
|
97ab328a9c | ||
|
|
7902691093 | ||
|
|
d5d9e20a06 | ||
|
|
1eb55428db | ||
|
|
9b82d704f6 | ||
|
|
dbf3d2776e | ||
|
|
c077b9d47c | ||
|
|
34157805a1 | ||
|
|
576da2a3eb | ||
|
|
e99e5b2836 | ||
|
|
fe25974958 | ||
|
|
1a95b4e361 | ||
|
|
35e3d5afc4 | ||
|
|
cc1e13d1bd | ||
|
|
0b622f8f2a | ||
|
|
9cf7da6419 | ||
|
|
812ab7198c | ||
|
|
e8c9253a76 | ||
|
|
c8f633ae3b | ||
|
|
af339a833f | ||
|
|
708142e1b8 | ||
|
|
55f32671c5 | ||
|
|
21603eedcf | ||
|
|
7fbef273a1 | ||
|
|
9e19716504 | ||
|
|
828d07d6b5 | ||
|
|
b473642ab4 | ||
|
|
fba55f01a0 | ||
|
|
015e63ba66 | ||
|
|
d92e2407f3 | ||
|
|
289db09770 | ||
|
|
a4f84fb8cd | ||
|
|
bfe88745ca | ||
|
|
0d334237ba | ||
|
|
fd5cff3fea | ||
|
|
af5b82e9fd | ||
|
|
d3561748c8 | ||
|
|
791a1d804b | ||
|
|
2442424e3b | ||
|
|
89d7dfd694 | ||
|
|
0ecedd2820 | ||
|
|
958d62ec0c | ||
|
|
400cfb2141 | ||
|
|
52b860dd8f | ||
|
|
4d57d8d576 | ||
|
|
9a098accd8 | ||
|
|
62f3b2522c | ||
|
|
9b48cd2037 | ||
|
|
69776d45d1 | ||
|
|
b8fb2660a4 | ||
|
|
941281298d | ||
|
|
8afc4511a6 | ||
|
|
f43ef325ae | ||
|
|
bbdc323204 | ||
|
|
60a998be89 | ||
|
|
128e75bc24 | ||
|
|
69c60d484c | ||
|
|
c70750a508 | ||
|
|
5553ca25a1 | ||
|
|
ff87829e72 | ||
|
|
02983c76b9 | ||
|
|
3e28eb2ccf | ||
|
|
3cf23474e3 | ||
|
|
9f0ff2dcd4 | ||
|
|
8ed9fb6276 | ||
|
|
27cbb70352 | ||
|
|
f5b10b516c | ||
|
|
5580308968 | ||
|
|
df7a0ee490 | ||
|
|
2e360dc275 | ||
|
|
8dc88d2fd7 | ||
|
|
901c70efc3 | ||
|
|
3d44e5d2cc | ||
|
|
894c8cacd6 | ||
|
|
33ea3da84d | ||
|
|
572901ec9d | ||
|
|
965239d215 | ||
|
|
ac1e5e991e | ||
|
|
e97203a6e3 | ||
|
|
66b7b127f9 | ||
|
|
b3f2987b14 | ||
|
|
c7426453a5 | ||
|
|
b60c1a5db3 | ||
|
|
664d5cc4c3 | ||
|
|
47d1b182ac | ||
|
|
c33c3d9112 | ||
|
|
ff99e1023a | ||
|
|
8009dc0a75 | ||
|
|
20039ec97e | ||
|
|
172d25eeb0 | ||
|
|
fa04e00032 | ||
|
|
3ff442894d | ||
|
|
edb0823bf9 | ||
|
|
9de83355e1 | ||
|
|
183fb0e8c0 | ||
|
|
d624a04e18 | ||
|
|
303461d803 | ||
|
|
11c7816bb1 | ||
|
|
b5eae13a16 | ||
|
|
59026286a1 | ||
|
|
ef0fbb26a4 | ||
|
|
387b31193f | ||
|
|
28e816fd89 | ||
|
|
77081f6cc9 | ||
|
|
95fa547f15 | ||
|
|
3198c9adb1 | ||
|
|
e1af6f4643 | ||
|
|
cc52e491b0 | ||
|
|
b0db0d9f2e | ||
|
|
d699788a12 | ||
|
|
bdbf441e1a | ||
|
|
0deba2885b | ||
|
|
48b39fd3bc | ||
|
|
875b235e6d | ||
|
|
1711682322 | ||
|
|
70f5ec7500 | ||
|
|
d34c73c210 | ||
|
|
50a0586e8a | ||
|
|
1d2ca1e500 | ||
|
|
34b7123a8d | ||
|
|
ec718ff58e | ||
|
|
20a442e27f | ||
|
|
47cb74abc9 | ||
|
|
a11e53c89a | ||
|
|
d721e8ca46 | ||
|
|
23e5dcd030 | ||
|
|
25dd2cfab8 | ||
|
|
8537c7a7ae | ||
|
|
ebd7f9c36c | ||
|
|
49cda3aeb2 | ||
|
|
67cbc8aff2 | ||
|
|
5db44cbf9d | ||
|
|
95858e3280 | ||
|
|
36846acbe5 | ||
|
|
b95d944003 | ||
|
|
f52886af74 | ||
|
|
6cbe406cef | ||
|
|
3097bc7168 | ||
|
|
5f7faa69e8 | ||
|
|
ed24e89a96 | ||
|
|
8ea05c6ebd | ||
|
|
a30fcbbe34 | ||
|
|
39f76f9988 | ||
|
|
5135653cd3 | ||
|
|
2e4f04cd88 | ||
|
|
a44e0e036a | ||
|
|
5d54c1bae4 | ||
|
|
09577074a9 | ||
|
|
5c071aa3ec | ||
|
|
53f0e2a933 | ||
|
|
b8dccbbef1 | ||
|
|
2ef17ba051 | ||
|
|
f63daf3a4e | ||
|
|
52b079be2a | ||
|
|
c5a61c3916 | ||
|
|
efeca17106 | ||
|
|
6827166c1d | ||
|
|
04483e61e8 | ||
|
|
0ed858b99c | ||
|
|
9b3e153a4d | ||
|
|
e525aef3d9 | ||
|
|
22fe174922 | ||
|
|
f143da3913 | ||
|
|
7e9f41c74b | ||
|
|
a1474d0d29 | ||
|
|
0dce936ad3 | ||
|
|
6ebbbb4c6c | ||
|
|
dc6ddbd0ee | ||
|
|
86c81d6b53 | ||
|
|
451a92aa36 | ||
|
|
5c42e67e73 | ||
|
|
d20d36d964 | ||
|
|
1a8d46c71e | ||
|
|
91da10eca3 | ||
|
|
3bb0dcee53 | ||
|
|
d3fd4b200f | ||
|
|
fed96864e1 | ||
|
|
3b351bea27 | ||
|
|
254e01dca1 | ||
|
|
19158e1d48 | ||
|
|
bffb78fccf | ||
|
|
a3800592a2 | ||
|
|
22a498dfc9 | ||
|
|
9ea96e32bd | ||
|
|
68f51a123e | ||
|
|
43b1b63581 | ||
|
|
43b4a2c515 | ||
|
|
5b9cfdb689 | ||
|
|
b43cd7103a | ||
|
|
30e4f6e0f5 | ||
|
|
1d0ebf889b | ||
|
|
7c4f1da485 | ||
|
|
8163921014 | ||
|
|
993393dd96 | ||
|
|
82aed43934 | ||
|
|
fa65134c26 | ||
|
|
af4266c739 | ||
|
|
f72ea2e763 | ||
|
|
c5540270a3 | ||
|
|
201b72c9c8 | ||
|
|
26b99f5f68 | ||
|
|
d3dc774492 | ||
|
|
1f7155a932 | ||
|
|
02729fe02b | ||
|
|
5840c94395 | ||
|
|
92d7ae67ef | ||
|
|
fc09514a4d | ||
|
|
300fa97781 | ||
|
|
e8f0891f3a | ||
|
|
f25906a694 | ||
|
|
e52699bb1c | ||
|
|
712826451f | ||
|
|
3b3065d8bd | ||
|
|
e11fe7d8cf | ||
|
|
9a2f7475c9 | ||
|
|
6dffb10906 | ||
|
|
71039d6901 | ||
|
|
db18c7a0d0 | ||
|
|
7e80ed6af2 | ||
|
|
bdd2b90581 | ||
|
|
50d017d8ba | ||
|
|
98e003437c | ||
|
|
d5d06af614 | ||
|
|
1c930ca3bb | ||
|
|
0f0c1093d4 | ||
|
|
2ad5dc5a74 | ||
|
|
e02e0865bd | ||
|
|
59ee1af75d | ||
|
|
c04584dfa6 | ||
|
|
190a3b5b08 | ||
|
|
f5a67e65f0 | ||
|
|
498078b6e0 | ||
|
|
de8c289ca7 | ||
|
|
07b205a746 | ||
|
|
a7941310bc | ||
|
|
864b6dcdac | ||
|
|
d3744bb397 | ||
|
|
7d1853bc88 | ||
|
|
9f0db755d1 | ||
|
|
06819806be | ||
|
|
526f5e319b | ||
|
|
30a4d0efd9 | ||
|
|
d419dba44a | ||
|
|
87da6b9b81 | ||
|
|
71970b2249 | ||
|
|
49dc34b256 | ||
|
|
1ca95be7e0 | ||
|
|
8a5b881a57 | ||
|
|
67cc1553da | ||
|
|
9a985aad29 | ||
|
|
fd98159fce | ||
|
|
42fac30e63 | ||
|
|
18e7f14c16 | ||
|
|
7b263800a6 | ||
|
|
17387a32b2 | ||
|
|
3fe642c2f2 | ||
|
|
9225447409 | ||
|
|
e05dee64b7 | ||
|
|
7f30973b39 | ||
|
|
25da9bb2d0 | ||
|
|
e17b49e704 | ||
|
|
db661b56cb | ||
|
|
638e1bf8e9 | ||
|
|
dafe233315 | ||
|
|
4107169c73 | ||
|
|
83786171a7 | ||
|
|
f638a538c1 | ||
|
|
11e2316cbc | ||
|
|
1b372c7dca | ||
|
|
cf4e37fade | ||
|
|
f3fc60ac00 | ||
|
|
1026d99025 | ||
|
|
3fb947fbfe | ||
|
|
f097506d49 | ||
|
|
3df7d74fbf | ||
|
|
3fc17eb9c9 | ||
|
|
67ee74c1a0 | ||
|
|
bcebba8896 | ||
|
|
451b0c1ac5 | ||
|
|
5d08b4923b | ||
|
|
9ba948c721 | ||
|
|
23e7513133 | ||
|
|
aaed8e53c8 | ||
|
|
f2754cc5c9 | ||
|
|
6a65edd089 | ||
|
|
4d49b10585 | ||
|
|
767312f71c | ||
|
|
045668d67b | ||
|
|
f5b98009dd | ||
|
|
cf0b66d852 | ||
|
|
8f80668e0f | ||
|
|
b1754846f8 | ||
|
|
681ee1c211 | ||
|
|
714ec9692c | ||
|
|
86bfab81bd | ||
|
|
07d2ad749e | ||
|
|
50e313cff0 | ||
|
|
1e7e4810f1 | ||
|
|
5d07cde6dd | ||
|
|
f84a923102 | ||
|
|
9964236616 | ||
|
|
c9eac34ae6 | ||
|
|
181a0577c8 | ||
|
|
0426084194 | ||
|
|
26579abe6a | ||
|
|
d829c659bd | ||
|
|
ce3ca42c7a | ||
|
|
197d0caf44 | ||
|
|
a4a082f76a | ||
|
|
84026afb92 | ||
|
|
4dea7d2a52 | ||
|
|
2df1b7dd61 | ||
|
|
89042113a5 | ||
|
|
48665ebcce | ||
|
|
2528d48010 | ||
|
|
5456d71979 | ||
|
|
e36aae3cf3 | ||
|
|
d6040c0895 | ||
|
|
6d12e2dd72 | ||
|
|
ff47e6edba | ||
|
|
327ceb04d4 | ||
|
|
40a34b07de | ||
|
|
f117249bb5 | ||
|
|
1c90164ece | ||
|
|
0d5fb250bc | ||
|
|
e9bd5a373a | ||
|
|
c494d283ba | ||
|
|
cf1d537367 | ||
|
|
517d13b400 | ||
|
|
103aaafff1 | ||
|
|
fae870c93a | ||
|
|
f8e00dcc80 | ||
|
|
5fdbb597bb | ||
|
|
d74b286a9d | ||
|
|
ecb3c521ff | ||
|
|
1d093ce928 | ||
|
|
46b711af2e | ||
|
|
772e6ddb5d | ||
|
|
f84e8443d2 | ||
|
|
250c18ebf1 | ||
|
|
e3d0f38b79 | ||
|
|
c512f97783 | ||
|
|
0594680775 | ||
|
|
f999881f59 | ||
|
|
4fe9192ac6 | ||
|
|
d936702fa9 | ||
|
|
74e284b0de | ||
|
|
4c42b72ed8 | ||
|
|
0e0046df65 | ||
|
|
c80d1d10c2 | ||
|
|
da97971011 | ||
|
|
700447dbe7 | ||
|
|
37e7b5ee93 | ||
|
|
1265afa93f | ||
|
|
1e09481b02 | ||
|
|
9996a5a05e | ||
|
|
f20aac7c81 | ||
|
|
98f7b0bacd | ||
|
|
3f6d3fb3a2 | ||
|
|
663b49c76b | ||
|
|
16e38f2541 | ||
|
|
842cc55e47 | ||
|
|
72db099e6f | ||
|
|
be130bc3a7 | ||
|
|
42253336e1 | ||
|
|
572631e1d7 | ||
|
|
723777a800 | ||
|
|
b825d534c1 | ||
|
|
b9749620a8 | ||
|
|
70ea9989aa | ||
|
|
b3ec9c981c | ||
|
|
bf72085abb | ||
|
|
64dd416b59 | ||
|
|
ab2a920455 | ||
|
|
7580446d60 | ||
|
|
ade18ac6fc | ||
|
|
005c851d72 | ||
|
|
0f1d46c765 | ||
|
|
21fbb07b1d | ||
|
|
dff2217e80 | ||
|
|
58e0ce3970 | ||
|
|
139a7d7c98 | ||
|
|
a1c81e89e8 | ||
|
|
a5c197b496 | ||
|
|
df49ef9d58 | ||
|
|
f747d4c979 | ||
|
|
98677cd307 | ||
|
|
cd3de97d55 | ||
|
|
4853a25710 | ||
|
|
eba9a1da7b | ||
|
|
068c62b060 | ||
|
|
5d7f06eba0 | ||
|
|
fed9dec33a | ||
|
|
7973914a5f | ||
|
|
35efb3f047 | ||
|
|
0a4ed50904 | ||
|
|
002c66174a | ||
|
|
22aac3d943 | ||
|
|
872f47305a | ||
|
|
75d5332411 | ||
|
|
035da8a517 | ||
|
|
4c2c877d41 | ||
|
|
0cc8cddfc3 | ||
|
|
4428ef7ac2 | ||
|
|
44912b7982 | ||
|
|
c930db6068 | ||
|
|
d96d4dd581 | ||
|
|
67e3a5bb47 | ||
|
|
b0a5aa93e1 | ||
|
|
0bc1459898 | ||
|
|
fae25e93a5 | ||
|
|
c0c121050c | ||
|
|
afe572ca7f | ||
|
|
8cb4db5fcf | ||
|
|
de235ec7cc | ||
|
|
140c2a7b9d | ||
|
|
c833513344 | ||
|
|
1e95536208 | ||
|
|
b58fda9795 | ||
|
|
3135aef398 | ||
|
|
c83dc51322 | ||
|
|
9cfaed89e6 | ||
|
|
5374ac766c | ||
|
|
98596e77f2 | ||
|
|
54aa89c7f8 | ||
|
|
f59157b160 | ||
|
|
6b38db9607 | ||
|
|
53afc120f3 | ||
|
|
3c603d5dbc | ||
|
|
c10cdfd795 | ||
|
|
c2184e7bd8 | ||
|
|
baf756e163 | ||
|
|
efc67fd7e8 | ||
|
|
43e737425a | ||
|
|
b5b3cb42a1 | ||
|
|
f72f7cb831 | ||
|
|
f86d60be23 | ||
|
|
7c8624bd53 | ||
|
|
a75ce70615 | ||
|
|
331548b38d | ||
|
|
8b8f192dfa | ||
|
|
061b2ee3de | ||
|
|
5ea2864bd5 | ||
|
|
ee2b4b6a1f | ||
|
|
697f801c1a | ||
|
|
ebb49c44fe | ||
|
|
bc4619e6b1 | ||
|
|
4a3b948760 | ||
|
|
f81283c892 | ||
|
|
7eae879037 | ||
|
|
1b0ce5d893 | ||
|
|
5d26ea85e9 | ||
|
|
6efe263dd8 | ||
|
|
0379347f2d | ||
|
|
1299b2ad42 | ||
|
|
f3b3bcaa0a | ||
|
|
b1bec870c5 | ||
|
|
36e05a6d14 | ||
|
|
2e11f78e9d | ||
|
|
9fcfbe5593 | ||
|
|
c17745368d | ||
|
|
e78b518654 | ||
|
|
55a8634be2 | ||
|
|
ac891eea53 | ||
|
|
6096857613 | ||
|
|
20c2681872 | ||
|
|
6778e302b4 | ||
|
|
6f981a1c7d | ||
|
|
d07ec7f1af | ||
|
|
391a44c0e5 | ||
|
|
74fa2a3081 | ||
|
|
a849ced203 | ||
|
|
6c1c5b7759 | ||
|
|
f2030a8c26 | ||
|
|
1f4152b588 | ||
|
|
70386ea1b2 | ||
|
|
02f97cd142 | ||
|
|
76e0c5bc4d | ||
|
|
084bbc18cf | ||
|
|
cbce90c461 | ||
|
|
fb8a38e77b | ||
|
|
2e184589e6 | ||
|
|
f13b6bd4b4 | ||
|
|
74ae3bf706 | ||
|
|
1feccdc26d | ||
|
|
3f253c1211 | ||
|
|
584e2c0daa | ||
|
|
6f180c0451 | ||
|
|
c7017c12d9 | ||
|
|
df2211fa15 | ||
|
|
c38c2a425b | ||
|
|
8adb1d569e | ||
|
|
9e27e21e78 | ||
|
|
f43352b790 | ||
|
|
c5b52b2781 | ||
|
|
b91840fb95 | ||
|
|
2f8b6acd58 | ||
|
|
ce9dda4e6e | ||
|
|
63338a139a | ||
|
|
49c2365120 | ||
|
|
df06cdb0bb | ||
|
|
fda89a8d36 | ||
|
|
1988849b26 | ||
|
|
fc10fbffb0 | ||
|
|
3b74a183f8 | ||
|
|
41a639bb02 | ||
|
|
e40841c128 | ||
|
|
a21a74a8e7 | ||
|
|
8a6c9881e4 | ||
|
|
20e5d2a545 | ||
|
|
7da363fb87 | ||
|
|
c46f78272d | ||
|
|
95685d4de8 | ||
|
|
d0c5a2ed2e | ||
|
|
3fceeec362 | ||
|
|
cbee0fe72e | ||
|
|
6d085ae6f0 | ||
|
|
12cbb7518f | ||
|
|
5220a98082 | ||
|
|
b9bf1e6601 | ||
|
|
4de7211523 | ||
|
|
05f7a44bd5 | ||
|
|
1079f600bc | ||
|
|
72580dadd0 | ||
|
|
16e04a5a0e | ||
|
|
98a02e874b | ||
|
|
d219d7aa4b | ||
|
|
68a9fe8376 | ||
|
|
098b0885a2 | ||
|
|
e8631e432a | ||
|
|
f2f8620312 | ||
|
|
4ee229ea79 | ||
|
|
c261214e49 | ||
|
|
b06df8c3d0 |
43
.github/workflows/nightly-builds.yml
vendored
43
.github/workflows/nightly-builds.yml
vendored
@@ -3,6 +3,7 @@ name: Nightly builds
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -10,27 +11,27 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Appkit Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: LucasGGamerM/appkit
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
|
||||
- name: Comment out signing config in appkits gradle file
|
||||
run: |
|
||||
sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||
|
||||
- name: Grant execute permission for gradlew for Appkit
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Compile appkit
|
||||
run: ./gradlew publishToMavenLocal
|
||||
# - name: Checkout Appkit Repo
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# repository: grishka/appkit
|
||||
#
|
||||
# - name: set up JDK 17
|
||||
# uses: actions/setup-java@v3
|
||||
# with:
|
||||
# java-version: '17'
|
||||
# distribution: 'corretto'
|
||||
# cache: gradle
|
||||
#
|
||||
# - name: Comment out signing config in appkits gradle file
|
||||
# run: |
|
||||
# sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||
#
|
||||
# - name: Grant execute permission for gradlew for Appkit
|
||||
# run: chmod +x gradlew
|
||||
#
|
||||
# - name: Compile appkit
|
||||
# run: ./gradlew publishToMavenLocal
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
|
||||
2
FAQ.md
2
FAQ.md
@@ -4,6 +4,6 @@ Q: What are the main differences between Moshidon and Megalodon?
|
||||
|
||||
A: There are many, but the most outstanding differences are: the ability to have other server's local timeline inside the app. It can be acessed in the "Add community" option in the top right corner of the Edit timelines screen. Other outstanding features that Moshidon has are some quality of life improvements, such as notification actions and allowing for unlisted replies by default. Most other features are pretty minor, such as profile notes directly available in the person's profile. Other features are quite minor usability and visibility improvements. All of which can be found in the settings page.
|
||||
|
||||
Q: Will there ever be a versjon of Moshidon for iOS?
|
||||
Q: Will there ever be a version of Moshidon for iOS?
|
||||
|
||||
A: No. As android and iOS apps do not share code, it is incredibly hard to port.
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.moshinda"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
|
||||
|
||||
<a href="https://f-droid.org/pt_BR/packages/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on F-Droid" src="img/f-droid-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!
|
||||
|
||||
@@ -19,4 +19,5 @@ android.useAndroidX=true
|
||||
android.enableJetifier=false
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
android.nonFinalResIds=false
|
||||
org.gradle.configuration-cache=true
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.moshinda"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 102
|
||||
versionName "2.1.4+fork.102.moshinda"
|
||||
versionCode 105
|
||||
versionName "2.3.0+fork.105.moshinda"
|
||||
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']
|
||||
}
|
||||
@@ -44,6 +44,28 @@ android {
|
||||
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 {
|
||||
@@ -81,8 +103,10 @@ android {
|
||||
versionNameSuffix '-play'
|
||||
}
|
||||
githubRelease { initWith release }
|
||||
playRelease { initWith release }
|
||||
fdroidRelease { initWith release }
|
||||
fdroidRelease {
|
||||
initWith release
|
||||
// signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -118,7 +142,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.14'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
@@ -257,5 +257,9 @@ public class UiUtilsTest {
|
||||
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "-- * (asterisk) --")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("they/(she?)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "they/(she?)...")
|
||||
)).orElseThrow());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import static org.joinmastodon.android.model.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.FilterContext.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.junit.Test;
|
||||
@@ -32,11 +35,11 @@ public class StatusFilterPredicateTest {
|
||||
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";
|
||||
}
|
||||
// noAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
|
||||
// withAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
|
||||
// for (Attachment mediaAttachment : withAltText.mediaAttachments) {
|
||||
// mediaAttachment.description = "Alt Text";
|
||||
// }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -25,12 +25,17 @@
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TRANSLATE" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MastodonApp"
|
||||
android:allowBackup="true"
|
||||
android:label="@string/mo_app_name"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
||||
@@ -1,56 +1,43 @@
|
||||
13bells.com
|
||||
1611.social
|
||||
4aem.com
|
||||
5dollah.click
|
||||
adachi.party
|
||||
anime.website
|
||||
adtension.com
|
||||
annihilation.social
|
||||
anon-kenkai.com
|
||||
asbestos.cafe
|
||||
bae.st
|
||||
bajax.us
|
||||
banepo.st
|
||||
baraag.net
|
||||
bassam.social
|
||||
battlepenguin.video
|
||||
beefyboys.win
|
||||
beepboop.ga
|
||||
berserker.town
|
||||
bikeshed.party
|
||||
boks.moe
|
||||
boymoder.biz
|
||||
brainsoap.net
|
||||
breastmilk.club
|
||||
brighteon.social
|
||||
bungle.online
|
||||
cachapa.xyz
|
||||
canary.fedinuke.example.com
|
||||
catgirl.life
|
||||
cawfee.club
|
||||
childlove.space
|
||||
clew.lol
|
||||
clubcyberia.co
|
||||
collapsitarian.io
|
||||
comfyboy.club
|
||||
contrapointsfan.club
|
||||
crucible.world
|
||||
cum.camp
|
||||
cum.salon
|
||||
darknight-coffee.org
|
||||
decayable.ink
|
||||
dembased.xyz
|
||||
desupost.soy
|
||||
detroitriotcity.com
|
||||
eatthebugs.social
|
||||
djsumdog.com
|
||||
eientei.org
|
||||
elementality.org
|
||||
eveningzoo.club
|
||||
firedragonstudios.com
|
||||
firefaithfellowship.com
|
||||
fluf.club
|
||||
foxfam.club
|
||||
freak.university
|
||||
freeatlantis.com
|
||||
freedomstrike.org
|
||||
freesoftwareextremist.com
|
||||
freespeech.group
|
||||
freespeechextremist.com
|
||||
freetalklive.com
|
||||
froth.zone
|
||||
fulltermprivacy.com
|
||||
gameliberty.club
|
||||
gearlandia.haus
|
||||
genderheretics.xyz
|
||||
@@ -59,42 +46,34 @@ gleasonator.com
|
||||
glee.li
|
||||
glindr.org
|
||||
goyim.app
|
||||
goyslop.cafe
|
||||
h5q.net
|
||||
haeder.net
|
||||
handholding.io
|
||||
hitchhiker.social
|
||||
hunk.city
|
||||
iddqd.social
|
||||
intkos.link
|
||||
justicewarrior.social
|
||||
kawa-kun.com
|
||||
kitsunemimi.club
|
||||
kiwifarms.cc
|
||||
kompost.cz
|
||||
kurosawa.moe
|
||||
kyaruc.moe
|
||||
leafposter.club
|
||||
leftychan.net
|
||||
lewdieheaven.com
|
||||
liberdon.com
|
||||
ligma.pro
|
||||
lolicon.rocks
|
||||
lolison.network
|
||||
lolison.top
|
||||
lovingexpressions.net
|
||||
mahodou.moe
|
||||
makemysarcophagus.com
|
||||
maladaptive.art
|
||||
marsey.moe
|
||||
masochi.st
|
||||
mastinator.com
|
||||
merovingian.club
|
||||
midwaytrades.com
|
||||
mirr0r.city
|
||||
moa.st
|
||||
morale.ch
|
||||
mouse.services
|
||||
mugicha.club
|
||||
narrativerry.xyz
|
||||
natehiggers.online
|
||||
neckbeard.xyz
|
||||
needs.vodka
|
||||
neenster.org
|
||||
nicecrew.digital
|
||||
@@ -103,18 +82,18 @@ noagendasocial.com
|
||||
noagendasocial.nl
|
||||
noagendatube.com
|
||||
nobodyhasthe.biz
|
||||
nukem.biz
|
||||
obo.sh
|
||||
norwoodzero.net
|
||||
nyanide.com
|
||||
onionfarms.org
|
||||
pawlicker.com
|
||||
pawoo.net
|
||||
pedo.school
|
||||
peervideo.club
|
||||
piazza.today
|
||||
pibvt.net
|
||||
pieville.net
|
||||
pisskey.io
|
||||
plagu.ee
|
||||
pmth.us
|
||||
poa.st
|
||||
poast.org
|
||||
poast.tv
|
||||
@@ -123,17 +102,18 @@ prospeech.space
|
||||
quodverum.com
|
||||
r18.social
|
||||
rakket.app
|
||||
rapemeat.express
|
||||
rapemeat.solutions
|
||||
rdrama.cc
|
||||
rayci.st
|
||||
rebelbase.site
|
||||
retardedniggers.forsale
|
||||
rojogato.com
|
||||
ryona.agency
|
||||
sad.cab
|
||||
schwartzwelt.xyz
|
||||
seal.cafe
|
||||
shaw.app
|
||||
shigusegubu.club
|
||||
shitpost.cloud
|
||||
shota.house
|
||||
shortstacksran.ch
|
||||
silliness.observer
|
||||
skinheads.eu
|
||||
skinheads.io
|
||||
@@ -148,23 +128,20 @@ sneed.social
|
||||
sonichu.com
|
||||
spinster.xyz
|
||||
springbo.cc
|
||||
starnix.network
|
||||
strelizia.net
|
||||
syspxl.xyz
|
||||
tastingtraffic.net
|
||||
teci.world
|
||||
theapex.social
|
||||
thechimp.zone
|
||||
thenobody.club
|
||||
thepostearthdestination.com
|
||||
tkammer.de
|
||||
trumpislovetrumpis.life
|
||||
truthsocial.co.in
|
||||
urchan.org
|
||||
usualsuspects.lol
|
||||
varishangout.net
|
||||
whinge.house
|
||||
whinge.town
|
||||
wideboys.org
|
||||
vtuberfan.social
|
||||
wolfgirl.bar
|
||||
xn--p1abe3d.xn--80asehdb
|
||||
yggdrasil.social
|
||||
youjo.love
|
||||
zztails.gay
|
||||
|
||||
202
mastodon/src/main/java/name/fraser/neil/plaintext/LICENSE
Normal file
202
mastodon/src/main/java/name/fraser/neil/plaintext/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -266,7 +266,7 @@ public class AudioPlayerService extends Service{
|
||||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||
Notification.Builder bldr=new Notification.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setContentTitle(status.account.displayName)
|
||||
.setContentTitle(status.account.getDisplayName())
|
||||
.setContentText(HtmlParser.strip(status.content))
|
||||
.setOngoing(!dismissable)
|
||||
.setShowWhen(false)
|
||||
@@ -281,7 +281,7 @@ public class AudioPlayerService extends Service{
|
||||
|
||||
if(playerReady){
|
||||
boolean isPlaying=player.isPlaying();
|
||||
bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_pause_24 : R.drawable.ic_play_24),
|
||||
bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_fluent_pause_24_filled : R.drawable.ic_fluent_play_24_filled),
|
||||
getString(isPlaying ? R.string.pause : R.string.play),
|
||||
PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE))
|
||||
.build());
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
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.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.jsoup.internal.StringUtil;
|
||||
|
||||
@@ -32,10 +32,9 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
|
||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);
|
||||
boolean isOpenable = isFediUrl || fediHandle.isPresent();
|
||||
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference.MATERIAL3;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -13,6 +14,7 @@ import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
@@ -26,7 +28,8 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
private static final String TAG="GlobalUserPreferences";
|
||||
@@ -42,7 +45,6 @@ public class GlobalUserPreferences{
|
||||
public static boolean showNewPostsButton;
|
||||
public static boolean toolbarMarquee;
|
||||
public static boolean disableSwipe;
|
||||
public static boolean voteButtonForSingleChoice;
|
||||
public static boolean enableDeleteNotifications;
|
||||
public static boolean translateButtonOpenedOnly;
|
||||
public static boolean uniformNotificationIcon;
|
||||
@@ -54,16 +56,17 @@ public class GlobalUserPreferences{
|
||||
public static boolean collapseLongPosts;
|
||||
public static boolean spectatorMode;
|
||||
public static boolean autoHideFab;
|
||||
public static boolean compactReblogReplyLine;
|
||||
public static boolean allowRemoteLoading;
|
||||
public static boolean forwardReportDefault;
|
||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||
public static ColorPreference color;
|
||||
public static boolean disableM3PillActiveIndicator;
|
||||
public static boolean showNavigationLabels;
|
||||
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
|
||||
public static boolean overlayMedia;
|
||||
public static boolean showSuicideHelp;
|
||||
public static boolean underlinedLinks;
|
||||
public static ColorPreference color;
|
||||
public static boolean likeIcon;
|
||||
|
||||
// MOSHIDON
|
||||
public static boolean showDividers;
|
||||
@@ -84,6 +87,11 @@ public class GlobalUserPreferences{
|
||||
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){
|
||||
if(json==null) return orElse;
|
||||
try{
|
||||
@@ -116,7 +124,6 @@ public class GlobalUserPreferences{
|
||||
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
||||
toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
|
||||
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
||||
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
||||
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
||||
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
@@ -128,7 +135,6 @@ public class GlobalUserPreferences{
|
||||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
||||
@@ -139,12 +145,14 @@ public class GlobalUserPreferences{
|
||||
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
|
||||
overlayMedia=prefs.getBoolean("overlayMedia", false);
|
||||
showSuicideHelp=prefs.getBoolean("showSuicideHelp", true);
|
||||
underlinedLinks=prefs.getBoolean("underlinedLinks", true);
|
||||
color=ColorPreference.valueOf(prefs.getString("color", MATERIAL3.name()));
|
||||
likeIcon=prefs.getBoolean("likeIcon", false);
|
||||
|
||||
// MOSHIDON
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
showDividers =prefs.getBoolean("showDividers", false);
|
||||
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
|
||||
doubleTapToSearch =prefs.getBoolean("doubleTapToSearch", true);
|
||||
doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true);
|
||||
@@ -169,18 +177,11 @@ public class GlobalUserPreferences{
|
||||
.apply();
|
||||
}
|
||||
|
||||
try {
|
||||
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.MATERIAL3.name()));
|
||||
}else{
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PURPLE.name()));
|
||||
}
|
||||
} catch (IllegalArgumentException|ClassCastException ignored) {
|
||||
// invalid color name or color was previously saved as integer
|
||||
color=ColorPreference.PURPLE;
|
||||
}
|
||||
|
||||
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
|
||||
int migrationLevel=prefs.getInt("migrationLevel", BuildConfig.VERSION_CODE);
|
||||
if(migrationLevel < 61)
|
||||
migrateToUpstreamVersion61();
|
||||
if(migrationLevel < BuildConfig.VERSION_CODE)
|
||||
prefs.edit().putInt("migrationLevel", BuildConfig.VERSION_CODE).apply();
|
||||
}
|
||||
|
||||
public static void save(){
|
||||
@@ -210,8 +211,6 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||
.putBoolean("spectatorMode", spectatorMode)
|
||||
.putBoolean("autoHideFab", autoHideFab)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putString("color", color.name())
|
||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||
.putBoolean("forwardReportDefault", forwardReportDefault)
|
||||
@@ -222,12 +221,14 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
|
||||
.putBoolean("overlayMedia", overlayMedia)
|
||||
.putBoolean("showSuicideHelp", showSuicideHelp)
|
||||
.putBoolean("underlinedLinks", underlinedLinks)
|
||||
.putString("color", color.name())
|
||||
.putBoolean("likeIcon", likeIcon)
|
||||
|
||||
// MOSHIDON
|
||||
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
|
||||
.putBoolean("doubleTapToSearch", doubleTapToSearch)
|
||||
.putBoolean("doubleTapToSwipe", doubleTapToSwipe)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
|
||||
@@ -240,11 +241,59 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
|
||||
.putBoolean("showMediaPreview", showMediaPreview)
|
||||
|
||||
.putInt("theme", theme.ordinal())
|
||||
|
||||
.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{
|
||||
AUTO,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
public enum PreReplySheetType{
|
||||
OLD_POST,
|
||||
NON_MUTUAL
|
||||
}
|
||||
|
||||
public enum AutoRevealMode {
|
||||
NEVER,
|
||||
THREADS,
|
||||
DISCUSSIONS
|
||||
}
|
||||
|
||||
public enum PrefixRepliesMode {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
TO_OTHERS
|
||||
}
|
||||
|
||||
|
||||
//region preferences migrations
|
||||
|
||||
private static void migrateToUpstreamVersion61(){
|
||||
Log.d(TAG, "Migrating preferences to upstream version 61!!");
|
||||
|
||||
@@ -291,53 +340,7 @@ public class GlobalUserPreferences{
|
||||
|
||||
localPrefs.save();
|
||||
}
|
||||
|
||||
prefs.edit().putInt("migrationLevel", 61).apply();
|
||||
}
|
||||
|
||||
public enum ColorPreference{
|
||||
MATERIAL3,
|
||||
PURPLE,
|
||||
PINK,
|
||||
GREEN,
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
YELLOW,
|
||||
NORD,
|
||||
WHITE;
|
||||
|
||||
public @StringRes int getName() {
|
||||
return switch(this){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
case NORD -> R.string.mo_color_palette_nord;
|
||||
case WHITE -> R.string.mo_color_palette_black_and_white;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
AUTO,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
public enum AutoRevealMode {
|
||||
NEVER,
|
||||
THREADS,
|
||||
DISCUSSIONS
|
||||
}
|
||||
|
||||
public enum PrefixRepliesMode {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
TO_OTHERS
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.net.Uri;
|
||||
import android.os.BadParcelableException;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
@@ -39,62 +40,46 @@ import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Instant;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
private static final String TAG="MainActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
AccountSession session=getCurrentSession();
|
||||
UiUtils.setUserPreferredTheme(this, session);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState==null){
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||
}else{
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
AccountSessionManager.getInstance()
|
||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
||||
showFragmentForExternalShare(intent.getExtras());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!hasNotification) args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
if(fromNotification && hasNotification){
|
||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
} else if (intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
} else {
|
||||
showFragmentClearingBackStack(fragment);
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e)->{
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "crash.log");
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
PrintWriter writer=new PrintWriter(out);
|
||||
writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")");
|
||||
writer.println(Instant.now().toString());
|
||||
writer.println();
|
||||
e.printStackTrace(writer);
|
||||
writer.flush();
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing crash.log", x);
|
||||
}finally{
|
||||
defaultHandler.uncaughtException(t, e);
|
||||
}
|
||||
});
|
||||
|
||||
if(savedInstanceState==null){
|
||||
restartHomeFragment();
|
||||
}
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
@@ -147,11 +132,11 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
||||
session=AccountSessionManager.get(accountID);
|
||||
if(session==null || !session.activated)
|
||||
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){
|
||||
new GetSearchResults(q, null, true, null, 0, 0)
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
|
||||
new GetSearchResults(q, type, true, null, 0, 0)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
@@ -285,4 +270,87 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
||||
Fragment fragment = getCurrentFragment();
|
||||
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
|
||||
}
|
||||
|
||||
public AccountSession getCurrentSession(){
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
return AccountSessionManager.getInstance()
|
||||
.getAccount(intent.getStringExtra("account"));
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!hasNotification) args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
public void restartActivity(){
|
||||
finish();
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
}
|
||||
|
||||
public void restartHomeFragment(){
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||
}else{
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
AccountSessionManager.getInstance()
|
||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
||||
showFragmentForExternalShare(intent.getExtras());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!hasNotification) args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
if(fromNotification && hasNotification){
|
||||
// Parcelables might not be compatible across app versions so this protects against possible crashes
|
||||
// when a notification was received, then the app was updated, and then the user opened the notification
|
||||
try{
|
||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
}catch(BadParcelableException x){
|
||||
Log.w(TAG, x);
|
||||
}
|
||||
} else if (intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
} else {
|
||||
showFragmentClearingBackStack(fragment);
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import android.graphics.drawable.Drawable;
|
||||
import android.opengl.Visibility;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -38,7 +37,6 @@ import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -135,7 +133,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
|
||||
if(intent.hasExtra("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) {
|
||||
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
||||
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
||||
@@ -156,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
|
||||
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){
|
||||
@@ -218,7 +220,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
|
||||
if (!GlobalUserPreferences.uniformNotificationIcon) {
|
||||
builder.setSmallIcon(switch (pn.notificationType) {
|
||||
case FAVORITE -> R.drawable.ic_fluent_star_24_filled;
|
||||
case FAVORITE -> GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_filled : R.drawable.ic_fluent_star_24_filled;
|
||||
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
|
||||
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled;
|
||||
case MENTION -> R.drawable.ic_fluent_mention_24_filled;
|
||||
|
||||
@@ -72,7 +72,7 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
||||
result.items
|
||||
.stream()
|
||||
.findFirst()
|
||||
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
|
||||
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, account, value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,23 +9,35 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
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.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -43,6 +55,7 @@ public class CacheController{
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private List<FollowList> lists;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@@ -69,12 +82,11 @@ public class CacheController{
|
||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0) ? status.id : null;
|
||||
newMaxID=status.id;
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
@@ -86,9 +98,7 @@ public class CacheController{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -116,12 +126,14 @@ public class CacheController{
|
||||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
int flags=0;
|
||||
if(s.hasGapAfter)
|
||||
if(Objects.equals(s.hasGapAfter, s.id))
|
||||
flags|=POST_FLAG_GAP_AFTER;
|
||||
values.put("flags", flags);
|
||||
values.put("time", s.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
if(!clear)
|
||||
db.delete("home_timeline", "`id` NOT IN (SELECT `id` FROM `home_timeline` ORDER BY `time` DESC LIMIT ?)", new String[]{"1000"});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,6 +285,28 @@ public class CacheController{
|
||||
|
||||
public void deleteStatus(String id){
|
||||
runOnDbThread((db)->{
|
||||
String gapId=null;
|
||||
int gapFlags=0;
|
||||
// select to-be-removed and newer row
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"id", "flags"}, "`time`>=(SELECT `time` FROM `home_timeline` WHERE `id`=?)", new String[]{id}, null, null, "`time` ASC", "2")){
|
||||
boolean hadGapAfter=false;
|
||||
// always either one or two iterations (only one if there's no newer post)
|
||||
while(cursor.moveToNext()){
|
||||
String currentId=cursor.getString(0);
|
||||
int currentFlags=cursor.getInt(1);
|
||||
if(currentId.equals(id)){
|
||||
hadGapAfter=((currentFlags & POST_FLAG_GAP_AFTER)!=0);
|
||||
}else if(hadGapAfter){
|
||||
gapFlags=currentFlags|POST_FLAG_GAP_AFTER;
|
||||
gapId=currentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(gapId!=null){
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("flags", gapFlags);
|
||||
db.update("home_timeline", values, "`id`=?", new String[]{gapId});
|
||||
}
|
||||
db.delete("home_timeline", "`id`=?", new String[]{id});
|
||||
});
|
||||
}
|
||||
@@ -326,6 +360,99 @@ public class CacheController{
|
||||
}, 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{
|
||||
|
||||
public DatabaseHelper(){
|
||||
|
||||
@@ -54,7 +54,9 @@ public class MastodonAPIController{
|
||||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
private AccountSession session;
|
||||
@@ -113,24 +115,24 @@ public class MastodonAPIController{
|
||||
}
|
||||
|
||||
Request hreq=builder.build();
|
||||
Call call=httpClient.newCall(hreq);
|
||||
OkHttpClient client=req.timeout>0
|
||||
? httpClient.newBuilder().readTimeout(req.timeout, TimeUnit.MILLISECONDS).build()
|
||||
: httpClient;
|
||||
Call call=client.newCall(hreq);
|
||||
synchronized(req){
|
||||
req.okhttpCall=call;
|
||||
}
|
||||
if(req.timeout>0){
|
||||
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
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(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
if(call.isCanceled())
|
||||
if(req.canceled)
|
||||
return;
|
||||
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){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
@@ -139,10 +141,10 @@ public class MastodonAPIController{
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
if(call.isCanceled())
|
||||
if(req.canceled)
|
||||
return;
|
||||
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){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
@@ -153,7 +155,7 @@ public class MastodonAPIController{
|
||||
try{
|
||||
if(BuildConfig.DEBUG){
|
||||
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)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else if(req.respClass!=null)
|
||||
@@ -175,7 +177,7 @@ public class MastodonAPIController{
|
||||
return;
|
||||
}
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@@ -184,19 +186,19 @@ public class MastodonAPIController{
|
||||
req.validateAndPostprocessResponse(respObj, response);
|
||||
}catch(IOException x){
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}else{
|
||||
try{
|
||||
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")){
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||
@@ -231,7 +233,7 @@ public class MastodonAPIController{
|
||||
});
|
||||
}catch(Exception x){
|
||||
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);
|
||||
}
|
||||
}, 0);
|
||||
@@ -244,4 +246,8 @@ public class MastodonAPIController{
|
||||
public static OkHttpClient getHttpClient(){
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private static String logTag(AccountSession session){
|
||||
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,8 +153,9 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
protected void setTimeout(long timeout){
|
||||
public MastodonAPIRequest<T> setTimeout(long timeout){
|
||||
this.timeout=timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
@@ -179,6 +180,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
if(requestBody instanceof RequestBody rb)
|
||||
return rb;
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class PushSubscriptionManager{
|
||||
deviceToken=getPrefs().getString("deviceToken", null);
|
||||
int tokenVersion=getPrefs().getInt("version", 0);
|
||||
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE){
|
||||
registerAllAccountsForPush(true); // TODO: revert this before release
|
||||
registerAllAccountsForPush(false);
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "tryRegisterFCM: no token found or app was updated. Trying to get push token...");
|
||||
|
||||
@@ -8,7 +8,10 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusMuted;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
import org.joinmastodon.android.events.ReblogDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
@@ -50,7 +53,7 @@ public class StatusInteractionController{
|
||||
runningFavoriteRequests.remove(status.id);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,13 +62,13 @@ public class StatusInteractionController{
|
||||
error.showToast(MastodonApp.context);
|
||||
status.favourited=!favorited;
|
||||
cb.accept(status);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningFavoriteRequests.put(status.id, req);
|
||||
status.favourited=favorited;
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
|
||||
@@ -80,11 +83,15 @@ public class StatusInteractionController{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status reblog){
|
||||
Status result = reblog.getContentStatus();
|
||||
Status result=reblog.getContentStatus();
|
||||
runningReblogRequests.remove(status.id);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
if(updateCounters){
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
if(reblogged) E.post(new StatusCreatedEvent(reblog, accountID));
|
||||
else E.post(new ReblogDeletedEvent(status.id, accountID));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,13 +100,13 @@ public class StatusInteractionController{
|
||||
error.showToast(MastodonApp.context);
|
||||
status.reblogged=!reblogged;
|
||||
cb.accept(status);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningReblogRequests.put(status.id, req);
|
||||
status.reblogged=reblogged;
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setBookmarked(Status status, boolean bookmarked){
|
||||
@@ -120,7 +127,7 @@ public class StatusInteractionController{
|
||||
public void onSuccess(Status result){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -129,12 +136,12 @@ public class StatusInteractionController{
|
||||
error.showToast(MastodonApp.context);
|
||||
status.bookmarked=!bookmarked;
|
||||
cb.accept(status);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningBookmarkRequests.put(status.id, req);
|
||||
status.bookmarked=bookmarked;
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
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);
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone));
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
|
||||
}
|
||||
|
||||
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 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.email=email;
|
||||
this.password=password;
|
||||
this.locale=locale;
|
||||
this.reason=reason;
|
||||
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;
|
||||
|
||||
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);
|
||||
setRequestBody(new Request(duration));
|
||||
if(muted)
|
||||
setRequestBody(new Request(duration, muteNotifications));
|
||||
else{
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public long duration;
|
||||
public Request(long duration){
|
||||
public boolean muteNotifications;
|
||||
public Request(long duration, boolean muteNotifications){
|
||||
this.duration=duration;
|
||||
this.muteNotifications=muteNotifications;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
private Uri avatar, cover;
|
||||
private File avatarFile, coverFile;
|
||||
private List<AccountField> fields;
|
||||
private Boolean discoverable, indexable;
|
||||
|
||||
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
@@ -41,6 +42,12 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
this.fields=fields;
|
||||
}
|
||||
|
||||
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
|
||||
this.discoverable=discoverable;
|
||||
this.indexable=indexable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||
@@ -58,15 +65,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
}else if(coverFile!=null){
|
||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||
}
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
}else{
|
||||
int i=0;
|
||||
for(AccountField field:fields){
|
||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||
i++;
|
||||
if(fields!=null){
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
}else{
|
||||
int i=0;
|
||||
for(AccountField field:fields){
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import java.util.List;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class AddAccountsToList extends MastodonAPIRequest<Object> {
|
||||
public AddAccountsToList(String listId, List<String> accountIds){
|
||||
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
|
||||
Request req = new Request();
|
||||
req.accountIds = accountIds;
|
||||
setRequestBody(req);
|
||||
}
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
|
||||
public static class Request{
|
||||
public List<String> accountIds;
|
||||
}
|
||||
import okhttp3.FormBody;
|
||||
|
||||
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;
|
||||
|
||||
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 CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
||||
Request req = new Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
public class CreateList extends MastodonAPIRequest<FollowList>{
|
||||
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.POST, "/lists", FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
private static class Request{
|
||||
public String title;
|
||||
public FollowList.RepliesPolicy repliesPolicy;
|
||||
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;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteList extends MastodonAPIRequest<Object> {
|
||||
public DeleteList(String id) {
|
||||
super(HttpMethod.DELETE, "/lists/" + id, Object.class);
|
||||
public class DeleteList extends ResultlessMastodonAPIRequest{
|
||||
public DeleteList(String id){
|
||||
super(HttpMethod.DELETE, "/lists/"+id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
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) {
|
||||
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 org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
|
||||
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public GetLists() {
|
||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import java.util.List;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
|
||||
public RemoveAccountsFromList(String listId, List<String> accountIds){
|
||||
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
|
||||
Request req = new Request();
|
||||
req.accountIds = accountIds;
|
||||
setRequestBody(req);
|
||||
}
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
|
||||
public static class Request{
|
||||
public List<String> accountIds;
|
||||
}
|
||||
import okhttp3.FormBody;
|
||||
|
||||
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;
|
||||
|
||||
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 UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
||||
CreateList.Request req = new CreateList.Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
public class UpdateList extends MastodonAPIRequest<FollowList>{
|
||||
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
|
||||
private String maxID;
|
||||
private final String maxID;
|
||||
public PleromaMarkNotificationsRead(String maxID) {
|
||||
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
|
||||
this.maxID = maxID;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||
|
||||
public class AkkomaTranslateStatus extends MastodonAPIRequest<AkkomaTranslation>{
|
||||
public AkkomaTranslateStatus(String id, String lang){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toUpperCase(), AkkomaTranslation.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public String quoteId;
|
||||
public ContentType contentType;
|
||||
|
||||
public boolean preview;
|
||||
|
||||
public static class Poll{
|
||||
public ArrayList<String> options=new ArrayList<>();
|
||||
public int expiresIn;
|
||||
|
||||
@@ -26,6 +26,11 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
||||
s.visibility=StatusPrivacy.PUBLIC;
|
||||
s.mentions=Collections.emptyList();
|
||||
s.tags=Collections.emptyList();
|
||||
if(s.poll!=null){
|
||||
s.poll.id="fakeID"+i;
|
||||
s.poll.emojis=Collections.emptyList();
|
||||
s.poll.ownVotes=Collections.emptyList();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.TranslatedStatus;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
|
||||
public class TranslateStatus extends MastodonAPIRequest<TranslatedStatus> {
|
||||
public TranslateStatus(String id) {
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
import java.util.Map;
|
||||
|
||||
public class TranslateStatus extends MastodonAPIRequest<Translation>{
|
||||
public TranslateStatus(String id, String lang){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/translate", Translation.class);
|
||||
setRequestBody(Map.of("lang", lang));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
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<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
@@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("remote", "true");
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(!TextUtils.isEmpty(minID))
|
||||
addQueryParameter("min_id", minID);
|
||||
if(!TextUtils.isEmpty(sinceID))
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(replyVisibility != null)
|
||||
|
||||
@@ -6,10 +6,16 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
@@ -40,16 +46,21 @@ public class AccountLocalPreferences{
|
||||
public String publishButtonText;
|
||||
public String timelineReplyVisibility; // akkoma-only
|
||||
public boolean keepOnlyLatestNotification;
|
||||
|
||||
public boolean emojiReactionsEnabled;
|
||||
public ShowEmojiReactions showEmojiReactions;
|
||||
public ColorPreference color;
|
||||
public ArrayList<Emoji> recentCustomEmoji;
|
||||
public boolean preReplySheet;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
|
||||
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.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 recentCustomEmojiType=new TypeToken<ArrayList<Emoji>>() {}.getType();
|
||||
|
||||
// MOSHIDON
|
||||
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
||||
public Map<String, Integer> recentEmojis;
|
||||
// private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
||||
// public Map<String, Integer> recentEmojis;
|
||||
private final static Type notificationFiltersType = new TypeToken<PushSubscription.Alerts>() {}.getType();
|
||||
public PushSubscription.Alerts notificationFilters;
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
|
||||
this.prefs=prefs;
|
||||
@@ -58,6 +69,7 @@ public class AccountLocalPreferences{
|
||||
revealCWs=prefs.getBoolean("revealCWs", false);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
// preReplySheet=prefs.getBoolean("preReplySheet", false);
|
||||
|
||||
// MEGALODON
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
@@ -74,9 +86,12 @@ public class AccountLocalPreferences{
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
|
||||
showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name()));
|
||||
color=prefs.contains("color") ? ColorPreference.valueOf(prefs.getString("color", null)) : null;
|
||||
recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>());
|
||||
|
||||
// MOSHIDON
|
||||
recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>());
|
||||
// recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>());
|
||||
notificationFilters=fromJson(prefs.getString("notificationFilters", gson.toJson(PushSubscription.Alerts.ofAll())), notificationFiltersType, PushSubscription.Alerts.ofAll());
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
@@ -87,6 +102,10 @@ public class AccountLocalPreferences{
|
||||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||
}
|
||||
|
||||
public ColorPreference getCurrentColor(){
|
||||
return color!=null ? color : GlobalUserPreferences.color!=null ? GlobalUserPreferences.color : ColorPreference.MATERIAL3;
|
||||
}
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
@@ -95,6 +114,9 @@ public class AccountLocalPreferences{
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
|
||||
//TODO figure this stuff out
|
||||
// .putBoolean("preReplySheet", preReplySheet)
|
||||
|
||||
// MEGALODON
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
@@ -110,12 +132,43 @@ public class AccountLocalPreferences{
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
|
||||
.putString("showEmojiReactions", showEmojiReactions.name())
|
||||
.putString("color", color!=null ? color.name() : null)
|
||||
.putString("recentCustomEmoji", gson.toJson(recentCustomEmoji))
|
||||
|
||||
// MOSHIDON
|
||||
.putString("recentEmojis", gson.toJson(recentEmojis))
|
||||
// .putString("recentEmojis", gson.toJson(recentEmojis))
|
||||
.putString("notificationFilters", gson.toJson(notificationFilters))
|
||||
.apply();
|
||||
}
|
||||
|
||||
public enum ColorPreference{
|
||||
MATERIAL3,
|
||||
PURPLE,
|
||||
PINK,
|
||||
GREEN,
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
YELLOW,
|
||||
NORD,
|
||||
WHITE;
|
||||
|
||||
public @StringRes int getName() {
|
||||
return switch(this){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
case NORD -> R.string.mo_color_palette_nord;
|
||||
case WHITE -> R.string.mo_color_palette_black_and_white;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShowEmojiReactions{
|
||||
HIDE_EMPTY,
|
||||
ONLY_OPENED,
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@@ -34,13 +35,13 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -71,6 +72,7 @@ public class AccountSession{
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
private transient List<FollowList> lists;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -219,7 +221,7 @@ public class AccountSession{
|
||||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, self.discoverable, self.source.indexable)
|
||||
new UpdateAccountCredentialsPreferences(preferences, self.locked, self.discoverable, self.source.indexable)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
@@ -255,52 +257,77 @@ public class AccountSession{
|
||||
filterStatusContainingObjects(objects, extractor, context, null);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
|
||||
private boolean statusIsOnOwnProfile(Status s, Account profile){
|
||||
return self != null && profile != null && s.account != null
|
||||
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
||||
}
|
||||
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
if(s.filtered==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(FilterResult filter:s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(wordFilters==null)
|
||||
return;
|
||||
for(T obj:objects){
|
||||
private boolean isFilteredType(Status s){
|
||||
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||
return (!localPreferences.showReplies && s.inReplyToId != null)
|
||||
|| (!localPreferences.showBoosts && s.reblog != null);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
getLocalPreferences().serverSideFiltersSupported=true;
|
||||
getLocalPreferences().save();
|
||||
return;
|
||||
localPreferences.serverSideFiltersSupported=true;
|
||||
localPreferences.save();
|
||||
break;
|
||||
}
|
||||
}
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(LegacyFilter filter:wordFilters){
|
||||
|
||||
List<T> removeUs=new ArrayList<>();
|
||||
for(int i=0; i<objects.size(); i++){
|
||||
T o=objects.get(i);
|
||||
if(filterStatusContainingObject(o, extractor, context, profile)){
|
||||
Status s=extractor.apply(o);
|
||||
removeUs.add(o);
|
||||
if(s!=null && s.hasGapAfter!=null && i>0){
|
||||
// oops, we're about to remove an item that has a gap after...
|
||||
// gotta find the previous status that's not also about to be removed
|
||||
for(int j=i-1; j>=0; j--){
|
||||
T p=objects.get(j);
|
||||
Status prev=extractor.apply(objects.get(j));
|
||||
if(prev!=null && !removeUs.contains(p)){
|
||||
prev.hasGapAfter=s.hasGapAfter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
objects.removeAll(removeUs);
|
||||
}
|
||||
|
||||
public <T> boolean filterStatusContainingObject(T object, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
Status s=extractor.apply(object);
|
||||
if(s==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if(statusIsOnOwnProfile(s, profile))
|
||||
return false;
|
||||
if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC))
|
||||
return true;
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
for(FilterResult filter : s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
}else if(wordFilters!=null){
|
||||
for(LegacyFilter filter : wordFilters){
|
||||
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void updateAccountInfo(){
|
||||
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||
}
|
||||
|
||||
public Optional<Instance> getInstance() {
|
||||
@@ -320,7 +347,11 @@ public class AccountSession{
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
public void updateAccountInfo(){
|
||||
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||
public boolean isNotificationsMentionsOnly(){
|
||||
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
|
||||
}
|
||||
|
||||
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
@@ -34,6 +36,7 @@ import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.unifiedpush.android.connector.UnifiedPush;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -93,6 +96,7 @@ public class AccountSessionManager{
|
||||
|
||||
private AccountSessionManager(){
|
||||
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");
|
||||
if(!file.exists())
|
||||
return;
|
||||
@@ -112,6 +116,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
|
||||
Context context = MastodonApp.context;
|
||||
instances.put(instance.uri, instance);
|
||||
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
|
||||
sessions.put(session.getID(), session);
|
||||
@@ -124,7 +129,14 @@ public class AccountSessionManager{
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
||||
|
||||
updateMoreInstanceInfo(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
if (!UnifiedPush.getDistributor(context).isEmpty()) {
|
||||
UnifiedPush.registerApp(
|
||||
context,
|
||||
session.getID(),
|
||||
new ArrayList<>(),
|
||||
context.getPackageName()
|
||||
);
|
||||
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
maybeUpdateShortcuts();
|
||||
@@ -205,12 +217,17 @@ public class AccountSessionManager{
|
||||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
session.getCacheController().getListsFile().delete();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
MastodonApp.context.deleteSharedPreferences(id);
|
||||
}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);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
@@ -314,8 +331,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void updateSessionLocalInfo(AccountSession session){
|
||||
/*package*/ void updateSessionLocalInfo(AccountSession session){
|
||||
new GetOwnAccount()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
|
||||
public class ListDeletedEvent {
|
||||
public final String id;
|
||||
public class ListDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
|
||||
public ListDeletedEvent(String id) {
|
||||
this.id = id;
|
||||
public ListDeletedEvent(String accountID, String listID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class ListUpdatedCreatedEvent {
|
||||
public final String id;
|
||||
public final String title;
|
||||
public final ListTimeline.RepliesPolicy repliesPolicy;
|
||||
public final FollowList.RepliesPolicy repliesPolicy;
|
||||
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.title = title;
|
||||
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,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class ReblogDeletedEvent{
|
||||
public final String statusID;
|
||||
public final String accountID;
|
||||
|
||||
public ReblogDeletedEvent(String statusID, String accountID){
|
||||
this.statusID=statusID;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
|
||||
public class ScheduledStatusDeletedEvent{
|
||||
public final String id;
|
||||
public final String accountID;
|
||||
|
||||
@@ -9,5 +9,6 @@ public class StatusCreatedEvent{
|
||||
public StatusCreatedEvent(Status status, String accountID){
|
||||
this.status=status;
|
||||
this.accountID=accountID;
|
||||
status.fromStatusCreated=true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,16 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
@@ -55,15 +52,14 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
|
||||
currentRequest=new GetAccountStatuses(user.id, getMaxID(), null, count, filter)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
boolean empty=result.isEmpty();
|
||||
boolean more=applyMaxID(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
|
||||
onDataLoaded(result, !empty);
|
||||
onDataLoaded(result, more);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
|
||||
// get unread items first
|
||||
List<Announcement> data = result.stream().filter(a -> !a.read).collect(toList());
|
||||
|
||||
@@ -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,7 +9,6 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
@@ -24,18 +23,28 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||
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.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
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.EmojiReactionsStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
@@ -51,18 +60,23 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.PreviewlessMediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -95,6 +109,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, PreviewlessMediaAttachmentViewController> previewlessAttachmentViewsPool=new TypedObjectPool<>(this::makeNewPreviewlessMediaAttachmentView);
|
||||
|
||||
protected boolean currentlyScrolling;
|
||||
protected String maxID;
|
||||
|
||||
public BaseStatusListFragment(){
|
||||
super(20);
|
||||
@@ -136,6 +151,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(T s:items){
|
||||
displayItems.addAll(buildDisplayItems(s));
|
||||
}
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,10 +173,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
if(notify)
|
||||
adapter.notifyItemRangeInserted(0, offset);
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected String getMaxID(){
|
||||
if(refreshing) return null;
|
||||
if(maxID!=null) return maxID;
|
||||
if(!preloadedData.isEmpty())
|
||||
return preloadedData.get(preloadedData.size()-1).getID();
|
||||
else if(!data.isEmpty())
|
||||
@@ -169,6 +188,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean applyMaxID(List<Status> result){
|
||||
boolean empty=result.isEmpty();
|
||||
if(!empty) maxID=result.get(result.size()-1).id;
|
||||
return !empty;
|
||||
}
|
||||
|
||||
protected abstract List<StatusDisplayItem> buildDisplayItems(T s);
|
||||
protected abstract void addAccountToKnown(T s);
|
||||
|
||||
@@ -205,7 +230,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
||||
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;
|
||||
|
||||
@Override
|
||||
@@ -271,6 +296,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
currentPhotoViewer=null;
|
||||
gridHolder.itemView.setHasTransientState(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -282,12 +308,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return gridHolder.getViewController(index);
|
||||
}
|
||||
});
|
||||
gridHolder.itemView.setHasTransientState(true);
|
||||
}
|
||||
|
||||
|
||||
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
|
||||
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;
|
||||
|
||||
@Override
|
||||
@@ -429,12 +456,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
});
|
||||
list.addItemDecoration(new StatusListItemDecoration());
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||
private Rect tmpRect=new Rect();
|
||||
@Override
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||
int lastIndex = -1, firstIndex = -1;
|
||||
if(list!=view.getParent()) return;
|
||||
boolean hasDescendant=false, hasAncestor=false, isWarning=false;
|
||||
int lastIndex=-1, firstIndex=-1;
|
||||
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
}else{
|
||||
@@ -539,10 +568,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected void updatePoll(String itemID, Status status, Poll poll){
|
||||
status.poll=poll;
|
||||
int firstOptionIndex=-1, footerIndex=-1;
|
||||
int spoilerFirstOptionIndex=-1, spoilerFooterIndex=-1;
|
||||
SpoilerStatusDisplayItem spoilerItem=null;
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.parentID.equals(itemID)){
|
||||
if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||
if(item instanceof SpoilerStatusDisplayItem){
|
||||
spoilerItem=(SpoilerStatusDisplayItem) item;
|
||||
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||
firstOptionIndex=i;
|
||||
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||
footerIndex=i;
|
||||
@@ -551,12 +584,39 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
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)
|
||||
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||
int prevSize=pollItems.size();
|
||||
if(spoilerItem!=null){
|
||||
spoilerFirstOptionIndex=spoilerItem.contentItems.indexOf(pollItems.get(0));
|
||||
spoilerFooterIndex=spoilerItem.contentItems.indexOf(pollItems.get(pollItems.size()-1));
|
||||
}
|
||||
pollItems.clear();
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems, status);
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems);
|
||||
if(spoilerItem!=null){
|
||||
spoilerItem.contentItems.subList(spoilerFirstOptionIndex, spoilerFooterIndex+1).clear();
|
||||
spoilerItem.contentItems.addAll(spoilerFirstOptionIndex, pollItems);
|
||||
}
|
||||
if(prevSize!=pollItems.size()){
|
||||
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
|
||||
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
|
||||
@@ -568,7 +628,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
||||
Poll poll=holder.getItem().poll;
|
||||
Poll.Option option=holder.getItem().option;
|
||||
if(poll.multiple || GlobalUserPreferences.voteButtonForSingleChoice){
|
||||
// MEGALODON: always show vote button
|
||||
// if(poll.multiple){
|
||||
if(poll.selectedOptions==null)
|
||||
poll.selectedOptions=new ArrayList<>();
|
||||
boolean optionContained=poll.selectedOptions.contains(option);
|
||||
@@ -583,7 +644,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(!poll.multiple && vh instanceof PollOptionStatusDisplayItem.Holder item){
|
||||
if (item != holder) item.itemView.setSelected(false);
|
||||
if(item!=holder) item.itemView.setSelected(false);
|
||||
}
|
||||
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
||||
if(footer.getItemID().equals(holder.getItemID())){
|
||||
@@ -592,9 +653,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||
}
|
||||
// }else{
|
||||
// submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||
// }
|
||||
}
|
||||
|
||||
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
|
||||
@@ -602,6 +663,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
submitPollVote(holder.getItemID(), poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public void onPollViewResultsButtonClick(PollFooterStatusDisplayItem.Holder holder, boolean shown){
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item && item.getItemID().equals(holder.getItemID())){
|
||||
item.showResults(shown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void submitPollVote(String parentID, String pollID, List<Integer> choices){
|
||||
if(refreshing)
|
||||
return;
|
||||
@@ -623,37 +692,39 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
boolean isForQuote=holder.getItem().isForQuote;
|
||||
toggleSpoiler(status, isForQuote, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
||||
Status status = holder.getItem().status;
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if (mediaGrid != null) {
|
||||
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
||||
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
if(!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
||||
else mediaGrid.hideSensitive();
|
||||
} else {
|
||||
// media grid's methods normally change the status' state - we still want to be able
|
||||
// to do this if the media grid is not bound, tho - so, doing it ourselves here
|
||||
status.sensitiveRevealed = !status.sensitiveRevealed;
|
||||
}else{
|
||||
status.sensitiveRevealed=false;
|
||||
notifyItemChangedAfter(holder.getItem(), MediaGridStatusDisplayItem.class);
|
||||
}
|
||||
holder.rebind();
|
||||
}
|
||||
|
||||
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
|
||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(header != null) header.rebind();
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null && header.getItem().hasVisibilityToggle) header.animateVisibilityToggle(true);
|
||||
else notifyItemChangedBefore(holder.getItem(), HeaderStatusDisplayItem.class);
|
||||
}
|
||||
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
protected void toggleSpoiler(Status status, boolean isForQuote, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
|
||||
status.sensitiveRevealed = false;
|
||||
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null)
|
||||
spoiler.rebind();
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
List<SpoilerStatusDisplayItem.Holder> spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0);
|
||||
if(spoiler!=null) spoiler.rebind();
|
||||
else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class);
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem());
|
||||
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
@@ -664,50 +735,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||
}
|
||||
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
if(header!=null) header.rebind();
|
||||
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
||||
if (holder.getItem().status.textExpandable != expandable && list != null) {
|
||||
holder.getItem().status.textExpandable = expandable;
|
||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if (header != null) header.rebind();
|
||||
Status s=holder.getItem().status;
|
||||
if(s.textExpandable!=expandable && list!=null) {
|
||||
s.textExpandable=expandable;
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null) header.bindCollapseButton();
|
||||
}
|
||||
}
|
||||
|
||||
public void onToggleExpanded(Status status, String itemID) {
|
||||
status.textExpanded = !status.textExpanded;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if (text != null) text.rebind();
|
||||
if (header != null) header.rebind();
|
||||
}
|
||||
|
||||
public void updateEmojiReactions(Status status, String itemID){
|
||||
EmojiReactionsStatusDisplayItem.Holder reactions=findHolderOfType(itemID, EmojiReactionsStatusDisplayItem.Holder.class);
|
||||
if(reactions != null){
|
||||
reactions.getItem().status.reactions.clear();
|
||||
reactions.getItem().status.reactions.addAll(status.reactions);
|
||||
reactions.rebind();
|
||||
}
|
||||
if(header!=null) header.animateExpandToggle();
|
||||
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
|
||||
|
||||
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.addAll(startPos, warning.filteredItems);
|
||||
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
||||
if (startPos == 0) scrollToTop();
|
||||
warning.getItem().status.filterRevealed = true;
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -724,6 +787,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
protected void loadRelationships(Set<String> ids){
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
// TODO somehow manage these and cancel outstanding requests on refresh
|
||||
@@ -755,9 +821,61 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this as a fallback if findHolderOfType fails to find the ViewHolder.
|
||||
* It might still be bound but off-screen and therefore not a child of the RecyclerView -
|
||||
* resulting in the ViewHolder displaying an outdated state once scrolled back into view.
|
||||
*/
|
||||
protected <I extends StatusDisplayItem> int notifyItemChanged(String id, Class<I> type){
|
||||
boolean encounteredParent=false;
|
||||
for(int i=0; i<displayItems.size(); i++){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
boolean idEquals=id.equals(item.parentID);
|
||||
if(!encounteredParent && idEquals) encounteredParent=true; // reached top of the parent
|
||||
else if(encounteredParent && !idEquals) break; // passed by bottom of the parent. man muss ja wissen wann schluss is
|
||||
if(idEquals && type.isInstance(item)){
|
||||
adapter.notifyItemChanged(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected <I extends StatusDisplayItem> int notifyItemChangedAfter(StatusDisplayItem afterThis, Class<I> type){
|
||||
int startIndex=displayItems.indexOf(afterThis);
|
||||
if(startIndex == -1) throw new IllegalStateException("notifyItemChangedAfter didn't find the passed StatusDisplayItem");
|
||||
String parentID=afterThis.parentID;
|
||||
for(int i=startIndex; i<displayItems.size(); i++){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
if(!parentID.equals(item.parentID)) break; // didn't find anything
|
||||
if(type.isInstance(item)){
|
||||
// found it
|
||||
adapter.notifyItemChanged(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected <I extends StatusDisplayItem> int notifyItemChangedBefore(StatusDisplayItem beforeThis, Class<I> type){
|
||||
int startIndex=displayItems.indexOf(beforeThis);
|
||||
if(startIndex == -1) throw new IllegalStateException("notifyItemChangedBefore didn't find the passed StatusDisplayItem");
|
||||
String parentID=beforeThis.parentID;
|
||||
for(int i=startIndex; i>=0; i--){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
if(!parentID.equals(item.parentID)) break; // didn't find anything
|
||||
if(type.isInstance(item)){
|
||||
// found it
|
||||
adapter.notifyItemChanged(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
for(int i=0; i<list.getChildCount(); i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
|
||||
return type.cast(holder);
|
||||
@@ -862,6 +980,99 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
public void togglePostTranslation(Status status, String itemID){
|
||||
switch(status.translationState){
|
||||
case LOADING -> {
|
||||
return;
|
||||
}
|
||||
case SHOWN -> {
|
||||
status.translationState=Status.TranslationState.HIDDEN;
|
||||
}
|
||||
case HIDDEN -> {
|
||||
if(status.translation!=null){
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
}else{
|
||||
status.translationState=Status.TranslationState.LOADING;
|
||||
Consumer<Translation> successCallback=(result)->{
|
||||
status.translation=result;
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
updateTranslation(itemID);
|
||||
};
|
||||
MastodonAPIRequest<?> req=isInstanceAkkoma()
|
||||
? new AkkomaTranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(AkkomaTranslation result){
|
||||
if(getActivity()!=null) successCallback.accept(result.toTranslation());
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()!=null) translationCallbackError(status, itemID);
|
||||
}
|
||||
})
|
||||
: new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Translation result){
|
||||
if(getActivity()!=null) successCallback.accept(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()!=null) translationCallbackError(status, itemID);
|
||||
}
|
||||
});
|
||||
|
||||
// 1 minute
|
||||
req.setTimeout(60000).exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTranslation(itemID);
|
||||
}
|
||||
|
||||
private void translationCallbackError(Status status, String itemID) {
|
||||
status.translationState=Status.TranslationState.HIDDEN;
|
||||
updateTranslation(itemID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.translation_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updateTranslation(String itemID) {
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
text.updateTranslation(true);
|
||||
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
|
||||
}else{
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
}
|
||||
|
||||
if(isInstanceAkkoma())
|
||||
return;
|
||||
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null){
|
||||
spoiler.rebind();
|
||||
}
|
||||
|
||||
MediaGridStatusDisplayItem.Holder media=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
|
||||
if (media!=null) {
|
||||
media.rebind();
|
||||
}
|
||||
|
||||
PreviewlessMediaGridStatusDisplayItem.Holder previewLessMedia=findHolderOfType(itemID, PreviewlessMediaGridStatusDisplayItem.Holder.class);
|
||||
if (previewLessMedia!=null) {
|
||||
previewLessMedia.rebind();
|
||||
}
|
||||
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
|
||||
item.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
@@ -870,6 +1081,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
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){}
|
||||
|
||||
@Override
|
||||
@@ -877,7 +1108,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
if(getContext()==null) return;
|
||||
super.onDataLoaded(d, more);
|
||||
// more available, but the page isn't even full yet? seems wrong, let's load some more
|
||||
if(more && d.size() < itemsPerPage){
|
||||
if(more && data.size() < itemsPerPage){
|
||||
preloader.onScrolledToLastItem();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
||||
@@ -7,6 +7,9 @@ import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDra
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.DatePickerDialog;
|
||||
@@ -75,7 +78,7 @@ import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
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.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
@@ -123,11 +126,11 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -135,12 +138,14 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID, CustomTransitionsFragment {
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
@@ -165,7 +170,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
public LinearLayout mainLayout;
|
||||
private SizeListenerLinearLayout contentView;
|
||||
private TextView selfName, selfUsername, selfExtraText, extraText, pronouns;
|
||||
private TextView selfName, selfUsername, selfExtraText, extraText;
|
||||
private ImageView selfAvatar;
|
||||
private Account self;
|
||||
private String instanceDomain;
|
||||
@@ -176,7 +181,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private int charCount, charLimit, trimmedCharCount;
|
||||
|
||||
private Button publishButton, languageButton, scheduleTimeBtn;
|
||||
private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
|
||||
private PopupMenu contentTypePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton publishButtonRelocated, mediaBtn, pollBtn, emojiBtn, spoilerBtn, draftsBtn, scheduleDraftDismiss, contentTypeBtn;
|
||||
private View sensitiveBtn;
|
||||
private TextView replyText;
|
||||
@@ -215,7 +220,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public Instance instance;
|
||||
|
||||
public Status editingStatus;
|
||||
private ScheduledStatus scheduledStatus;
|
||||
public ScheduledStatus scheduledStatus;
|
||||
private boolean redraftStatus;
|
||||
|
||||
private Uri photoUri;
|
||||
@@ -312,7 +317,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
creatingView=true;
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain, getAccountID());
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), accountID, customEmojis, instanceDomain);
|
||||
emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){
|
||||
@Override
|
||||
public void onEmojiSelected(Emoji emoji){
|
||||
@@ -364,7 +369,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
selfUsername=view.findViewById(R.id.self_username);
|
||||
selfAvatar=view.findViewById(R.id.self_avatar);
|
||||
selfExtraText=view.findViewById(R.id.self_extra_text);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.getDisplayName(), self.emojis);
|
||||
selfUsername.setText('@'+self.username+'@'+instanceDomain);
|
||||
if(self.avatar!=null)
|
||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||
@@ -527,7 +532,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
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();
|
||||
@@ -685,7 +690,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
});
|
||||
View originalPost=view.findViewById(R.id.original_post);
|
||||
extraText=view.findViewById(R.id.extra_text);
|
||||
pronouns=view.findViewById(R.id.pronouns);
|
||||
originalPost.setVisibility(View.VISIBLE);
|
||||
originalPost.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
@@ -725,7 +729,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
moreBtn.setBackground(null);
|
||||
|
||||
TextView name = view.findViewById(R.id.name);
|
||||
name.setText(HtmlParser.parseCustomEmoji(status.account.displayName, status.account.emojis));
|
||||
name.setText(HtmlParser.parseCustomEmoji(status.account.getDisplayName(), status.account.emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(name);
|
||||
|
||||
String time = status==null || status.editedAt==null
|
||||
@@ -759,7 +763,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
|
||||
}
|
||||
|
||||
replyText.setText(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.displayName));
|
||||
replyText.setText(HtmlParser.parseCustomEmoji(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.getDisplayName()), status.account.emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(replyText);
|
||||
int visibilityNameRes = switch (status.visibility) {
|
||||
case PUBLIC -> R.string.visibility_public;
|
||||
case UNLISTED -> R.string.sk_visibility_unlisted;
|
||||
@@ -767,7 +772,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
case DIRECT -> R.string.visibility_private;
|
||||
case LOCAL -> R.string.sk_local_only;
|
||||
};
|
||||
replyText.setContentDescription(getString(R.string.in_reply_to, status.account.displayName) + ", " + getString(visibilityNameRes));
|
||||
replyText.setContentDescription(getString(R.string.in_reply_to, status.account.getDisplayName()) + ", " + getString(visibilityNameRes));
|
||||
replyText.setOnClickListener(v->{
|
||||
scrollView.smoothScrollTo(0, 0);
|
||||
});
|
||||
@@ -801,7 +806,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|
||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
|
||||
&& !status.spoilerText.startsWith("re: ") ? "re: " : "";
|
||||
spoilerEdit.setText(prefix + replyTo.spoilerText);
|
||||
spoilerEdit.setText(prefix + status.spoilerText);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
|
||||
@@ -862,6 +867,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu);
|
||||
@@ -889,19 +895,22 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
charCounter.setText(String.valueOf(charLimit));
|
||||
}
|
||||
|
||||
// draftsBtn = wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
|
||||
// draftsBtn=wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup=new PopupMenu(getContext(), draftsBtn);
|
||||
draftOptionsPopup.inflate(R.menu.compose_more);
|
||||
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
|
||||
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
|
||||
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
|
||||
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
|
||||
Menu draftOptionsMenu=draftOptionsPopup.getMenu();
|
||||
draftMenuItem=draftOptionsMenu.findItem(R.id.draft);
|
||||
undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft);
|
||||
scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule);
|
||||
unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule);
|
||||
draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma());
|
||||
draftOptionsPopup.setOnMenuItemClickListener(i->{
|
||||
int id = i.getItemId();
|
||||
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if (id == R.id.schedule) pickScheduledDateTime();
|
||||
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
|
||||
else navigateToUnsentPosts();
|
||||
int id=i.getItemId();
|
||||
if(id==R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if(id==R.id.schedule) pickScheduledDateTime();
|
||||
else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null);
|
||||
else if(id==R.id.drafts) navigateToUnsentPosts();
|
||||
else if(id==R.id.preview) publish(true);
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
|
||||
@@ -909,23 +918,37 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
languageButton = wrap.findViewById(R.id.language_btn);
|
||||
languageButton.setOnClickListener(v->showLanguageAlert());
|
||||
languageButton.setOnLongClickListener(v->{
|
||||
if(!getLocalPrefs().bottomEncoding){
|
||||
getLocalPrefs().bottomEncoding=true;
|
||||
getLocalPrefs().save();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!GlobalUserPreferences.relocatePublishButton)
|
||||
publishButton.post(()->publishButton.setMinimumWidth(publishButton.getWidth()));
|
||||
|
||||
if(GlobalUserPreferences.relocatePublishButton){
|
||||
publishButtonRelocated.setOnClickListener(v -> {
|
||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
});
|
||||
} else {
|
||||
publishButton.setOnClickListener(v -> {
|
||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
});
|
||||
}
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setOnClickListener(v->{
|
||||
Consumer<Boolean> draftCheckComplete=(isDraft)->{
|
||||
if(GlobalUserPreferences.altTextReminders && !isDraft) checkAltTextsAndPublish();
|
||||
else publish();
|
||||
};
|
||||
|
||||
boolean isAlreadyDraft=scheduledAt!=null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT);
|
||||
if(editingStatus!=null && scheduledAt!=null && isAlreadyDraft) {
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_save_draft)
|
||||
.setMessage(R.string.sk_save_draft_message)
|
||||
.setPositiveButton(R.string.save, (d, w)->draftCheckComplete.accept(isAlreadyDraft))
|
||||
.setNegativeButton(R.string.publish, (d, w)->{
|
||||
updateScheduledAt(null);
|
||||
draftCheckComplete.accept(false);
|
||||
})
|
||||
.show();
|
||||
}else{
|
||||
draftCheckComplete.accept(isAlreadyDraft);
|
||||
}
|
||||
});
|
||||
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
|
||||
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
|
||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
||||
@@ -936,8 +959,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
? languageResolver.fromOrFallback(prefs.postingDefaultLanguage)
|
||||
: languageResolver.getDefault());
|
||||
|
||||
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
|
||||
if(isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||
if(isInstancePixelfed() || (editingStatus!=null && !redraftStatus)) {
|
||||
// editing an already published post
|
||||
draftsBtn.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -1068,7 +1091,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
protected int getNavigationIconDrawableResource(){
|
||||
return R.drawable.ic_baseline_close_24;
|
||||
return R.drawable.ic_fluent_dismiss_24_regular;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1127,6 +1150,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void publish(){
|
||||
publish(false);
|
||||
}
|
||||
|
||||
private void publish(boolean preview){
|
||||
sendingOverlay=new View(getActivity());
|
||||
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
||||
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
|
||||
@@ -1137,31 +1164,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
overlayParams.token=mainEditText.getWindowToken();
|
||||
wm.addView(sendingOverlay, overlayParams);
|
||||
|
||||
if(GlobalUserPreferences.relocatePublishButton){
|
||||
publishButtonRelocated.setEnabled(false);
|
||||
} else {
|
||||
publishButton.setEnabled(false);
|
||||
}
|
||||
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||
|
||||
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
|
||||
mediaViewController.saveAltTextsBeforePublishing(
|
||||
()->actuallyPublish(preview),
|
||||
this::handlePublishError);
|
||||
}
|
||||
|
||||
private void actuallyPublish(){
|
||||
actuallyPublish(false);
|
||||
}
|
||||
private void actuallyPublish(boolean force){
|
||||
private void actuallyPublish(boolean preview){
|
||||
String text=mainEditText.getText().toString();
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
if ("bottom".equals(postLang.encoding)) {
|
||||
text = new StatusTextEncoder(Bottom::encode).encode(text);
|
||||
req.spoilerText = "bottom-encoded emoji spam";
|
||||
if("bottom".equals(postLang.encoding)){
|
||||
text=new StatusTextEncoder(Bottom::encode).encode(text);
|
||||
req.spoilerText="bottom-encoded emoji spam";
|
||||
}
|
||||
if (localOnly &&
|
||||
if(localOnly &&
|
||||
AccountSessionManager.get(accountID).getLocalPreferences().glitchInstance &&
|
||||
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) {
|
||||
text += " " + GLITCH_LOCAL_ONLY_SUFFIX;
|
||||
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()){
|
||||
text+=" "+GLITCH_LOCAL_ONLY_SUFFIX;
|
||||
}
|
||||
req.status=text;
|
||||
req.localOnly=localOnly;
|
||||
@@ -1169,25 +1190,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
req.sensitive=sensitive;
|
||||
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
|
||||
req.scheduledAt=scheduledAt;
|
||||
req.preview=preview;
|
||||
if(!mediaViewController.isEmpty()){
|
||||
req.mediaIds=mediaViewController.getAttachmentIDs();
|
||||
if(editingStatus != null){
|
||||
req.mediaAttributes=mediaViewController.getAttachmentAttributes();
|
||||
}
|
||||
}
|
||||
// ask whether to publish now when editing an existing draft
|
||||
if (!force && editingStatus != null && scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_save_draft)
|
||||
.setMessage(R.string.sk_save_draft_message)
|
||||
.setPositiveButton(R.string.save, (d, w) -> actuallyPublish(true))
|
||||
.setNegativeButton(R.string.publish, (d, w) -> {
|
||||
updateScheduledAt(null);
|
||||
actuallyPublish();
|
||||
})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
|
||||
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
|
||||
}
|
||||
@@ -1209,7 +1218,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Callback<Status> resCallback=new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
maybeDeleteScheduledPost(() -> {
|
||||
if(preview){
|
||||
openPreview(result);
|
||||
return;
|
||||
}
|
||||
|
||||
maybeDeleteScheduledPost(()->{
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null || redraftStatus){
|
||||
@@ -1231,10 +1245,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
E.post(new StatusUpdatedEvent(editedStatus));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()){
|
||||
Nav.finish(ComposeFragment.this);
|
||||
}
|
||||
if (getArguments().getBoolean("navigateToStatus", false)) {
|
||||
if(getArguments().getBoolean("navigateToStatus", false)){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
@@ -1250,11 +1264,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
};
|
||||
|
||||
if(editingStatus!=null && !redraftStatus){
|
||||
if(editingStatus!=null && !redraftStatus && !preview){
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}else if(req.scheduledAt == null){
|
||||
}else if(req.scheduledAt == null || preview){
|
||||
new CreateStatus(req, uuid)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
@@ -1284,7 +1298,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
.setPositiveButton(R.string.ok, (a, b)->{})
|
||||
.show();
|
||||
handlePublishError(null);
|
||||
publishButton.setEnabled(false);
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||
}
|
||||
|
||||
if (replyTo == null) updateRecentLanguages();
|
||||
@@ -1294,13 +1308,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||
|
||||
if(GlobalUserPreferences.relocatePublishButton) {
|
||||
publishButtonRelocated.setEnabled(true);
|
||||
} else {
|
||||
publishButton.setEnabled(true);
|
||||
}
|
||||
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
|
||||
if(error instanceof MastodonErrorResponse me){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.post_failed)
|
||||
@@ -1313,6 +1321,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
private void openPreview(Status result){
|
||||
result.preview=true;
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
|
||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
if(replyTo!=null){
|
||||
args.putParcelable("inReplyTo", Parcels.wrap(replyTo));
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo.account));
|
||||
}
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
|
||||
private void updateRecentLanguages() {
|
||||
if (postLang == null || postLang.language == null) return;
|
||||
String language = postLang.language.getLanguage();
|
||||
@@ -1388,20 +1415,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void confirmDiscardDraftAndFinish(){
|
||||
boolean attachmentsPending = mediaViewController.areAnyAttachmentsNotDone();
|
||||
if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
||||
boolean attachmentsPending=mediaViewController.areAnyAttachmentsNotDone();
|
||||
if(attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_unfinished_attachments)
|
||||
.setMessage(R.string.sk_unfinished_attachments_message)
|
||||
.setPositiveButton(R.string.edit, (d, w) -> {})
|
||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
||||
.setPositiveButton(R.string.ok, (d, w)->{})
|
||||
.setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
|
||||
.show();
|
||||
else new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||
.setPositiveButton(R.string.save, (d, w) -> {
|
||||
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
|
||||
.setTitle(editingStatus!=null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||
.setPositiveButton(R.string.save, (d, w)->{
|
||||
updateScheduledAt(scheduledAt==null ? getDraftInstant() : scheduledAt);
|
||||
publish();
|
||||
})
|
||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
||||
.setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
|
||||
.show();
|
||||
}
|
||||
|
||||
@@ -1528,7 +1555,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
public void updateSensitive() {
|
||||
sensitiveBtn.setVisibility(View.GONE);
|
||||
if (!mediaViewController.isEmpty() && !hasSpoiler) sensitiveBtn.setVisibility(View.VISIBLE);
|
||||
if (!mediaViewController.isEmpty()) sensitiveBtn.setVisibility(View.VISIBLE);
|
||||
if (mediaViewController.isEmpty()) sensitive = false;
|
||||
}
|
||||
|
||||
@@ -1539,8 +1566,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
.withMinute(0);
|
||||
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
|
||||
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
|
||||
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
|
||||
.toInstant(OffsetDateTime.now().getOffset()));
|
||||
LocalDateTime at=LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute);
|
||||
updateScheduledAt(at.toInstant(ZoneId.systemDefault().getRules().getOffset(at)));
|
||||
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
|
||||
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
|
||||
}
|
||||
@@ -1606,8 +1633,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void updateHeaders() {
|
||||
UiUtils.setExtraTextInfo(getContext(), selfExtraText, null, false, false, localOnly || statusVisibility==StatusPrivacy.LOCAL, null);
|
||||
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, pronouns, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
|
||||
UiUtils.setExtraTextInfo(getContext(), selfExtraText, false, false, localOnly, null);
|
||||
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
|
||||
}
|
||||
|
||||
private void buildVisibilityPopup(View v){
|
||||
@@ -1635,7 +1662,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
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()) m.setGroupDividerEnabled(true);
|
||||
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item){
|
||||
@@ -1679,6 +1706,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
contentTypePopup.setOnMenuItemClickListener(i->{
|
||||
uuid=null;
|
||||
int index=i.getItemId();
|
||||
contentType=ContentType.values()[index];
|
||||
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());
|
||||
@@ -1783,8 +1811,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
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
|
||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||
description = sanitizeMediaDescription(description);
|
||||
return mediaViewController.addMediaAttachment(uri, description);
|
||||
}
|
||||
|
||||
@@ -1827,6 +1873,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Editable e=mainEditText.getText();
|
||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||
if(start==-1 || end==-1)
|
||||
return;
|
||||
e.replace(start, end, text+" ");
|
||||
finishAutocomplete();
|
||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||
@@ -1895,4 +1943,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
languageButton.setText(opt.language.getLanguageName());
|
||||
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.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -26,12 +23,12 @@ import android.widget.ImageView;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.utils.ColorPalette;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@@ -54,16 +51,17 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
attachmentID=getArguments().getString("attachment");
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
accountID=getArguments().getString("account");
|
||||
attachmentID=getArguments().getString("attachment");
|
||||
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
|
||||
ColorPalette.palettes.get(GlobalUserPreferences.color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
|
||||
ColorPalette.palettes.get(AccountSessionManager.get(accountID).getLocalPreferences().getCurrentColor())
|
||||
.apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
|
||||
setTitle(R.string.add_alt_text);
|
||||
}
|
||||
|
||||
@@ -134,20 +132,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
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)
|
||||
.setTitle(R.string.what_is_alt_text)
|
||||
.setMessage(msg)
|
||||
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
@@ -184,7 +171,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
fakeAttachment.meta.width=width;
|
||||
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
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
||||
|
||||
@Override
|
||||
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){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
@@ -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.session.AccountLocalPreferences;
|
||||
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.FollowList;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -67,86 +64,86 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines = new ArrayList<>();
|
||||
private final List<Hashtag> hashtags = new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
||||
private final List<FollowList> followLists =new ArrayList<>();
|
||||
private final List<Hashtag> hashtags=new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
||||
|
||||
public EditTimelinesFragment() {
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
|
||||
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
public EditTimelinesFragment(){
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback();
|
||||
itemTouchHelper=new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID = getArguments().getString("account");
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID=getArguments().getString("account");
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result) {
|
||||
listTimelines.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
new GetLists().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
followLists.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result) {
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
new GetFollowedHashtags().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
||||
}
|
||||
@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);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
this.optionsMenu = menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
this.optionsMenu=menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_add_local_timelines) {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.menu_back){
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_add_local_timelines) {
|
||||
addNewLocalTimeline();
|
||||
return true;
|
||||
}
|
||||
@@ -161,14 +158,14 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addTimeline(TimelineDefinition tl) {
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
private void addTimeline(TimelineDefinition tl){
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
private void addNewLocalTimeline() {
|
||||
private void addNewLocalTimeline() {
|
||||
FrameLayout inputWrap = new FrameLayout(getContext());
|
||||
EditText input = new EditText(getContext());
|
||||
input.setHint(R.string.sk_example_domain);
|
||||
@@ -194,92 +191,92 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
timelineByMenuItem.put(item, tl);
|
||||
}
|
||||
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
|
||||
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
|
||||
MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void updateOptionsMenu() {
|
||||
if (getActivity() == null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
private void updateOptionsMenu(){
|
||||
if(getActivity()==null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
|
||||
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
SubMenu menu=optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
|
||||
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
|
||||
MenuItem addLocalTimelines = menu.add(0, R.id.menu_add_local_timelines, NONE, R.string.local_timeline);
|
||||
MenuItem addLocalTimelines = menu.add(0, R.id.menu_add_local_timelines, NONE, R.string.local_timeline);
|
||||
addLocalTimelines.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
|
||||
makeBackItem(timelinesMenu);
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.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);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
||||
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);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size() > 0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0);
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size()>0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size()>0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0);
|
||||
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
|
||||
private void saveTimelines() {
|
||||
updated=true;
|
||||
private void saveTimelines(){
|
||||
updated=true;
|
||||
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
||||
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
|
||||
prefs.timelines=data;
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
private void removeTimeline(int position) {
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
private void removeTimeline(int position){
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
|
||||
return adapter = new TimelinesAdapter();
|
||||
}
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter(){
|
||||
return adapter=new TimelinesAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (updated) UiUtils.restartApp();
|
||||
}
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(updated) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
|
||||
if (tags == null || tags.isEmpty()) return false;
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags){
|
||||
if(tags==null || tags.isEmpty()) return false;
|
||||
editText.setText(tags);
|
||||
editText.chipifyAllUnterminatedTokens();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho) {
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho){
|
||||
//I’ll Be Back
|
||||
nacho.setChipTerminators(
|
||||
Map.of(
|
||||
@@ -289,223 +286,228 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
';', BEHAVIOR_CHIPIFY_ALL
|
||||
)
|
||||
);
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) {
|
||||
Context ctx = getContext();
|
||||
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove){
|
||||
Context ctx=getContext();
|
||||
View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
|
||||
View divider = view.findViewById(R.id.divider);
|
||||
Button advancedBtn = view.findViewById(R.id.advanced);
|
||||
EditText editText = view.findViewById(R.id.input);
|
||||
if (item != null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
View divider=view.findViewById(R.id.divider);
|
||||
Button advancedBtn=view.findViewById(R.id.advanced);
|
||||
EditText editText=view.findViewById(R.id.input);
|
||||
if(item!=null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
|
||||
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
|
||||
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l -> {
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
LinearLayout tagWrap=view.findViewById(R.id.tag_wrap);
|
||||
boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l->{
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
|
||||
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) view);
|
||||
});
|
||||
});
|
||||
|
||||
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only)
|
||||
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
Switch localOnlySwitch=view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
|
||||
EditText tagMain = view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
if (item != null) {
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if (item.isHashtagLocalOnly()) {
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced = true;
|
||||
}
|
||||
if (hasAdvanced) {
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
EditText tagMain=view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
|
||||
if(item!=null && hashtagOptionsAvailable){
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if(item.isHashtagLocalOnly()){
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced=true;
|
||||
}
|
||||
if(hasAdvanced){
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
tagWrap.setVisibility(View.VISIBLE);
|
||||
divider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton btn = view.findViewById(R.id.button);
|
||||
PopupMenu popup = new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l -> popup.show());
|
||||
ImageButton btn=view.findViewById(R.id.button);
|
||||
PopupMenu popup=new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l->popup.show());
|
||||
|
||||
Menu menu = popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if (!currentIcon.equals(defaultIcon)) {
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
|
||||
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
Menu menu=popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if(!currentIcon.equals(defaultIcon)){
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){
|
||||
if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
popup.setOnMenuItemClickListener(menuItem->{
|
||||
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
|
||||
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
String name = editText.getText().toString().trim();
|
||||
String mainHashtag = tagMain.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(mainHashtag)) {
|
||||
mainHashtag = name;
|
||||
name = null;
|
||||
}
|
||||
if (TextUtils.isEmpty(mainHashtag) && (item != null && item.getType() == TimelineDefinition.TimelineType.HASHTAG)) {
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which)->{
|
||||
String name=editText.getText().toString().trim();
|
||||
|
||||
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
tl.setTagOptions(
|
||||
mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {});
|
||||
String mainHashtag=tagMain.getText().toString().trim();
|
||||
if(item != null && item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
if(TextUtils.isEmpty(mainHashtag)){
|
||||
mainHashtag=name;
|
||||
name=null;
|
||||
}
|
||||
if(TextUtils.isEmpty(mainHashtag) && (item!=null && item.getType()==TimelineDefinition.TimelineType.HASHTAG)){
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run());
|
||||
TimelineDefinition tl=item!=null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
if(item == null || item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||
tl.setTagOptions(
|
||||
mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
}
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which)->{});
|
||||
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run());
|
||||
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item) {
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
|
||||
private void onSave(TimelineDefinition tl) {
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item){
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event)->{
|
||||
if(event.getAction()==MotionEvent.ACTION_DOWN){
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void onRemove() {
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
private void onSave(TimelineDefinition tl){
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick() {
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
private void onRemove(){
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
|
||||
public ItemTouchHelperCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick(){
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition = target.getAbsoluteAdapterPosition();
|
||||
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
|
||||
return false;
|
||||
} else {
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{
|
||||
public ItemTouchHelperCallback(){
|
||||
super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
|
||||
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition=target.getAbsoluteAdapterPosition();
|
||||
if(Math.max(fromPosition, toPosition)>=data.size() || Math.min(fromPosition, toPosition)<0){
|
||||
return false;
|
||||
}else{
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
|
||||
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
|
||||
int position=viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
||||
@@ -45,8 +45,8 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String hashtag){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, hashtag, data.stream().filter(h -> Objects.equals(h.name, hashtag)).findAny().map(h -> h.following).orElse(null));
|
||||
public void onItemClick(String id){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -83,7 +82,7 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
@@ -271,7 +270,7 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
||||
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||
relationship=relationships.get(item.account.id);
|
||||
UiUtils.setExtraTextInfo(getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account);
|
||||
UiUtils.setExtraTextInfo(getContext(), null, true, false, false, item.account);
|
||||
|
||||
if(relationship==null || !relationship.followedBy){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
@@ -366,9 +365,9 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
||||
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(account.emojis.isEmpty()){
|
||||
parsedName=account.displayName;
|
||||
parsedName= account.getDisplayName();
|
||||
}else{
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
|
||||
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag>
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
@@ -122,7 +122,7 @@ public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag>
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.filters.CreateFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.DeleteFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.GetFilters;
|
||||
import org.joinmastodon.android.api.requests.tags.GetHashtag;
|
||||
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
|
||||
import org.joinmastodon.android.api.requests.tags.GetTag;
|
||||
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
||||
import org.joinmastodon.android.events.HashtagUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.EditFilterFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
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.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -42,17 +52,29 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
private String hashtag;
|
||||
public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
||||
private Hashtag hashtag;
|
||||
private String hashtagName;
|
||||
private TextView headerTitle, headerSubtitle;
|
||||
private ProgressBarButton followButton;
|
||||
private ProgressBar followProgress;
|
||||
private MenuItem followMenuItem, pinMenuItem, muteMenuItem;
|
||||
private boolean followRequestRunning;
|
||||
private boolean toolbarContentVisible;
|
||||
private String maxID;
|
||||
|
||||
private List<String> any;
|
||||
private List<String> all;
|
||||
private List<String> none;
|
||||
private boolean following;
|
||||
private boolean localOnly;
|
||||
private MenuItem followButton;
|
||||
private MenuItem muteButton;
|
||||
private Menu optionsMenu;
|
||||
private MenuInflater optionsMenuInflater;
|
||||
|
||||
private Optional<Filter> filter = Optional.empty();
|
||||
|
||||
@Override
|
||||
@@ -60,100 +82,27 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
updateTitle(getArguments().getString("hashtag"));
|
||||
following=getArguments().getBoolean("following", false);
|
||||
localOnly=getArguments().getBoolean("localOnly", false);
|
||||
any=getArguments().getStringArrayList("any");
|
||||
all=getArguments().getStringArrayList("all");
|
||||
none=getArguments().getStringArrayList("none");
|
||||
if(getArguments().containsKey("hashtag")){
|
||||
hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag"));
|
||||
hashtagName=hashtag.name;
|
||||
}else{
|
||||
hashtagName=getArguments().getString("hashtagName");
|
||||
}
|
||||
setTitle('#'+hashtagName);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
private void updateTitle(String hashtagName) {
|
||||
hashtag = hashtagName;
|
||||
setTitle('#'+hashtag);
|
||||
}
|
||||
|
||||
private void updateFollowingState(boolean newFollowing) {
|
||||
this.following = newFollowing;
|
||||
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
|
||||
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
|
||||
E.post(new HashtagUpdatedEvent(hashtag, following));
|
||||
}
|
||||
|
||||
private void updateMuteState(boolean newMute) {
|
||||
muteButton.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
|
||||
muteButton.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.hashtag_timeline, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
followButton = menu.findItem(R.id.follow_hashtag);
|
||||
updateFollowingState(following);
|
||||
muteButton = menu.findItem(R.id.mute_hashtag);
|
||||
updateMuteState(filter.isPresent());
|
||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
if (getActivity() == null) return;
|
||||
updateTitle(hashtag.name);
|
||||
updateFollowingState(hashtag.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFilters().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Filter> filters) {
|
||||
if (getActivity() == null) return;
|
||||
filter=filters.stream().filter(filter->filter.title.equals("#"+hashtag)).findAny();
|
||||
updateMuteState(filter.isPresent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (super.onOptionsItemSelected(item)) return true;
|
||||
if (item.getItemId() == R.id.follow_hashtag) {
|
||||
updateFollowingState(!following);
|
||||
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag i) {
|
||||
if (getActivity() == null) return;
|
||||
if (i.following == following) Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
|
||||
updateFollowingState(i.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
updateFollowingState(!following);
|
||||
}
|
||||
}).exec(accountID);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.mute_hashtag) {
|
||||
showMuteDialog(filter.isPresent());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
|
||||
muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
}
|
||||
|
||||
private void showMuteDialog(boolean mute) {
|
||||
@@ -184,8 +133,8 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
private void muteHashtag() {
|
||||
FilterKeyword hashtagFilter=new FilterKeyword();
|
||||
hashtagFilter.wholeWord=true;
|
||||
hashtagFilter.keyword="#"+hashtag;
|
||||
new CreateFilter("#"+hashtag, EnumSet.of(FilterContext.HOME), FilterAction.HIDE, 0 , List.of(hashtagFilter)).setCallback(new Callback<>(){
|
||||
hashtagFilter.keyword="#"+hashtagName;
|
||||
new CreateFilter("#"+hashtagName, EnumSet.of(FilterContext.HOME), FilterAction.HIDE, 0 , List.of(hashtagFilter)).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Filter result){
|
||||
filter=Optional.of(result);
|
||||
@@ -203,18 +152,19 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
@Override
|
||||
protected TimelineDefinition makeTimelineDefinition() {
|
||||
return TimelineDefinition.ofHashtag(hashtag);
|
||||
return TimelineDefinition.ofHashtag(hashtagName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
|
||||
currentRequest=new GetHashtagTimeline(hashtagName, getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
if(getActivity()==null) return;
|
||||
boolean more=applyMaxID(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||
onDataLoaded(result, more);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -227,16 +177,46 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(){
|
||||
reloadTag();
|
||||
super.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, '#'+hashtag+' ');
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtagName+' ');
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("prefilledText", '#'+hashtag+' ');
|
||||
args.putString("prefilledText", '#'+hashtagName+' ');
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -252,6 +232,219 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
|
||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtag).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
View header=getActivity().getLayoutInflater().inflate(R.layout.header_hashtag_timeline, list, false);
|
||||
headerTitle=header.findViewById(R.id.title);
|
||||
headerSubtitle=header.findViewById(R.id.subtitle);
|
||||
followButton=header.findViewById(R.id.profile_action_btn);
|
||||
followProgress=header.findViewById(R.id.action_progress);
|
||||
|
||||
headerTitle.setText("#"+hashtagName);
|
||||
followButton.setVisibility(View.GONE);
|
||||
followButton.setOnClickListener(v->{
|
||||
if(hashtag==null)
|
||||
return;
|
||||
setFollowed(!hashtag.following);
|
||||
});
|
||||
followButton.setOnLongClickListener(v->{
|
||||
if(hashtag==null) return false;
|
||||
UiUtils.pickAccount(getActivity(), accountID, R.string.sk_follow_as, R.drawable.ic_fluent_person_add_28_regular, session -> {
|
||||
new SetTagFollowed(hashtagName, true).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
Toast.makeText(
|
||||
getActivity(),
|
||||
getString(R.string.sk_followed_as, session.self.getShortUsername()),
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(session.getID());
|
||||
}, null);
|
||||
return true;
|
||||
});
|
||||
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.hashtag_timeline, optionsMenu);
|
||||
followMenuItem=optionsMenu.findItem(R.id.follow_hashtag);
|
||||
pinMenuItem=optionsMenu.findItem(R.id.pin);
|
||||
followMenuItem.setVisible(toolbarContentVisible);
|
||||
// pinMenuItem.setShowAsAction(toolbarContentVisible ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
super.updatePinButton(pinMenuItem);
|
||||
|
||||
muteMenuItem = optionsMenu.findItem(R.id.mute_hashtag);
|
||||
updateMuteState(filter.isPresent());
|
||||
new GetFilters().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Filter> filters) {
|
||||
if (getActivity() == null) return;
|
||||
filter=filters.stream().filter(filter->filter.title.equals("#"+hashtagName)).findAny();
|
||||
updateMuteState(filter.isPresent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePinButton(MenuItem pin){
|
||||
super.updatePinButton(pin);
|
||||
if(toolbarContentVisible) UiUtils.insetPopupMenuIcon(getContext(), pin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.hashtag_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.follow_hashtag && hashtag!=null) {
|
||||
setFollowed(!hashtag.following);
|
||||
} else if (item.getItemId() == R.id.mute_hashtag) {
|
||||
showMuteDialog(filter.isPresent());
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f);
|
||||
createOptionsMenu();
|
||||
}
|
||||
|
||||
private void updateHeader(){
|
||||
if(hashtag==null || getActivity()==null)
|
||||
return;
|
||||
|
||||
if(hashtag.history!=null && !hashtag.history.isEmpty()){
|
||||
int weekPosts=hashtag.history.stream().mapToInt(h->h.uses).sum();
|
||||
int todayPosts=hashtag.history.get(0).uses;
|
||||
int numAccounts=hashtag.history.stream().mapToInt(h->h.accounts).sum();
|
||||
int hSpace=V.dp(8);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
ssb.append(getResources().getQuantityString(R.plurals.x_posts, weekPosts, weekPosts));
|
||||
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
|
||||
ssb.append('·');
|
||||
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
|
||||
ssb.append(getResources().getQuantityString(R.plurals.x_participants, numAccounts, numAccounts));
|
||||
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
|
||||
ssb.append('·');
|
||||
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
|
||||
ssb.append(getResources().getQuantityString(R.plurals.x_posts_today, todayPosts, todayPosts));
|
||||
headerSubtitle.setText(ssb);
|
||||
}
|
||||
|
||||
int styleRes;
|
||||
followButton.setVisibility(View.VISIBLE);
|
||||
if(hashtag.following){
|
||||
followButton.setText(R.string.button_following);
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
|
||||
}else{
|
||||
followButton.setText(R.string.button_follow);
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
TypedArray ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
followButton.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
followButton.setTextColor(ta.getColorStateList(0));
|
||||
followProgress.setIndeterminateTintList(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
|
||||
followButton.setTextVisible(true);
|
||||
followProgress.setVisibility(View.GONE);
|
||||
if(followMenuItem!=null){
|
||||
followMenuItem.setTitle(getString(hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
|
||||
followMenuItem.setIcon(hashtag.following ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
|
||||
}
|
||||
if(muteMenuItem!=null){
|
||||
muteMenuItem.setTitle(getString(filter.isPresent() ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
|
||||
muteMenuItem.setIcon(filter.isPresent() ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadTag(){
|
||||
new GetTag(hashtagName)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Hashtag result){
|
||||
hashtag=result;
|
||||
updateHeader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void setFollowed(boolean followed){
|
||||
if(followRequestRunning)
|
||||
return;
|
||||
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
followButton.setTextVisible(false);
|
||||
followProgress.setVisibility(View.VISIBLE);
|
||||
followRequestRunning=true;
|
||||
new SetTagFollowed(hashtagName, followed)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Hashtag result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
hashtag=result;
|
||||
updateHeader();
|
||||
followRequestRunning=false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(error instanceof MastodonErrorResponse er && "Duplicate record".equals(er.error)){
|
||||
hashtag.following=true;
|
||||
}else{
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
updateHeader();
|
||||
followRequestRunning=false;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
@@ -35,10 +33,9 @@ import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
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.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
@@ -75,7 +72,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
private TextView notificationsBadge;
|
||||
|
||||
private String accountID;
|
||||
private boolean isAkkoma;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -83,8 +79,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.mo_app_name);
|
||||
|
||||
isAkkoma = getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
@@ -94,7 +88,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
homeTabFragment=new HomeTabFragment();
|
||||
homeTabFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("disableDiscover", isAkkoma);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
discoverFragment=new DiscoverFragment();
|
||||
discoverFragment.setArguments(args);
|
||||
@@ -304,7 +297,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if(tab==R.id.tab_profile){
|
||||
ArrayList<String> options=new ArrayList<>();
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
options.add(session.self.getDisplayName()+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
@@ -317,11 +310,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
discoverFragment.openSearch();
|
||||
return true;
|
||||
}
|
||||
if(tab==R.id.tab_home){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
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.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -95,7 +95,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
private ImageView collapsedChevron;
|
||||
private TextView timelineTitle;
|
||||
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 List<TimelineDefinition> timelinesList;
|
||||
private int count;
|
||||
@@ -237,21 +237,25 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
ViewTreeObserver vto = getToolbar().getViewTreeObserver();
|
||||
if (vto.isAlive()) {
|
||||
vto.addOnGlobalLayoutListener(() -> {
|
||||
Toolbar t = getToolbar();
|
||||
if (t == null) return;
|
||||
int toolbarWidth = t.getWidth();
|
||||
if (toolbarWidth == 0) return;
|
||||
vto.addOnGlobalLayoutListener(()->{
|
||||
Toolbar t=getToolbar();
|
||||
if(t==null) return;
|
||||
int toolbarWidth=t.getWidth();
|
||||
if(toolbarWidth==0) return;
|
||||
|
||||
int toolbarFrameWidth = toolbarFrame.getWidth();
|
||||
int padding = toolbarWidth - toolbarFrameWidth;
|
||||
FrameLayout parent = ((FrameLayout) toolbarShowNewPostsBtn.getParent());
|
||||
if (padding == parent.getPaddingStart()) return;
|
||||
int toolbarFrameWidth=toolbarFrame.getWidth();
|
||||
int actionsWidth=toolbarWidth-toolbarFrameWidth;
|
||||
// margin (4) + padding (12) + icon (24) + margin (8) + chevron (16) + padding (12)
|
||||
int switcherWidth=V.dp(76);
|
||||
FrameLayout parent=((FrameLayout) toolbarShowNewPostsBtn.getParent());
|
||||
if(actionsWidth==parent.getPaddingStart()) return;
|
||||
int paddingMax=Math.max(actionsWidth, switcherWidth);
|
||||
int paddingEnd=(Math.max(0, switcherWidth-actionsWidth));
|
||||
|
||||
// toolbar frame goes from screen edge to beginning of right-aligned option buttons.
|
||||
// centering button by applying the same space on the left
|
||||
parent.setPaddingRelative(padding, 0, 0, 0);
|
||||
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth - padding * 2);
|
||||
parent.setPaddingRelative(paddingMax, 0, paddingEnd, 0);
|
||||
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth-paddingMax*2);
|
||||
|
||||
switcher.setPivotX(V.dp(28)); // padding + half of icon
|
||||
switcher.setPivotY(switcher.getHeight() / 2f);
|
||||
@@ -266,7 +270,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
public void onSuccess(List<FollowList> lists) {
|
||||
updateList(lists, listItems);
|
||||
}
|
||||
|
||||
@@ -291,7 +295,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
new GetAnnouncements(false).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result) {
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if (result.stream().anyMatch(a -> !a.read)) {
|
||||
announcementsBadged = true;
|
||||
announcements.setVisible(false);
|
||||
@@ -385,7 +389,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
}
|
||||
|
||||
private void updateOverflowMenu() {
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
Menu m = overflowPopup.getMenu();
|
||||
m.clear();
|
||||
overflowPopup.inflate(R.menu.home_overflow);
|
||||
@@ -404,9 +408,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
addListsToOverflowMenu();
|
||||
addHashtagsToOverflowMenu();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !UiUtils.isEMUI()) {
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
|
||||
m.setGroupDividerEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -509,7 +512,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
int id = item.getItemId();
|
||||
ListTimeline list;
|
||||
FollowList list;
|
||||
Hashtag hashtag;
|
||||
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
@@ -528,9 +531,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
} else if ((hashtag = hashtagsItems.get(id)) != null) {
|
||||
args.putString("hashtag", hashtag.name);
|
||||
args.putBoolean("following", hashtag.following);
|
||||
Nav.go(getActivity(), HashtagTimelineFragment.class, args);
|
||||
UiUtils.openHashtagTimeline(getContext(), accountID, hashtag);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -700,13 +701,13 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
@Subscribe
|
||||
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
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
|
||||
ListTimeline list = new ListTimeline();
|
||||
FollowList list = new FollowList();
|
||||
list.id = event.id;
|
||||
list.title = event.title;
|
||||
list.repliesPolicy = event.repliesPolicy;
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
@@ -20,6 +19,7 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -49,16 +49,6 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
loadData();
|
||||
}
|
||||
|
||||
private boolean typeFilterPredicate(Status s) {
|
||||
AccountLocalPreferences lp=getLocalPrefs();
|
||||
return (lp.showReplies || s.inReplyToId == null) &&
|
||||
(lp.showBoosts || s.reblog == null);
|
||||
}
|
||||
|
||||
private List<Status> filterPosts(List<Status> items) {
|
||||
return items.stream().filter(this::typeFilterPredicate).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
@@ -66,11 +56,12 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if (getActivity() == null) return;
|
||||
List<Status> filteredItems = filterPosts(result.items);
|
||||
if(getActivity()==null) return;
|
||||
boolean empty=result.items.isEmpty();
|
||||
maxID=result.maxID;
|
||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
||||
if(result.isFromCache())
|
||||
AccountSessionManager.get(accountID).filterStatuses(result.items, getFilterContext());
|
||||
onDataLoaded(result.items, !empty);
|
||||
if(result.isFromCache() && GlobalUserPreferences.loadNewPosts)
|
||||
loadNewPosts();
|
||||
}
|
||||
});
|
||||
@@ -83,7 +74,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
if(parent != null && parent.isNewPostsBtnShown() && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
||||
if(parent!=null && parent.isNewPostsBtnShown() && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
||||
parent.hideNewPostsButton();
|
||||
}
|
||||
}
|
||||
@@ -96,7 +87,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
if(!getArguments().getBoolean("noAutoLoad")){
|
||||
if(!loaded && !dataLoading){
|
||||
loadData();
|
||||
}else if(!dataLoading){
|
||||
}else if(!dataLoading && GlobalUserPreferences.loadNewPosts){
|
||||
loadNewPosts();
|
||||
}
|
||||
}
|
||||
@@ -126,38 +117,46 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
}
|
||||
|
||||
public void onStatusCreated(Status status){
|
||||
if(status.reblog!=null) return;
|
||||
prependItems(Collections.singletonList(status), true);
|
||||
}
|
||||
|
||||
private void loadNewPosts(){
|
||||
if (!GlobalUserPreferences.loadNewPosts) return;
|
||||
dataLoading=true;
|
||||
// we only care about the data that was actually retrieved from the timeline api since
|
||||
// user-created statuses are probably in the wrong position
|
||||
List<Status> dataFromTimeline=data.stream().filter(s->!s.fromStatusCreated).collect(Collectors.toList());
|
||||
// The idea here is that we request the timeline such that if there are fewer than `limit` posts,
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
// between the existing and newly loaded parts of the timeline.
|
||||
String sinceID=data.size()>1 ? data.get(1).id : "1";
|
||||
String sinceID=dataFromTimeline.size()>1 ? dataFromTimeline.get(1).id : "1";
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
result = filterPosts(result);
|
||||
refreshDone();
|
||||
if(result.isEmpty() || getActivity()==null)
|
||||
return;
|
||||
Status last=result.get(result.size()-1);
|
||||
List<Status> toAdd;
|
||||
if(!data.isEmpty() && last.id.equals(data.get(0).id)){ // This part intersects with the existing one
|
||||
toAdd=result.subList(0, result.size()-1); // Remove the already known last post
|
||||
if(!dataFromTimeline.isEmpty() && last.id.equals(dataFromTimeline.get(0).id)){ // This part intersects with the existing one
|
||||
toAdd=new ArrayList<>(result.subList(0, result.size()-1)); // Remove the already known last post
|
||||
}else{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
last.hasGapAfter=last.id;
|
||||
toAdd=result;
|
||||
}
|
||||
if(!toAdd.isEmpty())
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(new ArrayList<>(toAdd), false);
|
||||
// removing statuses that come up as duplicates (hopefully only posts and boosts that were locally created
|
||||
// and thus were already prepended to the timeline earlier)
|
||||
List<String> existingIds=data.stream().map(Status::getID).collect(Collectors.toList());
|
||||
toAdd.removeIf(s->existingIds.contains(s.getID()));
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
if(parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +164,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
refreshDone();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -182,10 +182,10 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
gap.loading=true;
|
||||
dataLoading=true;
|
||||
|
||||
String maxID = null;
|
||||
String minID = null;
|
||||
String maxID=null;
|
||||
String minID=null;
|
||||
if (downwards) {
|
||||
maxID = item.getItemID();
|
||||
maxID=item.getItem().getMaxID();
|
||||
} else {
|
||||
int gapPos=displayItems.indexOf(gap);
|
||||
StatusDisplayItem nextItem=displayItems.get(gapPos + 1);
|
||||
@@ -202,15 +202,17 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
int gapPos=displayItems.indexOf(gap);
|
||||
if(gapPos==-1)
|
||||
return;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||
if(result.isEmpty()){
|
||||
displayItems.remove(gapPos);
|
||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||
Status gapStatus=getStatusByID(gap.parentID);
|
||||
if(gapStatus!=null){
|
||||
gapStatus.hasGapAfter=false;
|
||||
gapStatus.hasGapAfter=null;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
}
|
||||
}else{
|
||||
// TODO: refactor this code. it's too long. incomprehensible, even
|
||||
if(downwards) {
|
||||
Set<String> idsBelowGap=new HashSet<>();
|
||||
boolean belowGap=false;
|
||||
@@ -220,7 +222,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
idsBelowGap.add(s.id);
|
||||
}else if(s.id.equals(gap.parentID)){
|
||||
belowGap=true;
|
||||
s.hasGapAfter=false;
|
||||
s.hasGapAfter=null;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
}else{
|
||||
gapPostIndex++;
|
||||
@@ -233,7 +235,8 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
break;
|
||||
}
|
||||
if(endIndex==result.size()){
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
Status last=result.get(result.size()-1);
|
||||
last.hasGapAfter=last.id;
|
||||
}else{
|
||||
result=result.subList(0, endIndex);
|
||||
}
|
||||
@@ -278,7 +281,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
.filter(s->Objects.equals(s.id, gap.parentID))
|
||||
.findFirst();
|
||||
if (gapStatus.isPresent()) {
|
||||
gapStatus.get().hasGapAfter=false;
|
||||
gapStatus.get().hasGapAfter=null;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus.get()), false);
|
||||
}
|
||||
targetList.clear();
|
||||
@@ -329,7 +332,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
if (parent != null) parent.hideNewPostsButton();
|
||||
if(parent!=null) parent.hideNewPostsButton();
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,18 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.GetList;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
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.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ListEditor;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -40,7 +39,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
private String listID;
|
||||
private String listTitle;
|
||||
@Nullable
|
||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
||||
private FollowList.RepliesPolicy repliesPolicy;
|
||||
private boolean exclusive;
|
||||
|
||||
@Override
|
||||
@@ -55,19 +54,19 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
listID = args.getString("listID");
|
||||
listTitle = args.getString("listTitle");
|
||||
exclusive = args.getBoolean("listIsExclusive");
|
||||
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||
repliesPolicy = FollowList.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||
|
||||
setTitle(listTitle);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
new GetList(listID).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline listTimeline) {
|
||||
if (getActivity() == null) return;
|
||||
public void onSuccess(FollowList followList) {
|
||||
if(getActivity()==null) return;
|
||||
// TODO: save updated info
|
||||
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
|
||||
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
|
||||
repliesPolicy = listTimeline.repliesPolicy;
|
||||
if (!followList.title.equals(listTitle)) setTitle(followList.title);
|
||||
if (followList.repliesPolicy != null && !followList.repliesPolicy.equals(repliesPolicy)) {
|
||||
repliesPolicy = followList.repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +97,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
String newTitle = editor.getTitle().trim();
|
||||
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
|
||||
public void onSuccess(ListTimeline list) {
|
||||
if (getActivity() == null) return;
|
||||
public void onSuccess(FollowList list) {
|
||||
if(getActivity()==null) return;
|
||||
setTitle(list.title);
|
||||
listTitle = list.title;
|
||||
repliesPolicy = list.repliesPolicy;
|
||||
@@ -120,7 +119,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
.show();
|
||||
} else if (item.getItemId() == R.id.delete) {
|
||||
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
||||
E.post(new ListDeletedEvent(listID));
|
||||
E.post(new ListDeletedEvent(accountID, listID));
|
||||
Nav.finish(this);
|
||||
});
|
||||
}
|
||||
@@ -134,13 +133,14 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
currentRequest=new GetListTimeline(listID, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
if(getActivity()==null) return;
|
||||
boolean more=applyMaxID(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||
onDataLoaded(result, more);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -18,13 +18,14 @@ 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.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
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.M3AlertDialogBuilder;
|
||||
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.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 profileAccountId;
|
||||
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)
|
||||
.setView(editor)
|
||||
.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
|
||||
public void onSuccess(ListTimeline list) {
|
||||
public void onSuccess(FollowList list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
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) {
|
||||
userInList.put(listId, isMember);
|
||||
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<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
public void onSuccess(Void o) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
@@ -139,19 +140,19 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
if (getActivity() == null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
public void onSuccess(List<FollowList> lists) {
|
||||
if(getActivity()==null) return;
|
||||
for (FollowList l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
if (getActivity() == null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
public void onSuccess(List<FollowList> allLists) {
|
||||
if(getActivity()==null) return;
|
||||
List<FollowList> newLists = new ArrayList<>();
|
||||
for (FollowList l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
@@ -169,8 +170,8 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
FollowList item = data.get(i);
|
||||
if (item.id.equals(event.listID)) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
@@ -181,7 +182,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
FollowList item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
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 CheckBox listToggle;
|
||||
|
||||
@@ -241,7 +242,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
public void onBind(FollowList item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
||||
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 org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
@@ -40,11 +41,13 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(wantsElevationOnScrollEffect())
|
||||
if (getParentFragment() instanceof HasElevationOnScrollListener elevator)
|
||||
list.addOnScrollListener(elevator.getElevationOnScrollListener());
|
||||
else if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
if(refreshLayout!=null){
|
||||
if(refreshLayout!=null)
|
||||
setRefreshLayoutColors(refreshLayout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -15,10 +16,6 @@ import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -31,6 +28,8 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -39,6 +38,11 @@ import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -46,7 +50,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener {
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener, HasAccountID {
|
||||
|
||||
TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
@@ -54,7 +58,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
private View tabsDivider;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
String unreadMarker, realUnreadMarker;
|
||||
private MenuItem markAllReadItem;
|
||||
private MenuItem markAllReadItem, filterItem;
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||
private ElevationOnScrollListener elevationOnScrollListener;
|
||||
|
||||
@@ -92,9 +96,10 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
|
||||
filterItem=menu.findItem(R.id.filter_notifications).setVisible(true);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
updateMarkAllReadButton();
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests, R.id.mark_all_read);
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests, R.id.mark_all_read, R.id.filter_notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,11 +121,61 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
if (getCurrentFragment() instanceof NotificationsListFragment nlf) {
|
||||
nlf.resetUnreadBackground();
|
||||
}
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.filter_notifications) {
|
||||
Context ctx = getToolbarContext();
|
||||
String[] listItems = {
|
||||
ctx.getString(R.string.notification_type_mentions_and_replies),
|
||||
ctx.getString(R.string.notification_type_reblog),
|
||||
ctx.getString(R.string.notification_type_favorite),
|
||||
ctx.getString(R.string.notification_type_follow),
|
||||
ctx.getString(R.string.notification_type_poll),
|
||||
ctx.getString(R.string.sk_notification_type_update),
|
||||
ctx.getString(R.string.sk_notification_type_posts)
|
||||
};
|
||||
|
||||
boolean[] checkedItems = {
|
||||
getLocalPrefs().notificationFilters.mention,
|
||||
getLocalPrefs().notificationFilters.reblog,
|
||||
getLocalPrefs().notificationFilters.favourite,
|
||||
getLocalPrefs().notificationFilters.follow,
|
||||
getLocalPrefs().notificationFilters.poll,
|
||||
getLocalPrefs().notificationFilters.update,
|
||||
getLocalPrefs().notificationFilters.status,
|
||||
};
|
||||
|
||||
M3AlertDialogBuilder dialogBuilder = new M3AlertDialogBuilder(ctx);
|
||||
dialogBuilder.setTitle(R.string.sk_settings_filters);
|
||||
dialogBuilder.setMultiChoiceItems(listItems, checkedItems, (dialog, which, isChecked) ->checkedItems[which] = isChecked);
|
||||
|
||||
dialogBuilder.setPositiveButton(R.string.save, (d, which) -> {
|
||||
saveFilters(checkedItems);
|
||||
this.allNotificationsFragment.reload();
|
||||
}).setNeutralButton(R.string.mo_notification_filter_reset, (d, which) -> {
|
||||
Arrays.fill(checkedItems, true);
|
||||
saveFilters(checkedItems);
|
||||
this.allNotificationsFragment.reload();
|
||||
}).setNegativeButton(R.string.cancel, (d, which) -> {});
|
||||
|
||||
dialogBuilder.create().show();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveFilters(boolean[] checkedItems) {
|
||||
PushSubscription.Alerts filter = getLocalPrefs().notificationFilters;
|
||||
filter.mention = checkedItems[0];
|
||||
filter.reblog = checkedItems[1];
|
||||
filter.favourite = checkedItems[2];
|
||||
filter.follow = checkedItems[3];
|
||||
filter.poll = checkedItems[4];
|
||||
filter.update = checkedItems[5];
|
||||
filter.status = checkedItems[6];
|
||||
getLocalPrefs().save();
|
||||
}
|
||||
|
||||
void markAsRead(){
|
||||
if(allNotificationsFragment.getData().isEmpty()) return;
|
||||
String id=allNotificationsFragment.getData().get(0).id;
|
||||
@@ -184,6 +239,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
public void onPageSelected(int position){
|
||||
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f)
|
||||
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
|
||||
filterItem.setVisible(position==0);
|
||||
if(position==0)
|
||||
return;
|
||||
Fragment _page=getFragmentForPage(position);
|
||||
@@ -254,7 +310,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> accounts) {
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
||||
}
|
||||
|
||||
@@ -309,6 +365,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID(){
|
||||
return accountID;
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,10 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -14,6 +18,7 @@ import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||
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.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
private boolean onlyMentions;
|
||||
private boolean onlyPosts;
|
||||
private String maxID;
|
||||
private boolean reloadingFromCache;
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
private String unreadMarker, realUnreadMarker;
|
||||
private MenuItem markAllReadItem;
|
||||
|
||||
@Override
|
||||
protected boolean wantsComposeButton() {
|
||||
@@ -63,13 +69,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
if(savedInstanceState!=null){
|
||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
|
||||
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
|
||||
}
|
||||
if (onlyPosts) {
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
|
||||
}
|
||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,13 +82,45 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||
onlyPosts=getArguments().getBoolean("onlyPosts", false);
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
if(!onlyMentions){
|
||||
switch(n.type){
|
||||
case MENTION -> {
|
||||
if(!getLocalPrefs().notificationFilters.mention)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case REBLOG -> {
|
||||
if(!getLocalPrefs().notificationFilters.reblog)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case FAVORITE, REACTION -> {
|
||||
if(!getLocalPrefs().notificationFilters.favourite)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case FOLLOW, FOLLOW_REQUEST -> {
|
||||
if(!getLocalPrefs().notificationFilters.follow)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case POLL -> {
|
||||
if(!getLocalPrefs().notificationFilters.poll)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case UPDATE -> {
|
||||
if(!getLocalPrefs().notificationFilters.update)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case STATUS -> {
|
||||
if(!getLocalPrefs().notificationFilters.status)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationHeaderStatusDisplayItem titleItem;
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
titleItem=null;
|
||||
@@ -130,7 +163,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
.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
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
if(getActivity()==null)
|
||||
@@ -144,6 +177,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
|
||||
maxID=result.maxID;
|
||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||
if(bannerHelper!=null) bannerHelper.onBannerBecameVisible();
|
||||
reloadingFromCache=false;
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
nf.updateMarkAllReadButton();
|
||||
@@ -151,7 +185,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onRelationshipsLoaded(){
|
||||
if(getActivity()==null)
|
||||
@@ -166,13 +200,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!dataLoading){
|
||||
if(onlyMentions){
|
||||
refresh();
|
||||
}else{
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +225,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint();
|
||||
private Rect tmpRect=new Rect();
|
||||
@@ -234,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){
|
||||
for(Notification n:data){
|
||||
if(n.id.equals(id))
|
||||
@@ -258,7 +281,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
continue;
|
||||
Status contentStatus=ntf.status.getContentStatus();
|
||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
||||
updatePoll(ntf.id, ntf.status, ev.poll);
|
||||
updatePoll(ntf.id, contentStatus, ev.poll);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,11 +369,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
|
||||
}
|
||||
|
||||
@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(){
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
nf.unreadMarker=nf.realUnreadMarker;
|
||||
@@ -362,13 +412,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onRefresh(){
|
||||
super.onRefresh();
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
if (!onlyMentions && !onlyPosts) nf.markAsRead();
|
||||
if (!onlyMentions) nf.markAsRead();
|
||||
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
nf.unreadMarker=nf.realUnreadMarker=m;
|
||||
nf.updateMarkAllReadButton();
|
||||
});
|
||||
}
|
||||
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
|
||||
@@ -386,4 +443,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
? "/users/" + getSession().self.username + "/interactions"
|
||||
: "/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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -35,6 +36,8 @@ public class PinnedPostsListFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
@@ -8,6 +8,8 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
@@ -21,9 +23,9 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.transition.ChangeBounds;
|
||||
import android.transition.Fade;
|
||||
import android.transition.TransitionManager;
|
||||
@@ -40,8 +42,6 @@ import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
@@ -68,10 +68,10 @@ import org.joinmastodon.android.api.requests.accounts.SetPrivateNote;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.account_list.BlocksListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.MutesListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -85,6 +85,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
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.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
@@ -138,7 +139,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ImageView avatar;
|
||||
private CoverImageView cover;
|
||||
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 ProgressBarButton actionButton, notifyButton;
|
||||
private ViewPager2 pager;
|
||||
@@ -160,9 +161,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private View actionButtonWrap;
|
||||
private CustomDrawingOrderLinearLayout scrollableContent;
|
||||
|
||||
public FrameLayout noteWrap;
|
||||
public EditText noteEdit;
|
||||
private String note;
|
||||
private Account account, remoteAccount;
|
||||
private String accountID;
|
||||
private String domain;
|
||||
@@ -193,6 +191,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
|
||||
// profile note
|
||||
private FrameLayout noteWrap;
|
||||
private EditText noteEdit;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -213,7 +215,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else if (isInstanceAkkoma()) {
|
||||
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
||||
maxFields = (int) getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
||||
}
|
||||
}else{
|
||||
profileAccountID=getArguments().getString("profileAccountID");
|
||||
@@ -241,7 +243,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
cover=content.findViewById(R.id.cover);
|
||||
avatarBorder=content.findViewById(R.id.avatar_border);
|
||||
name=content.findViewById(R.id.name);
|
||||
usernameWrap=content.findViewById(R.id.username_wrap);
|
||||
username=content.findViewById(R.id.username);
|
||||
usernameDomain=content.findViewById(R.id.username_domain);
|
||||
lockIcon=content.findViewById(R.id.lock_icon);
|
||||
botIcon=content.findViewById(R.id.bot_icon);
|
||||
bio=content.findViewById(R.id.bio);
|
||||
@@ -261,7 +265,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
bioEdit=content.findViewById(R.id.bio_edit);
|
||||
nameEditWrap=content.findViewById(R.id.name_edit_wrap);
|
||||
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
|
||||
usernameWrap=content.findViewById(R.id.username_wrap);
|
||||
actionProgress=content.findViewById(R.id.action_progress);
|
||||
notifyProgress=content.findViewById(R.id.notify_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
@@ -278,61 +281,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
noteEdit = content.findViewById(R.id.note_edit);
|
||||
noteWrap = content.findViewById(R.id.note_edit_wrap);
|
||||
ImageButton noteEditConfirm = content.findViewById(R.id.note_edit_confirm);
|
||||
noteEdit=content.findViewById(R.id.note_edit);
|
||||
noteWrap=content.findViewById(R.id.note_edit_wrap);
|
||||
|
||||
noteEditConfirm.setOnClickListener((v -> {
|
||||
if (!noteEdit.getText().toString().trim().equals(note)) {
|
||||
savePrivateNote();
|
||||
}
|
||||
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
|
||||
noteEdit.clearFocus();
|
||||
}));
|
||||
|
||||
|
||||
noteEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
|
||||
noteEditConfirm.setVisibility(View.VISIBLE);
|
||||
noteEditConfirm.animate()
|
||||
.alpha(1.0f)
|
||||
.setDuration(700);
|
||||
} else {
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
|
||||
noteEditConfirm.animate()
|
||||
.alpha(0.0f)
|
||||
.setDuration(700);
|
||||
noteEditConfirm.setVisibility(View.INVISIBLE);
|
||||
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
|
||||
if(hasFocus){
|
||||
hideFab();
|
||||
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||
}else{
|
||||
showFab();
|
||||
savePrivateNote(noteEdit.getText().toString());
|
||||
}
|
||||
});
|
||||
|
||||
noteEditConfirm.setOnClickListener((v -> {
|
||||
if (!noteEdit.getText().toString().trim().equals(note)) {
|
||||
savePrivateNote();
|
||||
}
|
||||
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
|
||||
noteEdit.clearFocus();
|
||||
}));
|
||||
|
||||
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
@@ -369,6 +330,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabbar.setTabTextSize(V.dp(14));
|
||||
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, (tab, position)->tab.setText(switch(position){
|
||||
case 0 -> R.string.profile_featured;
|
||||
case 1 -> R.string.profile_timeline;
|
||||
case 2 -> R.string.profile_about;
|
||||
default -> throw new IllegalStateException();
|
||||
}));
|
||||
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
@@ -431,28 +398,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
|
||||
content.findViewById(R.id.username_wrap).setOnClickListener(v->{
|
||||
try {
|
||||
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();
|
||||
}
|
||||
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
|
||||
});
|
||||
|
||||
content.findViewById(R.id.username_wrap).setOnLongClickListener(v->{
|
||||
@@ -460,7 +406,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(!usernameString.contains("@")){
|
||||
usernameString+="@"+domain;
|
||||
}
|
||||
UiUtils.copyText(username, '@'+usernameString);
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+usernameString));
|
||||
UiUtils.maybeShowTextCopiedToast(getActivity());
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -488,9 +435,49 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
|
||||
|
||||
// qrCodeButton.setOnClickListener(v->{
|
||||
// Bundle args=new Bundle();
|
||||
// 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(){
|
||||
noteWrap.setVisibility(View.VISIBLE);
|
||||
noteEdit.setText(relationship.note);
|
||||
}
|
||||
|
||||
private void hidePrivateNote(){
|
||||
noteWrap.setVisibility(View.GONE);
|
||||
noteEdit.setText(null);
|
||||
}
|
||||
|
||||
private void savePrivateNote(String note){
|
||||
if(note!=null && note.equals(relationship.note)){
|
||||
updateRelationship();
|
||||
invalidateOptionsMenu();
|
||||
return;
|
||||
}
|
||||
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship result) {
|
||||
updateRelationship(result);
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
private void onAccountLoaded(Account result) {
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
@@ -517,25 +504,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
|
||||
public void setNote(String note){
|
||||
this.note=note;
|
||||
noteWrap.setVisibility(View.VISIBLE);
|
||||
noteEdit.setVisibility(View.VISIBLE);
|
||||
noteEdit.setText(note);
|
||||
}
|
||||
|
||||
private void savePrivateNote(){
|
||||
new SetPrivateNote(profileAccountID, noteEdit.getText().toString()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship result) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
if (remoteAccount != null) {
|
||||
@@ -557,7 +525,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
onAccountLoaded(result);
|
||||
}
|
||||
})
|
||||
@@ -668,6 +636,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
getChildFragmentManager().putFragment(outState, "pinnedPosts", pinnedPostsFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHidden(){
|
||||
if (relationship != null && !noteEdit.getText().toString().equals(relationship.note)){
|
||||
savePrivateNote(noteEdit.getText().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
@@ -701,14 +676,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void bindHeaderView(){
|
||||
setTitle(account.displayName);
|
||||
setTitle(account.getDisplayName());
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(
|
||||
TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() :
|
||||
GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic,
|
||||
V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.getDisplayName());
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
@@ -730,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)
|
||||
? account.getFullyQualifiedName()
|
||||
: account.acct);
|
||||
// String acct = ((isSelf || account.isRemote)
|
||||
// ? account.getFullyQualifiedName()
|
||||
// : 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.setImageTintList(ColorStateList.valueOf(username.getCurrentTextColor()));
|
||||
@@ -787,7 +767,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
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);
|
||||
ssb=new SpannableStringBuilder(field.name);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
@@ -832,27 +812,28 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return;
|
||||
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
||||
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{
|
||||
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;
|
||||
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
|
||||
openWithAccounts.setVisible(hasMultipleAccounts);
|
||||
SubMenu accountsMenu = openWithAccounts.getSubMenu();
|
||||
if (hasMultipleAccounts) {
|
||||
SubMenu accountsMenu=openWithAccounts.getSubMenu();
|
||||
if(hasMultipleAccounts){
|
||||
accountsMenu.clear();
|
||||
UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL(
|
||||
getActivity(), s.getID(), account.url, false
|
||||
));
|
||||
}
|
||||
menu.findItem(R.id.share).setTitle(R.string.share_user);
|
||||
|
||||
if(isOwnProfile) {
|
||||
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MenuItem mute = menu.findItem(R.id.mute);
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), mute);
|
||||
@@ -860,19 +841,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.manage_user_lists).setVisible(relationship.following);
|
||||
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if (relationship.following) {
|
||||
MenuItem hideBoosts = menu.findItem(R.id.hide_boosts);
|
||||
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
|
||||
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
} else {
|
||||
menu.findItem(R.id.hide_boosts).setVisible(false);
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
if(!account.isLocal())
|
||||
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
else
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
boolean canAddNote = noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty());
|
||||
menu.findItem(R.id.edit_note).setTitle(canAddNote ? R.string.sk_add_note : R.string.sk_delete_note);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -884,11 +870,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
}else if(id==R.id.mute){
|
||||
confirmToggleMuted();
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
confirmToggleBlocked();
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}else if(id==R.id.soft_block){
|
||||
confirmSoftBlockUser();
|
||||
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -933,16 +919,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
}
|
||||
Nav.go(getActivity(), ListsFragment.class, args);
|
||||
}else if(id==R.id.mutes){
|
||||
}else if(id==R.id.muted_accounts){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), MutesListFragment.class, args);
|
||||
}else if(id==R.id.blocks){
|
||||
Nav.go(getActivity(), MutedAccountsListFragment.class, args);
|
||||
}else if(id==R.id.blocked_accounts){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), BlocksListFragment.class, args);
|
||||
Nav.go(getActivity(), BlockedAccountsListFragment.class, args);
|
||||
}else if(id==R.id.followed_hashtags){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -954,6 +940,36 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
saveAndExitEditMode();
|
||||
}else if(id==R.id.edit_note){
|
||||
if(noteWrap.getVisibility()==View.GONE){
|
||||
showPrivateNote();
|
||||
UiUtils.beginLayoutTransition(scrollableContent);
|
||||
noteEdit.requestFocus();
|
||||
noteEdit.postDelayed(()->{
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.showSoftInput(noteEdit, 0);
|
||||
}, 100);
|
||||
}else if(relationship.note.isEmpty() && noteEdit.getText().toString().isEmpty()){
|
||||
hidePrivateNote();
|
||||
noteEdit.clearFocus();
|
||||
noteEdit.postDelayed(()->{
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(noteEdit.getWindowToken(), 0);
|
||||
}, 100);
|
||||
UiUtils.beginLayoutTransition(scrollableContent);
|
||||
}else{
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getContext().getString(R.string.sk_private_note_confirm_delete, account.getDisplayUsername()))
|
||||
.setPositiveButton(R.string.delete, (dlg, btn)->savePrivateNote(null))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
invalidateOptionsMenu();
|
||||
}else if(id==R.id.manage_user_lists){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -978,7 +994,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
private void updateRelationship(){
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if(relationship.note!=null && !relationship.note.isEmpty()) showPrivateNote();
|
||||
else hidePrivateNote();
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
|
||||
@@ -988,11 +1006,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
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));
|
||||
|
||||
if (!isOwnProfile) {
|
||||
setNote(relationship.note);
|
||||
// aboutFragment.setNote(relationship.note, accountID, profileAccountID);
|
||||
}
|
||||
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
UiUtils.beginLayoutTransition(scrollableContent);
|
||||
}
|
||||
|
||||
public ImageButton getFab() {
|
||||
@@ -1190,7 +1205,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
updateMetadataHeight();
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
Drawable close=getToolbarContext().getDrawable(R.drawable.ic_baseline_close_24).mutate();
|
||||
Drawable close=getToolbarContext().getDrawable(R.drawable.ic_fluent_dismiss_24_regular).mutate();
|
||||
close.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
toolbar.setNavigationIcon(close);
|
||||
toolbar.setNavigationContentDescription(R.string.discard);
|
||||
@@ -1266,6 +1281,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
imm.hideSoftInputFromWindow(content.getWindowToken(), 0);
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
bindHeaderView();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
|
||||
private void saveAndExitEditMode(){
|
||||
@@ -1280,7 +1296,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
savingEdits=false;
|
||||
account=result;
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
exitEditMode();
|
||||
setActionProgressVisible(false);
|
||||
}
|
||||
@@ -1295,18 +1311,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void confirmToggleMuted(){
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void confirmToggleBlocked(){
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void confirmSoftBlockUser(){
|
||||
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void updateRelationship(Relationship r){
|
||||
relationship=r;
|
||||
updateRelationship();
|
||||
@@ -1315,7 +1319,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(noteEdit.hasFocus()) {
|
||||
savePrivateNote();
|
||||
savePrivateNote(noteEdit.getText().toString());
|
||||
}
|
||||
if(isInEditMode){
|
||||
if(savingEdits)
|
||||
@@ -1357,7 +1361,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return;
|
||||
int radius=V.dp(25);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1369,7 +1373,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(drawable==null || drawable instanceof ColorDrawable)
|
||||
return;
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,7 +1580,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
if(item.verifiedAt!=null){
|
||||
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||
int textColor=UiUtils.getThemeColor(getContext(), R.attr.colorM3Success);
|
||||
value.setTextColor(textColor);
|
||||
value.setLinkTextColor(textColor);
|
||||
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate();
|
||||
|
||||
@@ -120,6 +120,15 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
// because, for some reason, when navigating back from compose fragment,
|
||||
// match_parent would otherwise be incorrect (leaving a gap for the keyboard
|
||||
// where there is none)
|
||||
list.post(list::requestLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count)
|
||||
@@ -130,7 +139,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
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.instance.GetInstance;
|
||||
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.ui.InterpolatingMotionEffect;
|
||||
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.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
||||
@@ -47,13 +55,15 @@ public class SplashFragment extends AppKitFragment{
|
||||
private ProgressBarButton defaultServerButton;
|
||||
private ProgressBar defaultServerProgress;
|
||||
private String chosenDefaultServer=DEFAULT_SERVER;
|
||||
private boolean loadingDefaultServer;
|
||||
private boolean loadingDefaultServer, loadedDefaultServer;
|
||||
private Uri currentInviteLink;
|
||||
private ProgressDialog instanceLoadingProgress;
|
||||
private String inviteCode;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
|
||||
loadAndChooseDefaultServer();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -101,6 +111,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
});
|
||||
}
|
||||
});
|
||||
if(!loadedDefaultServer && !loadingDefaultServer)
|
||||
loadAndChooseDefaultServer();
|
||||
|
||||
return contentView;
|
||||
}
|
||||
@@ -109,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
|
||||
Bundle extras=new Bundle();
|
||||
boolean isSignup=v.getId()==R.id.btn_get_started;
|
||||
extras.putBoolean("signup", isSignup);
|
||||
extras.putString("defaultServer", chosenDefaultServer);
|
||||
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
||||
}
|
||||
|
||||
private void onJoinDefaultServerClick(View v){
|
||||
if(loadingDefaultServer)
|
||||
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()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(!result.registrations){
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.instance_signup_closed)
|
||||
@@ -131,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(result));
|
||||
if(inviteCode!=null)
|
||||
args.putString("inviteCode", inviteCode);
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -138,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading_instance, true)
|
||||
.execNoAuth(chosenDefaultServer);
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private void onLearnMoreClick(View v){
|
||||
@@ -197,7 +258,18 @@ public class SplashFragment extends AppKitFragment{
|
||||
}
|
||||
|
||||
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()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -239,7 +311,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
private void setChosenDefaultServer(String domain){
|
||||
chosenDefaultServer=domain;
|
||||
loadingDefaultServer=false;
|
||||
if(defaultServerButton!=null && getActivity()!=null){
|
||||
loadedDefaultServer=true;
|
||||
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
|
||||
defaultServerButton.setTextVisible(true);
|
||||
defaultServerProgress.setVisibility(View.GONE);
|
||||
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
|
||||
|
||||
@@ -3,8 +3,8 @@ package org.joinmastodon.android.fragments;
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
@@ -12,18 +12,23 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import name.fraser.neil.plaintext.diff_match_patch;
|
||||
|
||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
private String id, url;
|
||||
@@ -48,17 +53,52 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||
if (getActivity() == null) return;
|
||||
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);
|
||||
}
|
||||
})
|
||||
.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
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS);
|
||||
List<StatusDisplayItem> items=new ArrayList<>();
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
@@ -83,8 +123,11 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
|
||||
Status prev=data.get(idx+1);
|
||||
|
||||
if(!Objects.equals(s.content, prev.content)){
|
||||
// if only formatting was changed, don't even try to create a diff text
|
||||
if(!Objects.equals(HtmlParser.text(s.content), HtmlParser.text(prev.content))){
|
||||
changes.add(StatusEditChangeType.TEXT_CHANGED);
|
||||
//update status content to display a diffs
|
||||
s.content=createDiffText(prev.content, s.content);
|
||||
}
|
||||
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
|
||||
if(s.spoilerText==null){
|
||||
@@ -147,15 +190,10 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null, s));
|
||||
items.add(1, new DummyStatusDisplayItem(s.id, this));
|
||||
}
|
||||
items.addAll(StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER|StatusDisplayItem.FLAG_INSET|StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS));
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
@@ -170,4 +208,28 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(url);
|
||||
}
|
||||
|
||||
private String createDiffText(String original, String modified) {
|
||||
diff_match_patch dmp=new diff_match_patch();
|
||||
LinkedList<diff_match_patch.Diff> diffs=dmp.diff_main(original, modified);
|
||||
dmp.diff_cleanupSemantic(diffs);
|
||||
|
||||
StringBuilder stringBuilder=new StringBuilder();
|
||||
for(diff_match_patch.Diff diff : diffs){
|
||||
switch(diff.operation){
|
||||
case DELETE->{
|
||||
stringBuilder.append("<edit-diff-delete>");
|
||||
stringBuilder.append(diff.text);
|
||||
stringBuilder.append("</edit-diff-delete>");
|
||||
}
|
||||
case INSERT->{
|
||||
stringBuilder.append("<edit-diff-insert>");
|
||||
stringBuilder.append(diff.text);
|
||||
stringBuilder.append("</edit-diff-insert>");
|
||||
}
|
||||
default->stringBuilder.append(diff.text);
|
||||
}
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusMuteChangedEvent;
|
||||
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.ReblogDeletedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
@@ -25,13 +27,19 @@ import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -45,10 +53,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
boolean isMainThreadStatus = this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id);
|
||||
int flags = 0;
|
||||
AccountLocalPreferences lp=getLocalPrefs();
|
||||
if (GlobalUserPreferences.spectatorMode)
|
||||
if(GlobalUserPreferences.spectatorMode)
|
||||
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
|
||||
if (!lp.emojiReactionsEnabled || lp.showEmojiReactions==ONLY_OPENED)
|
||||
if(!lp.emojiReactionsEnabled || lp.showEmojiReactions==ONLY_OPENED)
|
||||
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
|
||||
if(GlobalUserPreferences.translateButtonOpenedOnly)
|
||||
flags |= StatusDisplayItem.FLAG_NO_TRANSLATE;
|
||||
if(!GlobalUserPreferences.showMediaPreview)
|
||||
flags |= StatusDisplayItem.FLAG_NO_MEDIA_PREVIEW;
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags);
|
||||
@@ -79,8 +89,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Status status=getContentStatusByID(id);
|
||||
if(status==null)
|
||||
return;
|
||||
if(status==null || status.preview) return;
|
||||
if(status.isRemote){
|
||||
UiUtils.lookupStatus(getContext(), status, accountID, null, status1 -> {
|
||||
status1.filterRevealed = true;
|
||||
@@ -157,12 +166,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
}
|
||||
}
|
||||
|
||||
protected Status getContentStatusByID(String id){
|
||||
public Status getContentStatusByID(String id){
|
||||
Status s=getStatusByID(id);
|
||||
return s==null ? null : s.getContentStatus();
|
||||
}
|
||||
|
||||
protected Status getStatusByID(String id){
|
||||
public Status getStatusByID(String id){
|
||||
for(Status s:data){
|
||||
if(s.id.equals(id)){
|
||||
return s;
|
||||
@@ -189,41 +198,73 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeStatus(Status status){
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
StatusDisplayItem item = displayItems.get(i);
|
||||
if(status.id.equals(item.parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
if (item.parentID.equals(status.inReplyToId)) {
|
||||
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
|
||||
ancestorLastIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean removeStatusDisplayItems(String parentID, int firstIndex, int ancestorFirstIndex, int ancestorLastIndex){
|
||||
// did we find an ancestor that is also the status' neighbor?
|
||||
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) {
|
||||
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) {
|
||||
StatusDisplayItem item = displayItems.get(i);
|
||||
// update ancestor to have no descendant anymore
|
||||
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
|
||||
}
|
||||
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
|
||||
if(ancestorFirstIndex>=0 && ancestorLastIndex==firstIndex-1){
|
||||
// update ancestor to have no descendant anymore
|
||||
displayItems.subList(ancestorFirstIndex, ancestorLastIndex+1).forEach(i->i.hasDescendantNeighbor=false);
|
||||
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex-ancestorFirstIndex+1);
|
||||
}
|
||||
|
||||
if(index==-1)
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(status.id))
|
||||
break;
|
||||
if(firstIndex==-1) return false;
|
||||
int lastIndex=firstIndex;
|
||||
while(lastIndex<displayItems.size()){
|
||||
StatusDisplayItem item=displayItems.get(lastIndex);
|
||||
if(!item.parentID.equals(parentID) || item instanceof GapStatusDisplayItem) break;
|
||||
lastIndex++;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
int count=lastIndex-firstIndex;
|
||||
if(count<1) return false;
|
||||
displayItems.subList(firstIndex, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(firstIndex, count);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void removeStatus(Status status){
|
||||
final AccountSessionManager asm=AccountSessionManager.getInstance();
|
||||
final CacheController cache=AccountSessionManager.get(accountID).getCacheController();
|
||||
final boolean unReblogging=status.reblog!=null && asm.isSelf(accountID, status.account);
|
||||
final Predicate<Status> isToBeRemovedReblog=item->item!=null && item.reblog!=null
|
||||
&& item.reblog.id.equals(status.reblog.id)
|
||||
&& asm.isSelf(accountID, item.account);
|
||||
final BiPredicate<String, Supplier<String>> isToBeRemovedContent=(parentId, contentIdSupplier)->
|
||||
parentId.equals(status.id) || contentIdSupplier.get().equals(status.id);
|
||||
|
||||
int ancestorFirstIndex=-1, ancestorLastIndex=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
// we found a status that the to-be-removed status replies to!
|
||||
// storing indices to maybe update its display items
|
||||
if(item.parentID.equals(status.inReplyToId)){
|
||||
if(ancestorFirstIndex==-1) ancestorFirstIndex=i;
|
||||
ancestorLastIndex=i;
|
||||
}
|
||||
// if we're un-reblogging, we compare the reblogged status's id with the current status's
|
||||
if(unReblogging
|
||||
? isToBeRemovedReblog.test(getStatusByID(item.parentID))
|
||||
: isToBeRemovedContent.test(item.parentID, item::getContentStatusID)){
|
||||
// if statuses are removed from index i, the next iteration should be on the same index again
|
||||
if(removeStatusDisplayItems(item.parentID, i, ancestorFirstIndex, ancestorLastIndex)) i--;
|
||||
// resetting in case we find another occurrence of the same status that also has ancestors
|
||||
// (we won't - unless the timeline is being especially weird)
|
||||
ancestorFirstIndex=-1; ancestorLastIndex=-1;
|
||||
}
|
||||
}
|
||||
|
||||
Consumer<List<Status>> removeStatusFromData=(list)->{
|
||||
Iterator<Status> it=list.iterator();
|
||||
while(it.hasNext()){
|
||||
Status s=it.next();
|
||||
if(unReblogging
|
||||
? isToBeRemovedReblog.test(s)
|
||||
: isToBeRemovedContent.test(s.id, s::getContentStatusID)){
|
||||
it.remove();
|
||||
cache.deleteStatus(s.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
removeStatusFromData.accept(data);
|
||||
removeStatusFromData.accept(preloadedData);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -314,6 +355,22 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
removeStatus(status);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onReblogDeleted(ReblogDeletedEvent ev){
|
||||
AccountSessionManager asm=AccountSessionManager.getInstance();
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(Status item : data){
|
||||
boolean itemIsOwnReblog=item.reblog!=null
|
||||
&& item.getContentStatusID().equals(ev.statusID)
|
||||
&& asm.isSelf(accountID, item.account);
|
||||
if(itemIsOwnReblog){
|
||||
removeStatus(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
@@ -334,6 +391,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
Status contentStatus=status.getContentStatus();
|
||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
||||
updatePoll(status.id, contentStatus, ev.poll);
|
||||
AccountSessionManager.get(accountID).getCacheController().updateStatus(contentStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,19 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
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.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -23,18 +31,19 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
@@ -50,25 +59,41 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
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.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;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||
protected Status mainStatus, updatedStatus;
|
||||
protected Status mainStatus, updatedStatus, replyTo;
|
||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||
private StatusContext result;
|
||||
protected boolean contextInitiallyRendered, transitionFinished;
|
||||
protected boolean contextInitiallyRendered, transitionFinished, preview;
|
||||
private FrameLayout replyContainer;
|
||||
private LinearLayout replyButton;
|
||||
private ImageView replyButtonAva;
|
||||
private TextView replyButtonText;
|
||||
private int lastBottomInset;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setLayout(R.layout.fragment_thread);
|
||||
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
|
||||
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
|
||||
refreshing=contextInitiallyRendered=getArguments().getBoolean("refresh", false);
|
||||
if(inReplyToAccount!=null)
|
||||
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
preview=mainStatus.preview;
|
||||
if(preview) setRefreshEnabled(false);
|
||||
setTitle(preview ? getString(R.string.sk_post_preview) : HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis));
|
||||
transitionFinished = getArguments().getBoolean("noTransition", false);
|
||||
|
||||
E.register(this);
|
||||
@@ -81,7 +106,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusMuteChaged(StatusMuteChangedEvent ev){
|
||||
public void onStatusMuteChanged(StatusMuteChangedEvent ev){
|
||||
for(Status s:data){
|
||||
s.getContentStatus().update(ev);
|
||||
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
|
||||
@@ -131,12 +156,18 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
text.textSelectable=true;
|
||||
else if(item instanceof FooterStatusDisplayItem footer)
|
||||
footer.hideCounts=true;
|
||||
else if(item instanceof SpoilerStatusDisplayItem spoiler){
|
||||
for(StatusDisplayItem subItem:spoiler.contentItems){
|
||||
if(subItem instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||
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;
|
||||
}
|
||||
@@ -149,11 +180,21 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if (refreshing) loadMainStatus();
|
||||
currentRequest=new GetStatusContext(mainStatus.id)
|
||||
if(preview && replyTo==null){
|
||||
result=new StatusContext();
|
||||
result.descendants=Collections.emptyList();
|
||||
result.ancestors=Collections.emptyList();
|
||||
return;
|
||||
}
|
||||
if(refreshing && !preview) loadMainStatus();
|
||||
currentRequest=new GetStatusContext(preview ? replyTo.id : mainStatus.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(StatusContext result){
|
||||
if(preview){
|
||||
result.descendants=Collections.emptyList();
|
||||
result.ancestors.add(replyTo);
|
||||
}
|
||||
ThreadFragment.this.result = result;
|
||||
maybeApplyContext();
|
||||
}
|
||||
@@ -214,8 +255,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
// TODO: figure out how this code works
|
||||
if (isInstanceAkkoma()) sortStatusContext(mainStatus, result);
|
||||
|
||||
result.descendants=filterStatuses(result.descendants);
|
||||
result.ancestors=filterStatuses(result.ancestors);
|
||||
filterStatuses(result.descendants);
|
||||
filterStatuses(result.ancestors);
|
||||
restoreStatusStates(result.descendants, oldData);
|
||||
restoreStatusStates(result.ancestors, oldData);
|
||||
|
||||
@@ -295,12 +336,12 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
// descendant neighbor
|
||||
Optional
|
||||
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
|
||||
.filter(s -> s.inReplyToId.equals(current.id))
|
||||
.filter(s -> current.id.equals(s.inReplyToId))
|
||||
.orElse(null),
|
||||
// ancestoring neighbor
|
||||
Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
|
||||
.filter(ancestor -> Optional.ofNullable(ancestor.descendantNeighbor)
|
||||
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
|
||||
.map(ancestorsDescendant -> current.id.equals(ancestorsDescendant.id))
|
||||
.orElse(false))
|
||||
.map(a -> a.status)
|
||||
.orElse(null)
|
||||
@@ -351,11 +392,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
|
||||
return statuses.stream()
|
||||
.filter(statusFilterPredicate)
|
||||
.collect(Collectors.toList());
|
||||
private void filterStatuses(List<Status> statuses){
|
||||
AccountSessionManager.get(accountID).filterStatuses(statuses, getFilterContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -370,6 +408,22 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle 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());
|
||||
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);
|
||||
showContent();
|
||||
if(!loaded)
|
||||
@@ -509,4 +563,35 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
}
|
||||
super.onErrorRetryClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
lastBottomInset=insets.getSystemWindowInsetBottom();
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
|
||||
}
|
||||
|
||||
private void openReply(){
|
||||
maybeShowPreReplySheet(mainStatus, ()->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
|
||||
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 -> {
|
||||
UiUtils.lookupStatus(v.getContext(), mainStatus, accountID, session.getID(), status -> {
|
||||
if (status == null) return;
|
||||
openReply();
|
||||
});
|
||||
}, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getSnackbarOffset(){
|
||||
return replyContainer.getHeight()-lastBottomInset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
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.viewmodel.AccountViewModel;
|
||||
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.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private String currentQuery;
|
||||
public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
protected String currentQuery;
|
||||
private boolean resultDelivered;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
|
||||
@@ -29,12 +32,11 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
super.onCreate(savedInstanceState);
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
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.addDivider(contentView);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -52,13 +54,21 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
AccountSearchFragment.this.onSuccess(result.accounts);
|
||||
}
|
||||
})
|
||||
.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
|
||||
protected void 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 String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
protected int itemLayoutRes=R.layout.item_account_list;
|
||||
|
||||
public BaseAccountListFragment(){
|
||||
super(40);
|
||||
@@ -74,6 +75,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
req.setCallback(new Callback<>(){
|
||||
@@ -83,7 +86,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
for(Relationship rel:result){
|
||||
relationships.put(rel.id, rel);
|
||||
}
|
||||
if (getActivity() == null) return;
|
||||
if(getActivity()==null) return;
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
@@ -124,13 +127,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||
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 onBindViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
@@ -171,7 +168,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@NonNull
|
||||
@Override
|
||||
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);
|
||||
return holder;
|
||||
}
|
||||
@@ -179,6 +176,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
BaseAccountListFragment.this.onBindViewHolder(holder);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountBlocks;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
public class BlockedAccountsListFragment extends AccountRelatedAccountListFragment{
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.sk_blocked_accounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetAccountBlocks(maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return super.getWebUri(base).buildUpon()
|
||||
.appendPath("/blocks").build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountMutes;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
public class MutedAccountsListFragment extends AccountRelatedAccountListFragment{
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.sk_muted_accounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetAccountMutes(maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return super.getWebUri(base).buildUpon()
|
||||
.appendPath("/mutes").build();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user