Compare commits
2135 Commits
m3-merger
...
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 | ||
|
|
03cb0b3c47 | ||
|
|
53c454a59b | ||
|
|
f2f8620312 | ||
|
|
d21ae9d94e | ||
|
|
453f082822 | ||
|
|
23843e9002 | ||
|
|
232d2e7e6d | ||
|
|
e9d73cbe32 | ||
|
|
4ee229ea79 | ||
|
|
c261214e49 | ||
|
|
4bb0597edf | ||
|
|
b06df8c3d0 | ||
|
|
a00afd5d7f | ||
|
|
796bdf2893 | ||
|
|
5b7868b44e | ||
|
|
66e67231c8 | ||
|
|
58be2214a4 | ||
|
|
72a370aa84 | ||
|
|
9a41a2d6fb | ||
|
|
2629e0039c | ||
|
|
2cd98a6620 | ||
|
|
24048a677f | ||
|
|
283b56be5b | ||
|
|
debf8c52a1 | ||
|
|
7fdea9710f | ||
|
|
d2de62ee55 | ||
|
|
d706768112 | ||
|
|
1614213aae | ||
|
|
3045fe0357 | ||
|
|
5a35be0ebd | ||
|
|
22b1cd0d0b | ||
|
|
49f10bacb3 | ||
|
|
2926e5096e | ||
|
|
361f27d5df | ||
|
|
d068093c5a | ||
|
|
3bea1b7f3e | ||
|
|
5a34d9ffce | ||
|
|
240754b70a | ||
|
|
7973ad7042 | ||
|
|
8446dbe05d | ||
|
|
7b0313cc69 | ||
|
|
994ae0242c | ||
|
|
fd34be3c66 | ||
|
|
20785d53fe | ||
|
|
3cc1748231 | ||
|
|
079112b905 | ||
|
|
38aa55b371 | ||
|
|
e07c01018b | ||
|
|
08de4525eb | ||
|
|
062dca6a77 | ||
|
|
d9abb8dfa1 | ||
|
|
5bcbe19435 | ||
|
|
955b985d1a | ||
|
|
6d6ab789a3 | ||
|
|
ad8be2f7a3 | ||
|
|
64be55e406 | ||
|
|
6230f32135 | ||
|
|
30db82fc98 | ||
|
|
921589b9bb | ||
|
|
0b0de7a4e3 | ||
|
|
c790eaf9c0 | ||
|
|
2c7cef488b | ||
|
|
c26603e0ca | ||
|
|
e1e5c9b0bd | ||
|
|
96f0817c20 | ||
|
|
e0165a25f4 | ||
|
|
e465fb944a | ||
|
|
fb159b5a52 | ||
|
|
1a349f0bb8 | ||
|
|
034dfe551c | ||
|
|
1f713dc50e | ||
|
|
258df9546b | ||
|
|
2daabd6a7e | ||
|
|
1e96b76b33 | ||
|
|
6dd944341e | ||
|
|
5dca70c657 | ||
|
|
c6f2e8151f | ||
|
|
99135ebdd6 | ||
|
|
53348be732 | ||
|
|
023c1d1598 | ||
|
|
1d0c591cdb | ||
|
|
a03482fa53 | ||
|
|
bc5fd2530a | ||
|
|
0ca57c3d72 | ||
|
|
5d20697e07 | ||
|
|
2847a1fae0 | ||
|
|
4bd78f31f2 | ||
|
|
e6281f9bb5 | ||
|
|
2c83b5aa02 | ||
|
|
586d48c80e | ||
|
|
fff2c5a3f5 | ||
|
|
3411cf4e56 | ||
|
|
e126858e7a | ||
|
|
9e0b40d409 | ||
|
|
e362efb453 | ||
|
|
8095d3004a | ||
|
|
27f023863b | ||
|
|
af6fae4d86 | ||
|
|
8119049df1 | ||
|
|
b5c09a4398 | ||
|
|
14df6c4e05 | ||
|
|
5c34d38783 | ||
|
|
cfce3595f6 | ||
|
|
411b561a3a | ||
|
|
ff9c211f3c | ||
|
|
c8a650acd4 | ||
|
|
a8cca76392 | ||
|
|
5e2f653d30 | ||
|
|
418f87999c | ||
|
|
c330428c07 | ||
|
|
119537f45f | ||
|
|
9f09b437ab | ||
|
|
1141dd5875 | ||
|
|
a008a0ac9a | ||
|
|
5cf5267e49 | ||
|
|
df2246b71a | ||
|
|
7862fefe94 | ||
|
|
b59957bd90 | ||
|
|
e307f1cca2 | ||
|
|
de185e5a27 | ||
|
|
e1fbe2c96e | ||
|
|
d7babf1e4d | ||
|
|
1203de0015 | ||
|
|
e7148acefb | ||
|
|
88741b0db4 | ||
|
|
12f9a11f03 | ||
|
|
234972352d | ||
|
|
67448f9de5 | ||
|
|
9681256a06 | ||
|
|
7f44c2ca49 | ||
|
|
f355919660 | ||
|
|
928f24b460 | ||
|
|
40e54b165b | ||
|
|
57d6b33b58 | ||
|
|
6b02a2df68 | ||
|
|
6d56771aba | ||
|
|
56a83e66cd | ||
|
|
9f17f1600a | ||
|
|
1724d8a532 | ||
|
|
7b4be83781 | ||
|
|
52030b3b2d | ||
|
|
3b36434dff | ||
|
|
f73be653ce | ||
|
|
808c922637 | ||
|
|
ceef155e03 | ||
|
|
eb5a38d514 | ||
|
|
33d4e05e96 | ||
|
|
968668e38c | ||
|
|
097a18fa92 | ||
|
|
5a85420866 | ||
|
|
50cee02191 | ||
|
|
e7dbb9fdf7 | ||
|
|
e2b2361ad2 | ||
|
|
89fe1dc783 | ||
|
|
eaadd80eeb | ||
|
|
c8063e961e | ||
|
|
3057546c55 | ||
|
|
f90e799caa | ||
|
|
b4cdf35d36 | ||
|
|
cad0ad7a59 | ||
|
|
ca60003c39 | ||
|
|
0f030e0bac | ||
|
|
6d4f212a18 | ||
|
|
183b39bc24 | ||
|
|
27ad0c6fcf | ||
|
|
9944185424 | ||
|
|
72ca3d0204 | ||
|
|
53204a9998 | ||
|
|
c3624eddb3 | ||
|
|
e144d0ac6f | ||
|
|
ac8562aaa2 | ||
|
|
4f8c4c67d2 | ||
|
|
b5f661f1af | ||
|
|
0015f3f0bf | ||
|
|
c5d0fdd645 | ||
|
|
2d09ad44fb | ||
|
|
667fffd124 | ||
|
|
699233d8c7 | ||
|
|
56aabdc4a6 | ||
|
|
443e2c7a6f | ||
|
|
985b0f6e63 | ||
|
|
cc86edf276 | ||
|
|
4071b9342d | ||
|
|
c92ab11dcd | ||
|
|
f3a1e7cf58 | ||
|
|
ebcafbf90d | ||
|
|
b5f7135e38 | ||
|
|
8a3df3657b | ||
|
|
f71d1bc5d3 | ||
|
|
f293619189 | ||
|
|
68ec3499ea | ||
|
|
4b79d35854 | ||
|
|
54ec1d781a | ||
|
|
2b926ffa46 | ||
|
|
6bcdbaba34 | ||
|
|
a2beead3a5 | ||
|
|
e7a25e353d | ||
|
|
af04a01130 | ||
|
|
fe1cfa1d7b | ||
|
|
b248797bb0 | ||
|
|
f24eba08d3 | ||
|
|
0e89559a47 | ||
|
|
d7ab019107 | ||
|
|
858657799f | ||
|
|
d02a72e079 | ||
|
|
3be57d1b0b | ||
|
|
bed550e97c | ||
|
|
7e2619ea75 | ||
|
|
4b22f1d3a7 | ||
|
|
885b3e3c60 | ||
|
|
9dcc7e293f | ||
|
|
6a68cf5e41 | ||
|
|
1edcf847b5 | ||
|
|
f5aff4d262 | ||
|
|
29297be4a3 | ||
|
|
90b87529e0 | ||
|
|
39af05524d | ||
|
|
2c8246341a | ||
|
|
77e19b4d6f | ||
|
|
18857e6233 | ||
|
|
4aee67382b | ||
|
|
3656402277 | ||
|
|
e3fb2cd03c | ||
|
|
56e752c0b3 | ||
|
|
de7bc49f85 | ||
|
|
beeffdff91 | ||
|
|
90f84d628a | ||
|
|
b89e0b5c5a | ||
|
|
2dd983c803 | ||
|
|
bb895c88b2 | ||
|
|
e33d0e692c | ||
|
|
133c941dc8 | ||
|
|
62222cd1d9 | ||
|
|
5d358d79ff | ||
|
|
f724644d84 | ||
|
|
f71c4b1374 | ||
|
|
a921a925b0 | ||
|
|
0f08af8c03 | ||
|
|
d54a59dbee | ||
|
|
c6cd3995b0 | ||
|
|
54d06b7a23 | ||
|
|
1ec0505d52 | ||
|
|
be225f8bb4 | ||
|
|
f5732fd2ae | ||
|
|
17fc14de97 | ||
|
|
7c9d11fab0 | ||
|
|
24abf9d317 | ||
|
|
22bf8840d5 | ||
|
|
a1fbb733ce | ||
|
|
851fa70563 | ||
|
|
6cab15b4c6 | ||
|
|
aac89c354c | ||
|
|
a032f9af10 | ||
|
|
ec3670d562 | ||
|
|
96c3bd890a | ||
|
|
8a0852d1a4 | ||
|
|
77e2c6c09f | ||
|
|
af60c8ba4a | ||
|
|
70a2f1fc0b | ||
|
|
b7d06a47db | ||
|
|
f0a5799eea | ||
|
|
f0b2a25dce | ||
|
|
de1a3fb939 | ||
|
|
823344dfc0 | ||
|
|
a6e6143157 | ||
|
|
b5feab89b4 | ||
|
|
c2c31de433 | ||
|
|
775f799968 | ||
|
|
5cda112129 | ||
|
|
642aaec6da | ||
|
|
f69ae5e816 | ||
|
|
91c7fc64bd | ||
|
|
91211b7720 | ||
|
|
ff667d6aed | ||
|
|
5e98496ea6 | ||
|
|
972fe1d15b | ||
|
|
26eaa36faa | ||
|
|
c517f41595 | ||
|
|
56a6d7243f | ||
|
|
18e43dfc22 | ||
|
|
816f6370ef | ||
|
|
51ef732d50 | ||
|
|
31a7aa9d40 | ||
|
|
30866a5292 | ||
|
|
3e1403d18a | ||
|
|
ebc2b2e59d | ||
|
|
10c2ee9597 | ||
|
|
c9a796dbfe | ||
|
|
1ba185ea9c | ||
|
|
1a50c3ff5f | ||
|
|
35a85c3247 | ||
|
|
6a729fa97f | ||
|
|
923639a329 | ||
|
|
fd99879ffc | ||
|
|
a78be8bc1d | ||
|
|
abfb497577 | ||
|
|
a10b184508 | ||
|
|
f0ea6660e6 | ||
|
|
a829f25d56 | ||
|
|
deff3dd8e0 | ||
|
|
ed6aa0725e | ||
|
|
6fd9047a8e | ||
|
|
48d7635a56 | ||
|
|
9515edea4b | ||
|
|
dab596f527 | ||
|
|
0c18ab2319 | ||
|
|
652c5e1d16 | ||
|
|
e1d9cc4add | ||
|
|
d51cd2d497 | ||
|
|
39d1b69014 | ||
|
|
74a9e49190 | ||
|
|
b0763473c7 | ||
|
|
4e5bbe6f9b | ||
|
|
6c5fb5ea09 | ||
|
|
afe0c9e0db | ||
|
|
1f2213042f | ||
|
|
5edd2466f9 | ||
|
|
f3b3a1a577 | ||
|
|
068619b815 | ||
|
|
f121e94979 | ||
|
|
b5b52529d4 | ||
|
|
876bf73454 | ||
|
|
522dbf6e4a | ||
|
|
ae685095ba | ||
|
|
30d5fe2f12 | ||
|
|
2bf27c561c | ||
|
|
bbdc72323d | ||
|
|
6e335930f3 | ||
|
|
9b309939da | ||
|
|
faf2e5115d | ||
|
|
dc5d9412c8 | ||
|
|
fc0680d66f | ||
|
|
56c9a5433f | ||
|
|
60e473ee55 | ||
|
|
ae34ecd5c3 | ||
|
|
fd1caa8729 | ||
|
|
1182e5c60c | ||
|
|
d99d515dfa | ||
|
|
70a15e7d9c | ||
|
|
1691382369 | ||
|
|
b7da9c6d51 | ||
|
|
3426538dca | ||
|
|
63de2b200b | ||
|
|
ff1ee766dc | ||
|
|
f033411adf | ||
|
|
a738eaf8c0 | ||
|
|
5074aadd6e | ||
|
|
0854961470 | ||
|
|
227b077935 | ||
|
|
1e4358290a | ||
|
|
925169eb31 | ||
|
|
e1abeb9252 | ||
|
|
cbe0add211 | ||
|
|
299b524d62 | ||
|
|
31c094e696 | ||
|
|
a8038a2863 | ||
|
|
29933bb916 | ||
|
|
5ec0c078d8 | ||
|
|
e6287f1ff2 | ||
|
|
be9caf8905 | ||
|
|
f375142084 | ||
|
|
fd3668d520 | ||
|
|
d5e03e9d9e | ||
|
|
d62f094919 | ||
|
|
6d84f28600 | ||
|
|
209e603f2c | ||
|
|
1b4dc01c74 | ||
|
|
6aab8f6578 | ||
|
|
645af12c3f | ||
|
|
fadc42d72b | ||
|
|
fc831e7d42 | ||
|
|
1ba30a4d28 | ||
|
|
2998ee9145 | ||
|
|
971c4e5879 | ||
|
|
48c53ee88b | ||
|
|
acf1fa15da | ||
|
|
1c3b28f9d7 | ||
|
|
e274cf89c0 | ||
|
|
8929482466 | ||
|
|
b570c8d946 | ||
|
|
b396ee7987 | ||
|
|
90856a414a | ||
|
|
ea19925be6 | ||
|
|
03b3775843 | ||
|
|
38b39751ae | ||
|
|
54a4b0fe41 | ||
|
|
3bf591c944 | ||
|
|
584a6bbfa3 | ||
|
|
0f803cd4fa | ||
|
|
167a14b8db | ||
|
|
81cbc2d10c | ||
|
|
9bd8aff99b | ||
|
|
a770828165 | ||
|
|
ab457035ff | ||
|
|
f886e4c1d2 | ||
|
|
380e4ff77e | ||
|
|
58f0c07357 | ||
|
|
77dee59b9c | ||
|
|
464dc93d99 | ||
|
|
dcdfd3e5d3 | ||
|
|
646f83ff0a | ||
|
|
fdf0414698 | ||
|
|
cc699a3f5e | ||
|
|
12eaa8d5f1 | ||
|
|
70680e39c6 | ||
|
|
8bd8f90d58 | ||
|
|
54b53a266e | ||
|
|
66921e3b5a | ||
|
|
9d7af3964b | ||
|
|
ec73687e9b | ||
|
|
c8af800b88 | ||
|
|
e74ac5da56 | ||
|
|
efa003a9a5 | ||
|
|
de5165434d | ||
|
|
be648cc5ab | ||
|
|
90f1f464dc | ||
|
|
734aa52816 | ||
|
|
068d42175e | ||
|
|
2314871246 | ||
|
|
e4f13c900b | ||
|
|
4ddfa483d4 | ||
|
|
20f41ce7c9 | ||
|
|
c8df9e085e | ||
|
|
0238aa4375 | ||
|
|
79d7873790 | ||
|
|
996842489d | ||
|
|
23c2c2b5e7 | ||
|
|
9dd694ce2e | ||
|
|
f51f2a1197 | ||
|
|
3020cab243 | ||
|
|
be73c9e81c | ||
|
|
1c2183bf1a | ||
|
|
da54c93af1 | ||
|
|
777891ebe9 | ||
|
|
9836538562 | ||
|
|
4a443d4826 | ||
|
|
1c93d1fa67 | ||
|
|
0246059556 | ||
|
|
46382bd8b0 | ||
|
|
090d2658dd | ||
|
|
e47fa4f8db | ||
|
|
8338fff4e5 | ||
|
|
b2d09c2ab6 | ||
|
|
0aa4493e5c | ||
|
|
6e2a5437e8 | ||
|
|
ae61007d22 | ||
|
|
20f3c7c9db | ||
|
|
1789d90dc3 | ||
|
|
57306ff7fe | ||
|
|
bbdd1c4aa9 | ||
|
|
81d13537e3 | ||
|
|
b6c951c026 | ||
|
|
860cdb05e2 | ||
|
|
e239600af6 | ||
|
|
d7bef67550 | ||
|
|
ad86a00fa7 | ||
|
|
da13926269 | ||
|
|
9da0173215 | ||
|
|
cfeda06bf9 | ||
|
|
ca60ac6722 | ||
|
|
80f70bd530 | ||
|
|
3982fa4835 | ||
|
|
31e720690c | ||
|
|
560c75c669 | ||
|
|
fb441b21e0 | ||
|
|
e2f957f985 | ||
|
|
a4f97a134b | ||
|
|
18ae9efad3 | ||
|
|
8232047c55 | ||
|
|
fb3b9513f2 | ||
|
|
2ee4b03fe8 | ||
|
|
61b20eea05 | ||
|
|
2aba90f353 | ||
|
|
5065c7e7e2 | ||
|
|
a10e661b21 | ||
|
|
4975bde76f | ||
|
|
ebbd56e3bc | ||
|
|
454ec6b4c0 | ||
|
|
10a8b195b1 | ||
|
|
6f273df060 | ||
|
|
7cfade62d3 | ||
|
|
0e58b00086 | ||
|
|
9f18844148 | ||
|
|
d854da54fe | ||
|
|
33f4dd4716 | ||
|
|
93ca187f1c | ||
|
|
6fd59a19e8 | ||
|
|
531ce568c0 | ||
|
|
c49b25454e | ||
|
|
d39d5c2602 | ||
|
|
0a338ad607 | ||
|
|
0cb3e1863e | ||
|
|
209081f1f0 | ||
|
|
f0eb6573f4 | ||
|
|
e7f5dd3357 | ||
|
|
8101bb9ea1 | ||
|
|
54d48253d5 | ||
|
|
3373b2bb04 | ||
|
|
a7e23aa228 | ||
|
|
a2d0738a68 | ||
|
|
8cd55fc365 | ||
|
|
924d6a9dd9 | ||
|
|
816a307bc0 | ||
|
|
a947a6ea26 | ||
|
|
f14df2bb0f | ||
|
|
b5123ff865 | ||
|
|
ca4630e6e1 | ||
|
|
a8a1c7824d | ||
|
|
0725c2a741 | ||
|
|
c3f3a9be7e | ||
|
|
228fdc8ffe | ||
|
|
da2a69b4a1 | ||
|
|
822a6a3653 | ||
|
|
410f694863 | ||
|
|
57b2596d9f | ||
|
|
caebce897b | ||
|
|
677fb4e4da | ||
|
|
ce51fc3c0d | ||
|
|
0dcade767b | ||
|
|
f4f4cbe78b | ||
|
|
ebb6897582 | ||
|
|
9720a133ba | ||
|
|
29c36e90ab | ||
|
|
1b822676a8 | ||
|
|
2d24e50ff2 | ||
|
|
53369eb2d4 | ||
|
|
aeb0aa65a8 | ||
|
|
e9df125cde | ||
|
|
807010893a | ||
|
|
ea01b14ffb | ||
|
|
3fd9dc1dcd | ||
|
|
02a4a77885 | ||
|
|
651090a504 | ||
|
|
203254c9f4 | ||
|
|
74db111a4f | ||
|
|
16ef577a7a | ||
|
|
734b3bced6 | ||
|
|
e26c641dc7 | ||
|
|
9295cf4e9c | ||
|
|
dd9237e9ca | ||
|
|
ea81c1fad6 | ||
|
|
d334703c65 | ||
|
|
befa5a9f6d | ||
|
|
5f6f3c94c9 | ||
|
|
09ba42a974 | ||
|
|
87a9acd860 | ||
|
|
b1e43d6f97 | ||
|
|
ea92a61d13 | ||
|
|
7fda69a6aa | ||
|
|
cf8b9ac649 | ||
|
|
d871d2cad8 | ||
|
|
323018daa9 | ||
|
|
3c122b005d | ||
|
|
f9dc6105f4 | ||
|
|
d7fe3c80e6 | ||
|
|
e996deea0b | ||
|
|
67d89f5799 | ||
|
|
485d25f9f4 | ||
|
|
c4b7262e89 | ||
|
|
7191e5a96a | ||
|
|
0edaffee59 | ||
|
|
c859d35c93 | ||
|
|
17a59b5db4 | ||
|
|
580ae15af6 | ||
|
|
d76e823489 | ||
|
|
7c3b4128de | ||
|
|
da5929c092 | ||
|
|
77fca4d763 | ||
|
|
e66078e52e | ||
|
|
f84356f5d0 | ||
|
|
1fa5a8436b | ||
|
|
2a471ffa96 | ||
|
|
1ee5e1ab3a | ||
|
|
4d90cad034 | ||
|
|
45615b1fc5 | ||
|
|
9fd0e7fea4 | ||
|
|
696016bd8f | ||
|
|
cc46e09853 | ||
|
|
83e84836b5 | ||
|
|
14bb544344 | ||
|
|
ceec18ff5e | ||
|
|
d36ad43700 | ||
|
|
d93bcd9241 | ||
|
|
cfad3144f8 | ||
|
|
69272e891e | ||
|
|
576030f262 | ||
|
|
fb39f74ba5 | ||
|
|
272afca0fb | ||
|
|
9bfc73d6ee | ||
|
|
d0e3da67a4 | ||
|
|
1ecb9820c0 | ||
|
|
efb72eace9 | ||
|
|
84b15f4a49 | ||
|
|
2c793fd83b | ||
|
|
8810ac52f6 | ||
|
|
9442de7e5a | ||
|
|
4f40ecd112 | ||
|
|
5456f4fbea | ||
|
|
b294c998ae | ||
|
|
b7b7ef4faf | ||
|
|
6a736ef387 | ||
|
|
2a7d7d2e45 | ||
|
|
bdf822d6a5 | ||
|
|
a9cd67a64e | ||
|
|
a3ee20f06e | ||
|
|
2158a1abae | ||
|
|
52955b6c42 | ||
|
|
8524e57ecd | ||
|
|
639ecba380 | ||
|
|
ed6961016e | ||
|
|
5d1d6e4bd7 | ||
|
|
e87f47228f | ||
|
|
8e038b204c | ||
|
|
900b204bb0 | ||
|
|
17d679901a | ||
|
|
d8036779f8 | ||
|
|
3a35674ea2 | ||
|
|
3235cf1c4f | ||
|
|
3f6bda28b3 | ||
|
|
c0b4f4dd79 | ||
|
|
2a913e26e7 | ||
|
|
f60375cd5f | ||
|
|
5920270899 | ||
|
|
f36aee44c6 | ||
|
|
cd24526a9d | ||
|
|
a345ac1390 | ||
|
|
3d987b8e1d | ||
|
|
57043912e0 | ||
|
|
00aef5ea6b | ||
|
|
369b69668c | ||
|
|
65245f4560 | ||
|
|
4d4fdc97d4 | ||
|
|
c96577891c | ||
|
|
f48b2fc9cb | ||
|
|
2fca2580ed | ||
|
|
7adc1da361 | ||
|
|
6dc24dde43 | ||
|
|
4929e0e6ec | ||
|
|
16a8b8ed71 | ||
|
|
ad1412817e | ||
|
|
d9e6bb3bea | ||
|
|
8970404638 | ||
|
|
2a2241d7f9 | ||
|
|
db1a47e8eb | ||
|
|
3e57061cef | ||
|
|
cd200f8450 | ||
|
|
782013079f | ||
|
|
e5db8acd66 | ||
|
|
1a6a8019c8 | ||
|
|
e935eef29f | ||
|
|
381defda51 | ||
|
|
02ae80c204 | ||
|
|
82214b30e8 | ||
|
|
33a1f48602 | ||
|
|
aee845e5cc | ||
|
|
cd780f6006 | ||
|
|
d4741fefa0 | ||
|
|
7e1e8a2616 | ||
|
|
d73c05cdfc | ||
|
|
78323023cb | ||
|
|
2cf084c98f | ||
|
|
009dc226d5 | ||
|
|
94ab5835d8 | ||
|
|
ae5d2a7ed3 | ||
|
|
6ed7d24513 | ||
|
|
605ba441d3 |
43
.github/workflows/nightly-builds.yml
vendored
43
.github/workflows/nightly-builds.yml
vendored
@@ -3,6 +3,7 @@ name: Nightly builds
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -10,27 +11,27 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Appkit Repo
|
# - name: Checkout Appkit Repo
|
||||||
uses: actions/checkout@v3
|
# uses: actions/checkout@v3
|
||||||
with:
|
# with:
|
||||||
repository: grishka/appkit
|
# repository: grishka/appkit
|
||||||
|
#
|
||||||
- name: set up JDK 17
|
# - name: set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
# uses: actions/setup-java@v3
|
||||||
with:
|
# with:
|
||||||
java-version: '17'
|
# java-version: '17'
|
||||||
distribution: 'corretto'
|
# distribution: 'corretto'
|
||||||
cache: gradle
|
# cache: gradle
|
||||||
|
#
|
||||||
- name: Comment out signing config in appkits gradle file
|
# - name: Comment out signing config in appkits gradle file
|
||||||
run: |
|
# run: |
|
||||||
sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
# sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||||
|
#
|
||||||
- name: Grant execute permission for gradlew for Appkit
|
# - name: Grant execute permission for gradlew for Appkit
|
||||||
run: chmod +x gradlew
|
# run: chmod +x gradlew
|
||||||
|
#
|
||||||
- name: Compile appkit
|
# - name: Compile appkit
|
||||||
run: ./gradlew publishToMavenLocal
|
# run: ./gradlew publishToMavenLocal
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
|
|||||||
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.
|
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.
|
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://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>
|
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||||
|
|
||||||
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
||||||
|
|||||||
0
fix-metadata-markdown-lists.sh
Executable file → Normal file
0
fix-metadata-markdown-lists.sh
Executable file → Normal file
@@ -19,4 +19,5 @@ android.useAndroidX=true
|
|||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=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"
|
applicationId "org.joinmastodon.android.moshinda"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 101
|
versionCode 105
|
||||||
versionName "1.2.3+fork.101.moshinda"
|
versionName "2.3.0+fork.105.moshinda"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,28 @@ android {
|
|||||||
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
|
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// release{
|
||||||
|
// storeFile = file("keystore/release_keystore.jks")
|
||||||
|
// storePassword System.getenv("RELEASE_SIGNING_STORE_PASSWORD")
|
||||||
|
// if (storePassword == null) {
|
||||||
|
// Properties properties = new Properties()
|
||||||
|
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
// storePassword = properties.getProperty('RELEASE_SIGNING_STORE_PASSWORD')
|
||||||
|
// }
|
||||||
|
// keyAlias System.getenv("RELEASE_SIGNING_KEY_ALIAS")
|
||||||
|
// if (keyAlias == null) {
|
||||||
|
// Properties properties = new Properties()
|
||||||
|
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
// keyAlias = properties.getProperty('RELEASE_SIGNING_KEY_ALIAS')
|
||||||
|
// }
|
||||||
|
// keyPassword System.getenv("RELEASE_SIGNING_KEY_PASSWORD")
|
||||||
|
// if (keyPassword == null) {
|
||||||
|
// Properties properties = new Properties()
|
||||||
|
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||||
|
// keyPassword = properties.getProperty('RELEASE_SIGNING_KEY_PASSWORD')
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -81,8 +103,10 @@ android {
|
|||||||
versionNameSuffix '-play'
|
versionNameSuffix '-play'
|
||||||
}
|
}
|
||||||
githubRelease { initWith release }
|
githubRelease { initWith release }
|
||||||
playRelease { initWith release }
|
fdroidRelease {
|
||||||
fdroidRelease { initWith release }
|
initWith release
|
||||||
|
// signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -118,7 +142,7 @@ dependencies {
|
|||||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||||
implementation 'me.grishka.litex:palette:1.0.0'
|
implementation 'me.grishka.litex:palette:1.0.0'
|
||||||
implementation 'me.grishka.appkit:appkit:1.2.9'
|
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||||
implementation 'com.google.code.gson:gson:2.9.0'
|
implementation 'com.google.code.gson:gson:2.9.0'
|
||||||
implementation 'org.jsoup:jsoup:1.14.3'
|
implementation 'org.jsoup:jsoup:1.14.3'
|
||||||
implementation 'com.squareup:otto:1.3.8'
|
implementation 'com.squareup:otto:1.3.8'
|
||||||
|
|||||||
@@ -257,5 +257,9 @@ public class UiUtilsTest {
|
|||||||
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||||
makeField("pronouns", "-- * (asterisk) --")
|
makeField("pronouns", "-- * (asterisk) --")
|
||||||
)).orElseThrow());
|
)).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.joinmastodon.android.model.FilterContext.*;
|
||||||
import static org.junit.Assert.*;
|
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.LegacyFilter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -19,7 +22,9 @@ public class StatusFilterPredicateTest {
|
|||||||
|
|
||||||
private static final Status
|
private static final Status
|
||||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||||
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
|
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now()),
|
||||||
|
noAltText = Status.ofFake(null, "display me with a warning", Instant.now()),
|
||||||
|
withAltText = Status.ofFake(null, "display me with a warning", Instant.now());
|
||||||
|
|
||||||
static {
|
static {
|
||||||
hideMeFilter.phrase = "hide me";
|
hideMeFilter.phrase = "hide me";
|
||||||
@@ -29,6 +34,12 @@ public class StatusFilterPredicateTest {
|
|||||||
warnMeFilter.phrase = "warning";
|
warnMeFilter.phrase = "warning";
|
||||||
warnMeFilter.filterAction = WARN;
|
warnMeFilter.filterAction = WARN;
|
||||||
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||||
|
|
||||||
|
// noAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
|
||||||
|
// withAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
|
||||||
|
// for (Attachment mediaAttachment : withAltText.mediaAttachments) {
|
||||||
|
// mediaAttachment.description = "Alt Text";
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -78,4 +89,16 @@ public class StatusFilterPredicateTest {
|
|||||||
assertTrue("should pass because matching filter is for hiding",
|
assertTrue("should pass because matching filter is for hiding",
|
||||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAltTextFilterNoPass() {
|
||||||
|
assertFalse("should not pass because of no alt text",
|
||||||
|
new StatusFilterPredicate(allFilters, HOME).test(noAltText));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAltTextFilterPass() {
|
||||||
|
assertTrue("should pass because of alt text",
|
||||||
|
new StatusFilterPredicate(allFilters, HOME).test(withAltText));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,12 +25,17 @@
|
|||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.TRANSLATE" />
|
<action android:name="android.intent.action.TRANSLATE" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MastodonApp"
|
android:name=".MastodonApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:label="@string/mo_app_name"
|
android:label="@string/mo_app_name"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
@@ -1,56 +1,43 @@
|
|||||||
13bells.com
|
13bells.com
|
||||||
1611.social
|
1611.social
|
||||||
4aem.com
|
4aem.com
|
||||||
|
5dollah.click
|
||||||
adachi.party
|
adachi.party
|
||||||
anime.website
|
adtension.com
|
||||||
annihilation.social
|
annihilation.social
|
||||||
anon-kenkai.com
|
anon-kenkai.com
|
||||||
asbestos.cafe
|
asbestos.cafe
|
||||||
bae.st
|
bae.st
|
||||||
bajax.us
|
|
||||||
banepo.st
|
banepo.st
|
||||||
baraag.net
|
|
||||||
bassam.social
|
bassam.social
|
||||||
|
battlepenguin.video
|
||||||
beefyboys.win
|
beefyboys.win
|
||||||
beepboop.ga
|
|
||||||
berserker.town
|
|
||||||
bikeshed.party
|
|
||||||
boks.moe
|
|
||||||
boymoder.biz
|
boymoder.biz
|
||||||
brainsoap.net
|
brainsoap.net
|
||||||
breastmilk.club
|
breastmilk.club
|
||||||
brighteon.social
|
brighteon.social
|
||||||
bungle.online
|
cachapa.xyz
|
||||||
|
canary.fedinuke.example.com
|
||||||
|
catgirl.life
|
||||||
cawfee.club
|
cawfee.club
|
||||||
|
childlove.space
|
||||||
clew.lol
|
clew.lol
|
||||||
clubcyberia.co
|
clubcyberia.co
|
||||||
collapsitarian.io
|
|
||||||
comfyboy.club
|
|
||||||
contrapointsfan.club
|
contrapointsfan.club
|
||||||
|
crucible.world
|
||||||
cum.camp
|
cum.camp
|
||||||
cum.salon
|
cum.salon
|
||||||
darknight-coffee.org
|
|
||||||
decayable.ink
|
decayable.ink
|
||||||
dembased.xyz
|
dembased.xyz
|
||||||
desupost.soy
|
|
||||||
detroitriotcity.com
|
detroitriotcity.com
|
||||||
eatthebugs.social
|
djsumdog.com
|
||||||
eientei.org
|
eientei.org
|
||||||
elementality.org
|
|
||||||
eveningzoo.club
|
eveningzoo.club
|
||||||
firedragonstudios.com
|
|
||||||
firefaithfellowship.com
|
|
||||||
fluf.club
|
fluf.club
|
||||||
foxfam.club
|
|
||||||
freak.university
|
freak.university
|
||||||
freeatlantis.com
|
freeatlantis.com
|
||||||
freedomstrike.org
|
|
||||||
freesoftwareextremist.com
|
|
||||||
freespeech.group
|
|
||||||
freespeechextremist.com
|
freespeechextremist.com
|
||||||
freetalklive.com
|
|
||||||
froth.zone
|
froth.zone
|
||||||
fulltermprivacy.com
|
|
||||||
gameliberty.club
|
gameliberty.club
|
||||||
gearlandia.haus
|
gearlandia.haus
|
||||||
genderheretics.xyz
|
genderheretics.xyz
|
||||||
@@ -59,42 +46,34 @@ gleasonator.com
|
|||||||
glee.li
|
glee.li
|
||||||
glindr.org
|
glindr.org
|
||||||
goyim.app
|
goyim.app
|
||||||
goyslop.cafe
|
h5q.net
|
||||||
haeder.net
|
haeder.net
|
||||||
handholding.io
|
handholding.io
|
||||||
hitchhiker.social
|
hitchhiker.social
|
||||||
hunk.city
|
|
||||||
iddqd.social
|
iddqd.social
|
||||||
intkos.link
|
|
||||||
justicewarrior.social
|
|
||||||
kawa-kun.com
|
|
||||||
kitsunemimi.club
|
kitsunemimi.club
|
||||||
kiwifarms.cc
|
kiwifarms.cc
|
||||||
kompost.cz
|
|
||||||
kurosawa.moe
|
kurosawa.moe
|
||||||
|
kyaruc.moe
|
||||||
leafposter.club
|
leafposter.club
|
||||||
leftychan.net
|
|
||||||
lewdieheaven.com
|
lewdieheaven.com
|
||||||
liberdon.com
|
liberdon.com
|
||||||
ligma.pro
|
ligma.pro
|
||||||
lolicon.rocks
|
lolicon.rocks
|
||||||
|
lolison.network
|
||||||
lolison.top
|
lolison.top
|
||||||
lovingexpressions.net
|
lovingexpressions.net
|
||||||
mahodou.moe
|
|
||||||
makemysarcophagus.com
|
makemysarcophagus.com
|
||||||
maladaptive.art
|
|
||||||
marsey.moe
|
marsey.moe
|
||||||
masochi.st
|
|
||||||
mastinator.com
|
mastinator.com
|
||||||
merovingian.club
|
merovingian.club
|
||||||
midwaytrades.com
|
midwaytrades.com
|
||||||
mirr0r.city
|
mirr0r.city
|
||||||
moa.st
|
morale.ch
|
||||||
mouse.services
|
mouse.services
|
||||||
mugicha.club
|
mugicha.club
|
||||||
narrativerry.xyz
|
narrativerry.xyz
|
||||||
natehiggers.online
|
natehiggers.online
|
||||||
neckbeard.xyz
|
|
||||||
needs.vodka
|
needs.vodka
|
||||||
neenster.org
|
neenster.org
|
||||||
nicecrew.digital
|
nicecrew.digital
|
||||||
@@ -103,18 +82,18 @@ noagendasocial.com
|
|||||||
noagendasocial.nl
|
noagendasocial.nl
|
||||||
noagendatube.com
|
noagendatube.com
|
||||||
nobodyhasthe.biz
|
nobodyhasthe.biz
|
||||||
nukem.biz
|
norwoodzero.net
|
||||||
obo.sh
|
nyanide.com
|
||||||
onionfarms.org
|
onionfarms.org
|
||||||
pawlicker.com
|
pawlicker.com
|
||||||
pawoo.net
|
pawoo.net
|
||||||
pedo.school
|
pedo.school
|
||||||
|
peervideo.club
|
||||||
piazza.today
|
piazza.today
|
||||||
pibvt.net
|
pibvt.net
|
||||||
pieville.net
|
pieville.net
|
||||||
pisskey.io
|
pisskey.io
|
||||||
plagu.ee
|
plagu.ee
|
||||||
pmth.us
|
|
||||||
poa.st
|
poa.st
|
||||||
poast.org
|
poast.org
|
||||||
poast.tv
|
poast.tv
|
||||||
@@ -123,17 +102,18 @@ prospeech.space
|
|||||||
quodverum.com
|
quodverum.com
|
||||||
r18.social
|
r18.social
|
||||||
rakket.app
|
rakket.app
|
||||||
|
rapemeat.express
|
||||||
rapemeat.solutions
|
rapemeat.solutions
|
||||||
rdrama.cc
|
rayci.st
|
||||||
rebelbase.site
|
rebelbase.site
|
||||||
retardedniggers.forsale
|
|
||||||
rojogato.com
|
|
||||||
ryona.agency
|
ryona.agency
|
||||||
|
sad.cab
|
||||||
schwartzwelt.xyz
|
schwartzwelt.xyz
|
||||||
seal.cafe
|
seal.cafe
|
||||||
|
shaw.app
|
||||||
shigusegubu.club
|
shigusegubu.club
|
||||||
shitpost.cloud
|
shitpost.cloud
|
||||||
shota.house
|
shortstacksran.ch
|
||||||
silliness.observer
|
silliness.observer
|
||||||
skinheads.eu
|
skinheads.eu
|
||||||
skinheads.io
|
skinheads.io
|
||||||
@@ -148,23 +128,20 @@ sneed.social
|
|||||||
sonichu.com
|
sonichu.com
|
||||||
spinster.xyz
|
spinster.xyz
|
||||||
springbo.cc
|
springbo.cc
|
||||||
starnix.network
|
|
||||||
strelizia.net
|
strelizia.net
|
||||||
syspxl.xyz
|
|
||||||
tastingtraffic.net
|
tastingtraffic.net
|
||||||
teci.world
|
teci.world
|
||||||
theapex.social
|
theapex.social
|
||||||
|
thechimp.zone
|
||||||
|
thenobody.club
|
||||||
thepostearthdestination.com
|
thepostearthdestination.com
|
||||||
tkammer.de
|
tkammer.de
|
||||||
trumpislovetrumpis.life
|
trumpislovetrumpis.life
|
||||||
truthsocial.co.in
|
truthsocial.co.in
|
||||||
urchan.org
|
usualsuspects.lol
|
||||||
varishangout.net
|
varishangout.net
|
||||||
whinge.house
|
vtuberfan.social
|
||||||
whinge.town
|
|
||||||
wideboys.org
|
|
||||||
wolfgirl.bar
|
wolfgirl.bar
|
||||||
xn--p1abe3d.xn--80asehdb
|
xn--p1abe3d.xn--80asehdb
|
||||||
yggdrasil.social
|
yggdrasil.social
|
||||||
youjo.love
|
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){
|
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||||
Notification.Builder bldr=new Notification.Builder(this)
|
Notification.Builder bldr=new Notification.Builder(this)
|
||||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||||
.setContentTitle(status.account.displayName)
|
.setContentTitle(status.account.getDisplayName())
|
||||||
.setContentText(HtmlParser.strip(status.content))
|
.setContentText(HtmlParser.strip(status.content))
|
||||||
.setOngoing(!dismissable)
|
.setOngoing(!dismissable)
|
||||||
.setShowWhen(false)
|
.setShowWhen(false)
|
||||||
@@ -281,7 +281,7 @@ public class AudioPlayerService extends Service{
|
|||||||
|
|
||||||
if(playerReady){
|
if(playerReady){
|
||||||
boolean isPlaying=player.isPlaying();
|
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),
|
getString(isPlaying ? R.string.pause : R.string.play),
|
||||||
PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE))
|
PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE))
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.jsoup.internal.StringUtil;
|
import org.jsoup.internal.StringUtil;
|
||||||
|
|
||||||
@@ -32,10 +32,9 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||||||
UiUtils.setUserPreferredTheme(this);
|
UiUtils.setUserPreferredTheme(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
|
|
||||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||||
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
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();
|
boolean isOpenable = isFediUrl || fediHandle.isPresent();
|
||||||
|
|
||||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android;
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
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.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@@ -13,12 +14,12 @@ import com.google.gson.JsonSyntaxException;
|
|||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
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.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.ContentType;
|
import org.joinmastodon.android.model.ContentType;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.utils.ColorPalette;
|
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -27,6 +28,9 @@ import java.util.HashSet;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
public class GlobalUserPreferences{
|
public class GlobalUserPreferences{
|
||||||
private static final String TAG="GlobalUserPreferences";
|
private static final String TAG="GlobalUserPreferences";
|
||||||
|
|
||||||
@@ -41,7 +45,6 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean showNewPostsButton;
|
public static boolean showNewPostsButton;
|
||||||
public static boolean toolbarMarquee;
|
public static boolean toolbarMarquee;
|
||||||
public static boolean disableSwipe;
|
public static boolean disableSwipe;
|
||||||
public static boolean voteButtonForSingleChoice;
|
|
||||||
public static boolean enableDeleteNotifications;
|
public static boolean enableDeleteNotifications;
|
||||||
public static boolean translateButtonOpenedOnly;
|
public static boolean translateButtonOpenedOnly;
|
||||||
public static boolean uniformNotificationIcon;
|
public static boolean uniformNotificationIcon;
|
||||||
@@ -53,20 +56,23 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean collapseLongPosts;
|
public static boolean collapseLongPosts;
|
||||||
public static boolean spectatorMode;
|
public static boolean spectatorMode;
|
||||||
public static boolean autoHideFab;
|
public static boolean autoHideFab;
|
||||||
public static boolean compactReblogReplyLine;
|
|
||||||
public static boolean allowRemoteLoading;
|
public static boolean allowRemoteLoading;
|
||||||
public static boolean forwardReportDefault;
|
public static boolean forwardReportDefault;
|
||||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||||
public static ColorPreference color;
|
|
||||||
public static boolean disableM3PillActiveIndicator;
|
public static boolean disableM3PillActiveIndicator;
|
||||||
public static boolean showNavigationLabels;
|
public static boolean showNavigationLabels;
|
||||||
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
|
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
|
||||||
public static boolean overlayMedia;
|
public static boolean overlayMedia;
|
||||||
|
public static boolean showSuicideHelp;
|
||||||
|
public static boolean underlinedLinks;
|
||||||
|
public static ColorPreference color;
|
||||||
|
public static boolean likeIcon;
|
||||||
|
|
||||||
// MOSHIDON
|
// MOSHIDON
|
||||||
public static boolean showDividers;
|
public static boolean showDividers;
|
||||||
public static boolean relocatePublishButton;
|
public static boolean relocatePublishButton;
|
||||||
public static boolean defaultToUnlistedReplies;
|
public static boolean defaultToUnlistedReplies;
|
||||||
|
public static boolean doubleTapToSearch;
|
||||||
public static boolean doubleTapToSwipe;
|
public static boolean doubleTapToSwipe;
|
||||||
public static boolean confirmBeforeReblog;
|
public static boolean confirmBeforeReblog;
|
||||||
public static boolean hapticFeedback;
|
public static boolean hapticFeedback;
|
||||||
@@ -74,11 +80,18 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean swapBookmarkWithBoostAction;
|
public static boolean swapBookmarkWithBoostAction;
|
||||||
public static boolean loadRemoteAccountFollowers;
|
public static boolean loadRemoteAccountFollowers;
|
||||||
public static boolean mentionRebloggerAutomatically;
|
public static boolean mentionRebloggerAutomatically;
|
||||||
|
public static boolean showPostsWithoutAlt;
|
||||||
|
public static boolean showMediaPreview;
|
||||||
|
|
||||||
public static SharedPreferences getPrefs(){
|
public static SharedPreferences getPrefs(){
|
||||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences getPreReplyPrefs(){
|
||||||
|
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static <T> T fromJson(String json, Type type, T orElse){
|
public static <T> T fromJson(String json, Type type, T orElse){
|
||||||
if(json==null) return orElse;
|
if(json==null) return orElse;
|
||||||
try{
|
try{
|
||||||
@@ -111,7 +124,6 @@ public class GlobalUserPreferences{
|
|||||||
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
||||||
toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
|
toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
|
||||||
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
||||||
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
|
||||||
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
||||||
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
||||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||||
@@ -123,7 +135,6 @@ public class GlobalUserPreferences{
|
|||||||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
|
||||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||||
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
||||||
@@ -133,13 +144,17 @@ public class GlobalUserPreferences{
|
|||||||
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
|
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
|
||||||
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
|
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
|
||||||
overlayMedia=prefs.getBoolean("overlayMedia", false);
|
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
|
// MOSHIDON
|
||||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||||
showDividers =prefs.getBoolean("showDividers", false);
|
showDividers =prefs.getBoolean("showDividers", false);
|
||||||
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
|
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
|
||||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
|
||||||
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
|
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
|
||||||
|
doubleTapToSearch =prefs.getBoolean("doubleTapToSearch", true);
|
||||||
doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true);
|
doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true);
|
||||||
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
|
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
|
||||||
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
||||||
@@ -147,6 +162,9 @@ public class GlobalUserPreferences{
|
|||||||
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
|
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
|
||||||
loadRemoteAccountFollowers=prefs.getBoolean("loadRemoteAccountFollowers", true);
|
loadRemoteAccountFollowers=prefs.getBoolean("loadRemoteAccountFollowers", true);
|
||||||
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
|
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
|
||||||
|
showPostsWithoutAlt=prefs.getBoolean("showPostsWithoutAlt", true);
|
||||||
|
showMediaPreview=prefs.getBoolean("showMediaPreview", true);
|
||||||
|
|
||||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||||
|
|
||||||
|
|
||||||
@@ -159,18 +177,11 @@ public class GlobalUserPreferences{
|
|||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
int migrationLevel=prefs.getInt("migrationLevel", BuildConfig.VERSION_CODE);
|
||||||
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
|
if(migrationLevel < 61)
|
||||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.MATERIAL3.name()));
|
migrateToUpstreamVersion61();
|
||||||
}else{
|
if(migrationLevel < BuildConfig.VERSION_CODE)
|
||||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PURPLE.name()));
|
prefs.edit().putInt("migrationLevel", BuildConfig.VERSION_CODE).apply();
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException|ClassCastException ignored) {
|
|
||||||
// invalid color name or color was previously saved as integer
|
|
||||||
color=ColorPreference.PURPLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void save(){
|
public static void save(){
|
||||||
@@ -200,8 +211,6 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||||
.putBoolean("spectatorMode", spectatorMode)
|
.putBoolean("spectatorMode", spectatorMode)
|
||||||
.putBoolean("autoHideFab", autoHideFab)
|
.putBoolean("autoHideFab", autoHideFab)
|
||||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
|
||||||
.putString("color", color.name())
|
|
||||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||||
.putBoolean("forwardReportDefault", forwardReportDefault)
|
.putBoolean("forwardReportDefault", forwardReportDefault)
|
||||||
@@ -211,24 +220,80 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
|
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
|
||||||
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
|
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
|
||||||
.putBoolean("overlayMedia", overlayMedia)
|
.putBoolean("overlayMedia", overlayMedia)
|
||||||
|
.putBoolean("showSuicideHelp", showSuicideHelp)
|
||||||
|
.putBoolean("underlinedLinks", underlinedLinks)
|
||||||
|
.putString("color", color.name())
|
||||||
|
.putBoolean("likeIcon", likeIcon)
|
||||||
|
|
||||||
// MOSHIDON
|
// MOSHIDON
|
||||||
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
|
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
|
||||||
|
.putBoolean("doubleTapToSearch", doubleTapToSearch)
|
||||||
.putBoolean("doubleTapToSwipe", doubleTapToSwipe)
|
.putBoolean("doubleTapToSwipe", doubleTapToSwipe)
|
||||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
|
||||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||||
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
|
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
|
||||||
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
|
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
|
||||||
|
.putBoolean("hapticFeedback", hapticFeedback)
|
||||||
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
|
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
|
||||||
.putBoolean("showDividers", showDividers)
|
.putBoolean("showDividers", showDividers)
|
||||||
.putBoolean("relocatePublishButton", relocatePublishButton)
|
.putBoolean("relocatePublishButton", relocatePublishButton)
|
||||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||||
.putInt("theme", theme.ordinal())
|
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
|
||||||
|
.putBoolean("showMediaPreview", showMediaPreview)
|
||||||
|
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||||
|
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
|
||||||
|
return true;
|
||||||
|
if(account==null)
|
||||||
|
return false;
|
||||||
|
String accountKey=account.acct;
|
||||||
|
if(!accountKey.contains("@"))
|
||||||
|
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||||
|
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||||
|
String key;
|
||||||
|
if(account==null){
|
||||||
|
key="opt_out_"+type;
|
||||||
|
}else{
|
||||||
|
String accountKey=account.acct;
|
||||||
|
if(!accountKey.contains("@"))
|
||||||
|
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||||
|
key="opt_out_"+type+"_"+accountKey.toLowerCase();
|
||||||
|
}
|
||||||
|
getPreReplyPrefs().edit().putBoolean(key, true).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThemePreference{
|
||||||
|
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(){
|
private static void migrateToUpstreamVersion61(){
|
||||||
Log.d(TAG, "Migrating preferences to upstream version 61!!");
|
Log.d(TAG, "Migrating preferences to upstream version 61!!");
|
||||||
|
|
||||||
@@ -275,53 +340,7 @@ public class GlobalUserPreferences{
|
|||||||
|
|
||||||
localPrefs.save();
|
localPrefs.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit().putInt("migrationLevel", 61).apply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ColorPreference{
|
//endregion
|
||||||
MATERIAL3,
|
|
||||||
PINK,
|
|
||||||
PURPLE,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.BadParcelableException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
@@ -39,62 +40,46 @@ import org.joinmastodon.android.utils.ProvidesAssistContent;
|
|||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
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.FragmentStackActivity;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
|
||||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||||
|
private static final String TAG="MainActivity";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||||
UiUtils.setUserPreferredTheme(this);
|
AccountSession session=getCurrentSession();
|
||||||
|
UiUtils.setUserPreferredTheme(this, session);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if(savedInstanceState==null){
|
Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
|
||||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
Thread.setDefaultUncaughtExceptionHandler((t, e)->{
|
||||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
File file=new File(MastodonApp.context.getFilesDir(), "crash.log");
|
||||||
}else{
|
try(FileOutputStream out=new FileOutputStream(file)){
|
||||||
AccountSession session;
|
PrintWriter writer=new PrintWriter(out);
|
||||||
Bundle args=new Bundle();
|
writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")");
|
||||||
Intent intent=getIntent();
|
writer.println(Instant.now().toString());
|
||||||
if(intent.hasExtra("fromExternalShare")) {
|
writer.println();
|
||||||
AccountSessionManager.getInstance()
|
e.printStackTrace(writer);
|
||||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
writer.flush();
|
||||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
}catch(IOException x){
|
||||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
Log.e(TAG, "Error writing crash.log", x);
|
||||||
showFragmentForExternalShare(intent.getExtras());
|
}finally{
|
||||||
return;
|
defaultHandler.uncaughtException(t, e);
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(savedInstanceState==null){
|
||||||
|
restartHomeFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(GithubSelfUpdater.needSelfUpdating()){
|
if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
@@ -147,11 +132,11 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
session=AccountSessionManager.get(accountID);
|
session=AccountSessionManager.get(accountID);
|
||||||
if(session==null || !session.activated)
|
if(session==null || !session.activated)
|
||||||
return;
|
return;
|
||||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
|
||||||
new GetSearchResults(q, null, true)
|
new GetSearchResults(q, type, true, null, 0, 0)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults result){
|
public void onSuccess(SearchResults result){
|
||||||
@@ -285,4 +270,87 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
Fragment fragment = getCurrentFragment();
|
Fragment fragment = getCurrentFragment();
|
||||||
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
|
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.opengl.Visibility;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
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.Status;
|
||||||
import org.joinmastodon.android.model.StatusPrivacy;
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
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.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
@@ -135,7 +133,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
|
|
||||||
if(intent.hasExtra("notification")){
|
if(intent.hasExtra("notification")){
|
||||||
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||||
String statusID=notification.status.id;
|
|
||||||
|
String statusID = null;
|
||||||
|
if(notification != null && notification.status != null)
|
||||||
|
statusID=notification.status.id;
|
||||||
|
|
||||||
if (statusID != null) {
|
if (statusID != null) {
|
||||||
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
||||||
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
||||||
@@ -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
|
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
|
||||||
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
|
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, account, notification), account.getID(), notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||||
@@ -182,6 +184,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
||||||
.map(type->{
|
.map(type->{
|
||||||
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
|
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
channel.setLightColor(context.getColor(R.color.primary_700));
|
||||||
|
channel.enableLights(true);
|
||||||
channel.setGroup(accountID);
|
channel.setGroup(accountID);
|
||||||
return channel;
|
return channel;
|
||||||
})
|
})
|
||||||
@@ -211,11 +215,12 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
.setShowWhen(true)
|
.setShowWhen(true)
|
||||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
.setLights(context.getColor(R.color.primary_700), 500, 1000)
|
||||||
.setColor(context.getColor(R.color.shortcut_icon_background));
|
.setColor(context.getColor(R.color.shortcut_icon_background));
|
||||||
|
|
||||||
if (!GlobalUserPreferences.uniformNotificationIcon) {
|
if (!GlobalUserPreferences.uniformNotificationIcon) {
|
||||||
builder.setSmallIcon(switch (pn.notificationType) {
|
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 REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
|
||||||
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled;
|
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled;
|
||||||
case MENTION -> R.drawable.ic_fluent_mention_24_filled;
|
case MENTION -> R.drawable.ic_fluent_mention_24_filled;
|
||||||
@@ -336,11 +341,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
|
|
||||||
CreateStatus.Request req=new CreateStatus.Request();
|
CreateStatus.Request req=new CreateStatus.Request();
|
||||||
req.status = initialText + input.toString();
|
req.status = initialText + input.toString();
|
||||||
req.language = preferences.postingDefaultLanguage;
|
req.language = notification.status.language;
|
||||||
req.visibility = preferences.postingDefaultVisibility;
|
req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility);
|
||||||
req.inReplyToId = notification.status.id;
|
req.inReplyToId = notification.status.id;
|
||||||
|
|
||||||
if (!notification.status.spoilerText.isEmpty() &&
|
if (notification.status.hasSpoiler() &&
|
||||||
(GlobalUserPreferences.prefixReplies == ALWAYS
|
(GlobalUserPreferences.prefixReplies == ALWAYS
|
||||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
|
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
|
||||||
&& !notification.status.spoilerText.startsWith("re: ")) {
|
&& !notification.status.spoilerText.startsWith("re: ")) {
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||||||
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) {
|
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) {
|
||||||
// Called when a new endpoint be used for sending push messages
|
// Called when a new endpoint be used for sending push messages
|
||||||
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance);
|
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance);
|
||||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||||
if (account != null)
|
if (account != null)
|
||||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
account.getPushSubscriptionManager().registerAccountForPush(null, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -37,7 +37,7 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||||||
// called when the registration is not possible, eg. no network
|
// called when the registration is not possible, eg. no network
|
||||||
Log.d(TAG, "onRegistrationFailed: " + instance);
|
Log.d(TAG, "onRegistrationFailed: " + instance);
|
||||||
//re-register for gcm
|
//re-register for gcm
|
||||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||||
if (account != null)
|
if (account != null)
|
||||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||||||
// called when this application is unregistered from receiving push messages
|
// called when this application is unregistered from receiving push messages
|
||||||
Log.d(TAG, "onUnregistered: " + instance);
|
Log.d(TAG, "onUnregistered: " + instance);
|
||||||
//re-register for gcm
|
//re-register for gcm
|
||||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||||
if (account != null)
|
if (account != null)
|
||||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,10 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||||||
@Override
|
@Override
|
||||||
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) {
|
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) {
|
||||||
// Called when a new message is received. The message contains the full POST body of the push message
|
// Called when a new message is received. The message contains the full POST body of the push message
|
||||||
AccountSession account = AccountSessionManager.getInstance().getAccount(instance);
|
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||||
|
|
||||||
|
if (account == null)
|
||||||
|
return;
|
||||||
|
|
||||||
//this is stupid
|
//this is stupid
|
||||||
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
|
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
|
||||||
@@ -69,7 +72,7 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||||||
result.items
|
result.items
|
||||||
.stream()
|
.stream()
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
|
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, account, value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,23 +9,35 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.SearchResult;
|
import org.joinmastodon.android.model.SearchResult;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
@@ -43,6 +55,7 @@ public class CacheController{
|
|||||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||||
private boolean loadingNotifications;
|
private boolean loadingNotifications;
|
||||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||||
|
private List<FollowList> lists;
|
||||||
|
|
||||||
private static final int POST_FLAG_GAP_AFTER=1;
|
private static final int POST_FLAG_GAP_AFTER=1;
|
||||||
|
|
||||||
@@ -69,12 +82,11 @@ public class CacheController{
|
|||||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||||
status.postprocess();
|
status.postprocess();
|
||||||
int flags=cursor.getInt(1);
|
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;
|
newMaxID=status.id;
|
||||||
result.add(status);
|
result.add(status);
|
||||||
}while(cursor.moveToNext());
|
}while(cursor.moveToNext());
|
||||||
String _newMaxID=newMaxID;
|
String _newMaxID=newMaxID;
|
||||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
|
||||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,9 +98,7 @@ public class CacheController{
|
|||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
callback.onSuccess(new CacheablePaginatedResponse<>(result, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
|
||||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
|
||||||
putHomeTimeline(result, maxID==null);
|
putHomeTimeline(result, maxID==null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +126,14 @@ public class CacheController{
|
|||||||
values.put("id", s.id);
|
values.put("id", s.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||||
int flags=0;
|
int flags=0;
|
||||||
if(s.hasGapAfter)
|
if(Objects.equals(s.hasGapAfter, s.id))
|
||||||
flags|=POST_FLAG_GAP_AFTER;
|
flags|=POST_FLAG_GAP_AFTER;
|
||||||
values.put("flags", flags);
|
values.put("flags", flags);
|
||||||
values.put("time", s.createdAt.getEpochSecond());
|
values.put("time", s.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
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){
|
public void deleteStatus(String id){
|
||||||
runOnDbThread((db)->{
|
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});
|
db.delete("home_timeline", "`id`=?", new String[]{id});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -326,6 +360,99 @@ public class CacheController{
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reloadLists(Callback<List<FollowList>> callback){
|
||||||
|
new GetLists()
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> result){
|
||||||
|
result.sort(Comparator.comparing(l->l.title));
|
||||||
|
lists=result;
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onSuccess(result);
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onError(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FollowList> loadListsFromFile(){
|
||||||
|
File file=getListsFile();
|
||||||
|
if(!file.exists())
|
||||||
|
return null;
|
||||||
|
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||||
|
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||||
|
}catch(Exception x){
|
||||||
|
Log.w(TAG, "failed to read lists from cache file", x);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeListsToFile(){
|
||||||
|
databaseThread.postRunnable(()->{
|
||||||
|
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||||
|
MastodonAPIController.gson.toJson(lists, out);
|
||||||
|
}catch(IOException x){
|
||||||
|
Log.w(TAG, "failed to write lists to cache file", x);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getLists(Callback<List<FollowList>> callback){
|
||||||
|
if(lists!=null){
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onSuccess(lists);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
databaseThread.postRunnable(()->{
|
||||||
|
List<FollowList> lists=loadListsFromFile();
|
||||||
|
if(lists!=null){
|
||||||
|
this.lists=lists;
|
||||||
|
if(callback!=null)
|
||||||
|
uiHandler.post(()->callback.onSuccess(lists));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reloadLists(callback);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getListsFile(){
|
||||||
|
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addList(FollowList list){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
lists.add(list);
|
||||||
|
lists.sort(Comparator.comparing(l->l.title));
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteList(String id){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
lists.removeIf(l->l.id.equals(id));
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateList(FollowList list){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
for(int i=0;i<lists.size();i++){
|
||||||
|
if(lists.get(i).id.equals(list.id)){
|
||||||
|
lists.set(i, list);
|
||||||
|
lists.sort(Comparator.comparing(l->l.title));
|
||||||
|
writeListsToFile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class DatabaseHelper extends SQLiteOpenHelper{
|
private class DatabaseHelper extends SQLiteOpenHelper{
|
||||||
|
|
||||||
public DatabaseHelper(){
|
public DatabaseHelper(){
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ public class MastodonAPIController{
|
|||||||
.create();
|
.create();
|
||||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||||
.readTimeout(5, TimeUnit.MINUTES)
|
.connectTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private AccountSession session;
|
private AccountSession session;
|
||||||
@@ -113,24 +115,24 @@ public class MastodonAPIController{
|
|||||||
}
|
}
|
||||||
|
|
||||||
Request hreq=builder.build();
|
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){
|
synchronized(req){
|
||||||
req.okhttpCall=call;
|
req.okhttpCall=call;
|
||||||
}
|
}
|
||||||
if(req.timeout>0){
|
|
||||||
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
|
||||||
|
|
||||||
call.enqueue(new Callback(){
|
call.enqueue(new Callback(){
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||||
if(call.isCanceled())
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
@@ -139,10 +141,10 @@ public class MastodonAPIController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||||
if(call.isCanceled())
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
@@ -153,7 +155,7 @@ public class MastodonAPIController{
|
|||||||
try{
|
try{
|
||||||
if(BuildConfig.DEBUG){
|
if(BuildConfig.DEBUG){
|
||||||
JsonElement respJson=JsonParser.parseReader(reader);
|
JsonElement respJson=JsonParser.parseReader(reader);
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
Log.d(TAG, logTag(session)+"response body: "+respJson);
|
||||||
if(req.respTypeToken!=null)
|
if(req.respTypeToken!=null)
|
||||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||||
else if(req.respClass!=null)
|
else if(req.respClass!=null)
|
||||||
@@ -175,7 +177,7 @@ public class MastodonAPIController{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,19 +186,19 @@ public class MastodonAPIController{
|
|||||||
req.validateAndPostprocessResponse(respObj, response);
|
req.validateAndPostprocessResponse(respObj, response);
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
|
||||||
|
|
||||||
req.onSuccess(respObj);
|
req.onSuccess(respObj);
|
||||||
}else{
|
}else{
|
||||||
try{
|
try{
|
||||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
Log.w(TAG, logTag(session)+response+" received error: "+error);
|
||||||
if(error.has("details")){
|
if(error.has("details")){
|
||||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||||
@@ -231,7 +233,7 @@ public class MastodonAPIController{
|
|||||||
});
|
});
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
|
||||||
req.onError(x.getLocalizedMessage(), 0, x);
|
req.onError(x.getLocalizedMessage(), 0, x);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -244,4 +246,8 @@ public class MastodonAPIController{
|
|||||||
public static OkHttpClient getHttpClient(){
|
public static OkHttpClient getHttpClient(){
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String logTag(AccountSession session){
|
||||||
|
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,8 +153,9 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
headers.put(key, value);
|
headers.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setTimeout(long timeout){
|
public MastodonAPIRequest<T> setTimeout(long timeout){
|
||||||
this.timeout=timeout;
|
this.timeout=timeout;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getPathPrefix(){
|
protected String getPathPrefix(){
|
||||||
@@ -179,6 +180,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
|
if(requestBody instanceof RequestBody rb)
|
||||||
|
return rb;
|
||||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,17 +125,16 @@ public class PushSubscriptionManager{
|
|||||||
// this function is used for registering push notifications using FCM
|
// this function is used for registering push notifications using FCM
|
||||||
// to avoid NonFreeNet in F-Droid, this registration is disabled in it
|
// to avoid NonFreeNet in F-Droid, this registration is disabled in it
|
||||||
// see https://github.com/LucasGGamerM/moshidon/issues/206 for more context
|
// see https://github.com/LucasGGamerM/moshidon/issues/206 for more context
|
||||||
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease"))
|
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease") || TextUtils.isEmpty(deviceToken)){
|
||||||
|
Log.d(TAG, "Skipping registering for FCM push notifications");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(TextUtils.isEmpty(deviceToken))
|
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/";
|
||||||
throw new IllegalStateException("No device push token available");
|
|
||||||
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
|
||||||
registerAccountForPush(subscription, endpoint);
|
registerAccountForPush(subscription, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerAccountForPush(PushSubscription subscription, String endpoint){
|
public void registerAccountForPush(PushSubscription subscription, String endpoint){
|
||||||
|
|
||||||
MastodonAPIController.runInBackground(()->{
|
MastodonAPIController.runInBackground(()->{
|
||||||
Log.d(TAG, "registerAccountForPush: started for "+accountID);
|
Log.d(TAG, "registerAccountForPush: started for "+accountID);
|
||||||
String encodedPublicKey, encodedAuthKey, pushAccountID;
|
String encodedPublicKey, encodedAuthKey, pushAccountID;
|
||||||
@@ -164,7 +163,13 @@ public class PushSubscriptionManager{
|
|||||||
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
|
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new RegisterForPushNotifications(endpoint,
|
|
||||||
|
//work-around for adding the randomAccountId
|
||||||
|
String newEndpoint = endpoint;
|
||||||
|
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/"))
|
||||||
|
newEndpoint += pushAccountID;
|
||||||
|
|
||||||
|
new RegisterForPushNotifications(newEndpoint,
|
||||||
encodedPublicKey,
|
encodedPublicKey,
|
||||||
encodedAuthKey,
|
encodedAuthKey,
|
||||||
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
|
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import org.joinmastodon.android.E;
|
|||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
||||||
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
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.api.requests.statuses.SetStatusReblogged;
|
||||||
|
import org.joinmastodon.android.events.ReblogDeletedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
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.Status;
|
||||||
import org.joinmastodon.android.model.StatusPrivacy;
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@ public class StatusInteractionController{
|
|||||||
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
||||||
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
||||||
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
|
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
|
||||||
|
private final HashMap<String, SetStatusMuted> runningMuteRequests=new HashMap<>();
|
||||||
|
|
||||||
public StatusInteractionController(String accountID, boolean updateCounters) {
|
public StatusInteractionController(String accountID, boolean updateCounters) {
|
||||||
this.accountID=accountID;
|
this.accountID=accountID;
|
||||||
@@ -48,7 +53,7 @@ public class StatusInteractionController{
|
|||||||
runningFavoriteRequests.remove(status.id);
|
runningFavoriteRequests.remove(status.id);
|
||||||
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
||||||
cb.accept(result);
|
cb.accept(result);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -57,13 +62,13 @@ public class StatusInteractionController{
|
|||||||
error.showToast(MastodonApp.context);
|
error.showToast(MastodonApp.context);
|
||||||
status.favourited=!favorited;
|
status.favourited=!favorited;
|
||||||
cb.accept(status);
|
cb.accept(status);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
runningFavoriteRequests.put(status.id, req);
|
runningFavoriteRequests.put(status.id, req);
|
||||||
status.favourited=favorited;
|
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){
|
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
|
||||||
@@ -78,11 +83,15 @@ public class StatusInteractionController{
|
|||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status reblog){
|
public void onSuccess(Status reblog){
|
||||||
Status result = reblog.getContentStatus();
|
Status result=reblog.getContentStatus();
|
||||||
runningReblogRequests.remove(status.id);
|
runningReblogRequests.remove(status.id);
|
||||||
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
||||||
cb.accept(result);
|
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
|
@Override
|
||||||
@@ -91,13 +100,13 @@ public class StatusInteractionController{
|
|||||||
error.showToast(MastodonApp.context);
|
error.showToast(MastodonApp.context);
|
||||||
status.reblogged=!reblogged;
|
status.reblogged=!reblogged;
|
||||||
cb.accept(status);
|
cb.accept(status);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
runningReblogRequests.put(status.id, req);
|
runningReblogRequests.put(status.id, req);
|
||||||
status.reblogged=reblogged;
|
status.reblogged=reblogged;
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBookmarked(Status status, boolean bookmarked){
|
public void setBookmarked(Status status, boolean bookmarked){
|
||||||
@@ -118,7 +127,7 @@ public class StatusInteractionController{
|
|||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
runningBookmarkRequests.remove(status.id);
|
runningBookmarkRequests.remove(status.id);
|
||||||
cb.accept(result);
|
cb.accept(result);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -127,12 +136,12 @@ public class StatusInteractionController{
|
|||||||
error.showToast(MastodonApp.context);
|
error.showToast(MastodonApp.context);
|
||||||
status.bookmarked=!bookmarked;
|
status.bookmarked=!bookmarked;
|
||||||
cb.accept(status);
|
cb.accept(status);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
runningBookmarkRequests.put(status.id, req);
|
runningBookmarkRequests.put(status.id, req);
|
||||||
status.bookmarked=bookmarked;
|
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,16 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
public class GetAccountBlocks extends HeaderPaginationRequest<Account>{
|
||||||
|
public GetAccountBlocks(String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/blocks", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<>(){});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
public class GetAccountMutes extends HeaderPaginationRequest<Account>{
|
||||||
|
public GetAccountMutes(String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/mutes/", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
|
||||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
|
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
|
||||||
super(HttpMethod.POST, "/accounts", Token.class);
|
super(HttpMethod.POST, "/accounts", Token.class);
|
||||||
setRequestBody(new Body(username, email, password, locale, reason, timezone));
|
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Body{
|
private static class Body{
|
||||||
public String username, email, password, locale, reason, timeZone;
|
public String username, email, password, locale, reason, timeZone, inviteCode;
|
||||||
public boolean agreement=true;
|
public boolean agreement=true;
|
||||||
|
|
||||||
public Body(String username, String email, String password, String locale, String reason, String timeZone){
|
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
|
||||||
this.username=username;
|
this.username=username;
|
||||||
this.email=email;
|
this.email=email;
|
||||||
this.password=password;
|
this.password=password;
|
||||||
this.locale=locale;
|
this.locale=locale;
|
||||||
this.reason=reason;
|
this.reason=reason;
|
||||||
this.timeZone=timeZone;
|
this.timeZone=timeZone;
|
||||||
|
this.inviteCode=inviteCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
|
||||||
|
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
|
||||||
|
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
|
||||||
|
addQueryParameter("q", q);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
if(offset>0)
|
||||||
|
addQueryParameter("offset", offset+"");
|
||||||
|
if(resolve)
|
||||||
|
addQueryParameter("resolve", "true");
|
||||||
|
if(following)
|
||||||
|
addQueryParameter("following", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,21 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
|
|
||||||
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
|
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
|
||||||
public SetAccountMuted(String id, boolean muted, long duration){
|
public SetAccountMuted(String id, boolean muted, long duration, boolean muteNotifications){
|
||||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
|
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
|
||||||
setRequestBody(new Request(duration));
|
if(muted)
|
||||||
|
setRequestBody(new Request(duration, muteNotifications));
|
||||||
|
else{
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Request{
|
private static class Request{
|
||||||
public long duration;
|
public long duration;
|
||||||
public Request(long duration){
|
public boolean muteNotifications;
|
||||||
|
public Request(long duration, boolean muteNotifications){
|
||||||
this.duration=duration;
|
this.duration=duration;
|
||||||
|
this.muteNotifications=muteNotifications;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
private Uri avatar, cover;
|
private Uri avatar, cover;
|
||||||
private File avatarFile, coverFile;
|
private File avatarFile, coverFile;
|
||||||
private List<AccountField> fields;
|
private List<AccountField> fields;
|
||||||
|
private Boolean discoverable, indexable;
|
||||||
|
|
||||||
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
||||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||||
@@ -41,6 +42,12 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
this.fields=fields;
|
this.fields=fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
|
||||||
|
this.discoverable=discoverable;
|
||||||
|
this.indexable=indexable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||||
@@ -58,15 +65,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
}else if(coverFile!=null){
|
}else if(coverFile!=null){
|
||||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||||
}
|
}
|
||||||
if(fields.isEmpty()){
|
if(fields!=null){
|
||||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
if(fields.isEmpty()){
|
||||||
}else{
|
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||||
int i=0;
|
}else{
|
||||||
for(AccountField field:fields){
|
int i=0;
|
||||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
for(AccountField field:fields){
|
||||||
i++;
|
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(discoverable!=null)
|
||||||
|
bldr.addFormDataPart("discoverable", discoverable.toString());
|
||||||
|
if(indexable!=null)
|
||||||
|
bldr.addFormDataPart("indexable", indexable.toString());
|
||||||
|
|
||||||
return bldr.build();
|
return bldr.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import org.joinmastodon.android.model.Preferences;
|
|||||||
import org.joinmastodon.android.model.StatusPrivacy;
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
|
|
||||||
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
|
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable, Boolean indexable){
|
||||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||||
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
setRequestBody(new Request(locked, discoverable, indexable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Request{
|
private static class Request{
|
||||||
public Boolean locked, discoverable;
|
public Boolean locked, discoverable, indexable;
|
||||||
public RequestSource source;
|
public RequestSource source;
|
||||||
|
|
||||||
public Request(Boolean locked, Boolean discoverable, RequestSource source){
|
public Request(Boolean locked, Boolean discoverable, Boolean indexable, RequestSource source){
|
||||||
this.locked=locked;
|
this.locked=locked;
|
||||||
this.discoverable=discoverable;
|
this.discoverable=discoverable;
|
||||||
|
this.indexable=indexable;
|
||||||
this.source=source;
|
this.source=source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import org.joinmastodon.android.model.FilterContext;
|
|||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
|
@Keep
|
||||||
class FilterRequest{
|
class FilterRequest{
|
||||||
public String title;
|
public String title;
|
||||||
public EnumSet<FilterContext> context;
|
public EnumSet<FilterContext> context;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
|
|||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
|
@Keep
|
||||||
class KeywordAttribute{
|
class KeywordAttribute{
|
||||||
public String id;
|
public String id;
|
||||||
@SerializedName("_destroy")
|
@SerializedName("_destroy")
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class AddAccountsToList extends MastodonAPIRequest<Object> {
|
import java.nio.charset.StandardCharsets;
|
||||||
public AddAccountsToList(String listId, List<String> accountIds){
|
import java.util.Collection;
|
||||||
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
|
|
||||||
Request req = new Request();
|
|
||||||
req.accountIds = accountIds;
|
|
||||||
setRequestBody(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Request{
|
import okhttp3.FormBody;
|
||||||
public List<String> accountIds;
|
|
||||||
}
|
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
|
||||||
|
public AddAccountsToList(String listID, Collection<String> accountIDs){
|
||||||
|
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
|
||||||
|
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||||
|
for(String id:accountIDs){
|
||||||
|
builder.add("account_ids[]", id);
|
||||||
|
}
|
||||||
|
setRequestBody(builder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class CreateList extends MastodonAPIRequest<ListTimeline> {
|
public class CreateList extends MastodonAPIRequest<FollowList>{
|
||||||
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
super(HttpMethod.POST, "/lists", FollowList.class);
|
||||||
Request req = new Request();
|
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||||
req.title = title;
|
|
||||||
req.exclusive = exclusive;
|
|
||||||
req.repliesPolicy = repliesPolicy;
|
|
||||||
setRequestBody(req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Request {
|
private static class Request{
|
||||||
public String title;
|
public String title;
|
||||||
|
public FollowList.RepliesPolicy repliesPolicy;
|
||||||
public boolean exclusive;
|
public boolean exclusive;
|
||||||
public ListTimeline.RepliesPolicy repliesPolicy;
|
|
||||||
|
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
|
this.title=title;
|
||||||
|
this.repliesPolicy=repliesPolicy;
|
||||||
|
this.exclusive=exclusive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
|
||||||
|
|
||||||
public class DeleteList extends MastodonAPIRequest<Object> {
|
public class DeleteList extends ResultlessMastodonAPIRequest{
|
||||||
public DeleteList(String id) {
|
public DeleteList(String id){
|
||||||
super(HttpMethod.DELETE, "/lists/" + id, Object.class);
|
super(HttpMethod.DELETE, "/lists/"+id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class GetList extends MastodonAPIRequest<ListTimeline> {
|
public class GetList extends MastodonAPIRequest<FollowList> {
|
||||||
public GetList(String id) {
|
public GetList(String id) {
|
||||||
super(HttpMethod.GET, "/lists/" + id, ListTimeline.class);
|
super(HttpMethod.GET, "/lists/" + id, FollowList.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
public class GetListAccounts extends HeaderPaginationRequest<Account>{
|
||||||
|
public GetListAccounts(String listID, String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/lists/"+listID+"/accounts", new TypeToken<>(){});
|
||||||
|
if(!TextUtils.isEmpty(maxID))
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
addQueryParameter("limit", String.valueOf(limit));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ package org.joinmastodon.android.api.requests.lists;
|
|||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
|
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
|
||||||
public GetLists() {
|
public GetLists() {
|
||||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
|
import java.nio.charset.StandardCharsets;
|
||||||
public RemoveAccountsFromList(String listId, List<String> accountIds){
|
import java.util.Collection;
|
||||||
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
|
|
||||||
Request req = new Request();
|
|
||||||
req.accountIds = accountIds;
|
|
||||||
setRequestBody(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Request{
|
import okhttp3.FormBody;
|
||||||
public List<String> accountIds;
|
|
||||||
}
|
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
|
||||||
|
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
|
||||||
|
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
|
||||||
|
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||||
|
for(String id:accountIDs){
|
||||||
|
builder.add("account_ids[]", id);
|
||||||
|
}
|
||||||
|
setRequestBody(builder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package org.joinmastodon.android.api.requests.lists;
|
package org.joinmastodon.android.api.requests.lists;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
|
public class UpdateList extends MastodonAPIRequest<FollowList>{
|
||||||
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
|
||||||
CreateList.Request req = new CreateList.Request();
|
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||||
req.title = title;
|
}
|
||||||
req.exclusive = exclusive;
|
|
||||||
req.repliesPolicy = repliesPolicy;
|
private static class Request{
|
||||||
setRequestBody(req);
|
public String title;
|
||||||
|
public FollowList.RepliesPolicy repliesPolicy;
|
||||||
|
public boolean exclusive;
|
||||||
|
|
||||||
|
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||||
|
this.title=title;
|
||||||
|
this.repliesPolicy=repliesPolicy;
|
||||||
|
this.exclusive=exclusive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import okhttp3.MultipartBody;
|
|||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
|
|
||||||
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
|
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
|
||||||
private String maxID;
|
private final String maxID;
|
||||||
public PleromaMarkNotificationsRead(String maxID) {
|
public PleromaMarkNotificationsRead(String maxID) {
|
||||||
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
|
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
|
||||||
this.maxID = maxID;
|
this.maxID = maxID;
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.model.SearchResults;
|
import org.joinmastodon.android.model.SearchResults;
|
||||||
|
|
||||||
public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
|
public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
|
||||||
public GetSearchResults(String query, Type type, boolean resolve){
|
public GetSearchResults(String query, Type type, boolean resolve, String maxID, int offset, int count){
|
||||||
super(HttpMethod.GET, "/search", SearchResults.class);
|
super(HttpMethod.GET, "/search", SearchResults.class);
|
||||||
addQueryParameter("q", query);
|
addQueryParameter("q", query);
|
||||||
if(type!=null)
|
if(type!=null)
|
||||||
addQueryParameter("type", type.name().toLowerCase());
|
addQueryParameter("type", type.name().toLowerCase());
|
||||||
if(resolve)
|
if(resolve)
|
||||||
addQueryParameter("resolve", "true");
|
addQueryParameter("resolve", "true");
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(offset>0)
|
||||||
|
addQueryParameter("offset", String.valueOf(offset));
|
||||||
|
if(count>0)
|
||||||
|
addQueryParameter("limit", String.valueOf(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetSearchResults limit(int limit){
|
public GetSearchResults limit(int limit){
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,13 +11,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CreateStatus extends MastodonAPIRequest<Status>{
|
public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||||
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
|
public static long EPOCH_OF_THE_YEAR_FIVE_THOUSAND=95617584000000L;
|
||||||
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
|
public static final Instant DRAFTS_AFTER_INSTANT=Instant.ofEpochMilli(EPOCH_OF_THE_YEAR_FIVE_THOUSAND - 1) /* end of 4999 */;
|
||||||
|
|
||||||
public static Instant getDraftInstant() {
|
public static Instant getDraftInstant() {
|
||||||
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
|
return DRAFTS_AFTER_INSTANT.plusMillis(System.currentTimeMillis());
|
||||||
// yes, this is a weird implementation for something that hardly matters
|
|
||||||
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateStatus(CreateStatus.Request req, String uuid){
|
public CreateStatus(CreateStatus.Request req, String uuid){
|
||||||
@@ -36,6 +34,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
|||||||
|
|
||||||
public static class Request{
|
public static class Request{
|
||||||
public String status;
|
public String status;
|
||||||
|
public List<MediaAttribute> mediaAttributes;
|
||||||
public List<String> mediaIds;
|
public List<String> mediaIds;
|
||||||
public Poll poll;
|
public Poll poll;
|
||||||
public String inReplyToId;
|
public String inReplyToId;
|
||||||
@@ -49,11 +48,25 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
|||||||
public String quoteId;
|
public String quoteId;
|
||||||
public ContentType contentType;
|
public ContentType contentType;
|
||||||
|
|
||||||
|
public boolean preview;
|
||||||
|
|
||||||
public static class Poll{
|
public static class Poll{
|
||||||
public ArrayList<String> options=new ArrayList<>();
|
public ArrayList<String> options=new ArrayList<>();
|
||||||
public int expiresIn;
|
public int expiresIn;
|
||||||
public boolean multiple;
|
public boolean multiple;
|
||||||
public boolean hideTotals;
|
public boolean hideTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class MediaAttribute{
|
||||||
|
public String id;
|
||||||
|
public String description;
|
||||||
|
public String focus;
|
||||||
|
|
||||||
|
public MediaAttribute(String id, String description, String focus){
|
||||||
|
this.id=id;
|
||||||
|
this.description=description;
|
||||||
|
this.focus=focus;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
|||||||
s.visibility=StatusPrivacy.PUBLIC;
|
s.visibility=StatusPrivacy.PUBLIC;
|
||||||
s.mentions=Collections.emptyList();
|
s.mentions=Collections.emptyList();
|
||||||
s.tags=Collections.emptyList();
|
s.tags=Collections.emptyList();
|
||||||
|
if(s.poll!=null){
|
||||||
|
s.poll.id="fakeID"+i;
|
||||||
|
s.poll.emojis=Collections.emptyList();
|
||||||
|
s.poll.ownVotes=Collections.emptyList();
|
||||||
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
public class SetStatusMuted extends MastodonAPIRequest<Status>{
|
||||||
|
public SetStatusMuted(String id, boolean muted){
|
||||||
|
super(HttpMethod.POST, "/statuses/"+id+"/"+(muted ? "mute" : "unmute"), Status.class);
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package org.joinmastodon.android.api.requests.statuses;
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.TranslatedStatus;
|
import org.joinmastodon.android.model.Translation;
|
||||||
|
|
||||||
public class TranslateStatus extends MastodonAPIRequest<TranslatedStatus> {
|
import java.util.Map;
|
||||||
public TranslateStatus(String id) {
|
|
||||||
super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class);
|
public class TranslateStatus extends MastodonAPIRequest<Translation>{
|
||||||
setRequestBody(new Object());
|
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+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.tags;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
|
|
||||||
|
public class GetTag extends MastodonAPIRequest<Hashtag>{
|
||||||
|
public GetTag(String tag){
|
||||||
|
super(HttpMethod.GET, "/tags/"+tag, Hashtag.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.tags;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
|
|
||||||
|
public class SetTagFollowed extends MastodonAPIRequest<Hashtag>{
|
||||||
|
public SetTagFollowed(String tag, boolean followed){
|
||||||
|
super(HttpMethod.POST, "/tags/"+tag+(followed ? "/follow" : "/unfollow"), Hashtag.class);
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
|
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID, String replyVisibility){
|
||||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||||
if(local)
|
if(local)
|
||||||
addQueryParameter("local", "true");
|
addQueryParameter("local", "true");
|
||||||
@@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
|||||||
addQueryParameter("remote", "true");
|
addQueryParameter("remote", "true");
|
||||||
if(!TextUtils.isEmpty(maxID))
|
if(!TextUtils.isEmpty(maxID))
|
||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(!TextUtils.isEmpty(minID))
|
||||||
|
addQueryParameter("min_id", minID);
|
||||||
|
if(!TextUtils.isEmpty(sinceID))
|
||||||
|
addQueryParameter("since_id", sinceID);
|
||||||
if(limit>0)
|
if(limit>0)
|
||||||
addQueryParameter("limit", limit+"");
|
addQueryParameter("limit", limit+"");
|
||||||
if(replyVisibility != null)
|
if(replyVisibility != null)
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
|||||||
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
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.ContentType;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
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 org.joinmastodon.android.model.TimelineDefinition;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
@@ -40,16 +46,21 @@ public class AccountLocalPreferences{
|
|||||||
public String publishButtonText;
|
public String publishButtonText;
|
||||||
public String timelineReplyVisibility; // akkoma-only
|
public String timelineReplyVisibility; // akkoma-only
|
||||||
public boolean keepOnlyLatestNotification;
|
public boolean keepOnlyLatestNotification;
|
||||||
|
|
||||||
public boolean emojiReactionsEnabled;
|
public boolean emojiReactionsEnabled;
|
||||||
public boolean showEmojiReactionsInLists;
|
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 recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
|
||||||
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
private final static Type timelinesType=new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
||||||
|
private final static Type recentCustomEmojiType=new TypeToken<ArrayList<Emoji>>() {}.getType();
|
||||||
|
|
||||||
// MOSHIDON
|
// MOSHIDON
|
||||||
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
// private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
||||||
public Map<String, Integer> recentEmojis;
|
// 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){
|
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
|
||||||
this.prefs=prefs;
|
this.prefs=prefs;
|
||||||
@@ -58,6 +69,7 @@ public class AccountLocalPreferences{
|
|||||||
revealCWs=prefs.getBoolean("revealCWs", false);
|
revealCWs=prefs.getBoolean("revealCWs", false);
|
||||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||||
|
// preReplySheet=prefs.getBoolean("preReplySheet", false);
|
||||||
|
|
||||||
// MEGALODON
|
// MEGALODON
|
||||||
showReplies=prefs.getBoolean("showReplies", true);
|
showReplies=prefs.getBoolean("showReplies", true);
|
||||||
@@ -73,10 +85,13 @@ public class AccountLocalPreferences{
|
|||||||
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
|
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
|
||||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||||
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
|
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
|
||||||
showEmojiReactionsInLists=prefs.getBoolean("showEmojiReactionsInLists", false);
|
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
|
// 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(){
|
public long getNotificationsPauseEndTime(){
|
||||||
@@ -87,6 +102,10 @@ public class AccountLocalPreferences{
|
|||||||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ColorPreference getCurrentColor(){
|
||||||
|
return color!=null ? color : GlobalUserPreferences.color!=null ? GlobalUserPreferences.color : ColorPreference.MATERIAL3;
|
||||||
|
}
|
||||||
|
|
||||||
public void save(){
|
public void save(){
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putBoolean("interactionCounts", showInteractionCounts)
|
.putBoolean("interactionCounts", showInteractionCounts)
|
||||||
@@ -95,6 +114,9 @@ public class AccountLocalPreferences{
|
|||||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||||
|
|
||||||
|
//TODO figure this stuff out
|
||||||
|
// .putBoolean("preReplySheet", preReplySheet)
|
||||||
|
|
||||||
// MEGALODON
|
// MEGALODON
|
||||||
.putBoolean("showReplies", showReplies)
|
.putBoolean("showReplies", showReplies)
|
||||||
.putBoolean("showBoosts", showBoosts)
|
.putBoolean("showBoosts", showBoosts)
|
||||||
@@ -109,10 +131,47 @@ public class AccountLocalPreferences{
|
|||||||
.putString("timelineReplyVisibility", timelineReplyVisibility)
|
.putString("timelineReplyVisibility", timelineReplyVisibility)
|
||||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||||
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
|
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
|
||||||
.putBoolean("showEmojiReactionsInLists", showEmojiReactionsInLists)
|
.putString("showEmojiReactions", showEmojiReactions.name())
|
||||||
|
.putString("color", color!=null ? color.name() : null)
|
||||||
|
.putString("recentCustomEmoji", gson.toJson(recentCustomEmoji))
|
||||||
|
|
||||||
// MOSHIDON
|
// MOSHIDON
|
||||||
.putString("recentEmojis", gson.toJson(recentEmojis))
|
// .putString("recentEmojis", gson.toJson(recentEmojis))
|
||||||
|
.putString("notificationFilters", gson.toJson(notificationFilters))
|
||||||
.apply();
|
.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,
|
||||||
|
ALWAYS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.joinmastodon.android.model.FilterAction;
|
|||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.FilterResult;
|
import org.joinmastodon.android.model.FilterResult;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.LegacyFilter;
|
import org.joinmastodon.android.model.LegacyFilter;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
@@ -34,13 +35,13 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
|||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
@@ -71,6 +72,7 @@ public class AccountSession{
|
|||||||
private transient SharedPreferences prefs;
|
private transient SharedPreferences prefs;
|
||||||
private transient boolean preferencesNeedSaving;
|
private transient boolean preferencesNeedSaving;
|
||||||
private transient AccountLocalPreferences localPreferences;
|
private transient AccountLocalPreferences localPreferences;
|
||||||
|
private transient List<FollowList> lists;
|
||||||
|
|
||||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||||
this.token=token;
|
this.token=token;
|
||||||
@@ -146,6 +148,9 @@ public class AccountSession{
|
|||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
|
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
|
||||||
|
if (preferences==null)
|
||||||
|
preferences=new Preferences();
|
||||||
|
preferencesFromAccountSource(self);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(getID());
|
.exec(getID());
|
||||||
@@ -216,7 +221,7 @@ public class AccountSession{
|
|||||||
|
|
||||||
public void savePreferencesIfPending(){
|
public void savePreferencesIfPending(){
|
||||||
if(preferencesNeedSaving){
|
if(preferencesNeedSaving){
|
||||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
new UpdateAccountCredentialsPreferences(preferences, self.locked, self.discoverable, self.source.indexable)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Account result){
|
public void onSuccess(Account result){
|
||||||
@@ -252,52 +257,77 @@ public class AccountSession{
|
|||||||
filterStatusContainingObjects(objects, extractor, context, null);
|
filterStatusContainingObjects(objects, extractor, context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
private boolean statusIsOnOwnProfile(Status s, Account profile){
|
||||||
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
|
return self != null && profile != null && s.account != null
|
||||||
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
||||||
|
}
|
||||||
|
|
||||||
if(getLocalPreferences().serverSideFiltersSupported){
|
private boolean isFilteredType(Status s){
|
||||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||||
objects.removeIf(o->{
|
return (!localPreferences.showReplies && s.inReplyToId != null)
|
||||||
Status s=extractor.apply(o);
|
|| (!localPreferences.showBoosts && s.reblog != null);
|
||||||
if(s==null)
|
}
|
||||||
return false;
|
|
||||||
if(s.filtered==null)
|
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||||
return false;
|
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||||
// don't hide own posts in own profile
|
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
|
||||||
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){
|
|
||||||
Status s=extractor.apply(obj);
|
Status s=extractor.apply(obj);
|
||||||
if(s!=null && s.filtered!=null){
|
if(s!=null && s.filtered!=null){
|
||||||
getLocalPreferences().serverSideFiltersSupported=true;
|
localPreferences.serverSideFiltersSupported=true;
|
||||||
getLocalPreferences().save();
|
localPreferences.save();
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objects.removeIf(o->{
|
|
||||||
Status s=extractor.apply(o);
|
List<T> removeUs=new ArrayList<>();
|
||||||
if(s==null)
|
for(int i=0; i<objects.size(); i++){
|
||||||
return false;
|
T o=objects.get(i);
|
||||||
// don't hide own posts in own profile
|
if(filterStatusContainingObject(o, extractor, context, profile)){
|
||||||
if (statusIsOnOwnProfile.test(s))
|
Status s=extractor.apply(o);
|
||||||
return false;
|
removeUs.add(o);
|
||||||
for(LegacyFilter filter:wordFilters){
|
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())
|
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
}
|
||||||
});
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAccountInfo(){
|
||||||
|
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Instance> getInstance() {
|
public Optional<Instance> getInstance() {
|
||||||
@@ -310,4 +340,18 @@ public class AccountSession{
|
|||||||
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
|
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDefaultAvatarUrl() {
|
||||||
|
return getInstance()
|
||||||
|
.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
|
||||||
|
.orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
package org.joinmastodon.android.api.session;
|
||||||
|
|
||||||
|
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -34,6 +36,7 @@ import org.joinmastodon.android.model.EmojiCategory;
|
|||||||
import org.joinmastodon.android.model.LegacyFilter;
|
import org.joinmastodon.android.model.LegacyFilter;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
import org.unifiedpush.android.connector.UnifiedPush;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@@ -93,6 +96,7 @@ public class AccountSessionManager{
|
|||||||
|
|
||||||
private AccountSessionManager(){
|
private AccountSessionManager(){
|
||||||
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
||||||
|
// This file should not be backed up, otherwise the app may start with accounts already logged in. See res/xml/backup_rules.xml
|
||||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||||
if(!file.exists())
|
if(!file.exists())
|
||||||
return;
|
return;
|
||||||
@@ -112,6 +116,7 @@ public class AccountSessionManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
|
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
|
||||||
|
Context context = MastodonApp.context;
|
||||||
instances.put(instance.uri, instance);
|
instances.put(instance.uri, instance);
|
||||||
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
|
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
|
||||||
sessions.put(session.getID(), session);
|
sessions.put(session.getID(), session);
|
||||||
@@ -124,7 +129,14 @@ public class AccountSessionManager{
|
|||||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
||||||
|
|
||||||
updateMoreInstanceInfo(instance, 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);
|
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||||
}
|
}
|
||||||
maybeUpdateShortcuts();
|
maybeUpdateShortcuts();
|
||||||
@@ -205,12 +217,17 @@ public class AccountSessionManager{
|
|||||||
public void removeAccount(String id){
|
public void removeAccount(String id){
|
||||||
AccountSession session=getAccount(id);
|
AccountSession session=getAccount(id);
|
||||||
session.getCacheController().closeDatabase();
|
session.getCacheController().closeDatabase();
|
||||||
|
session.getCacheController().getListsFile().delete();
|
||||||
MastodonApp.context.deleteDatabase(id+".db");
|
MastodonApp.context.deleteDatabase(id+".db");
|
||||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||||
MastodonApp.context.deleteSharedPreferences(id);
|
MastodonApp.context.deleteSharedPreferences(id);
|
||||||
}else{
|
}else{
|
||||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
String dataDir=MastodonApp.context.getApplicationInfo().dataDir;
|
||||||
|
if(dataDir!=null){
|
||||||
|
File prefsDir=new File(dataDir, "shared_prefs");
|
||||||
|
new File(prefsDir, id+".xml").delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessions.remove(id);
|
sessions.remove(id);
|
||||||
if(lastActiveAccountID.equals(id)){
|
if(lastActiveAccountID.equals(id)){
|
||||||
@@ -314,8 +331,7 @@ public class AccountSessionManager{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*package*/ void updateSessionLocalInfo(AccountSession session){
|
||||||
private void updateSessionLocalInfo(AccountSession session){
|
|
||||||
new GetOwnAccount()
|
new GetOwnAccount()
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@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,19 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import org.joinmastodon.android.model.EmojiReaction;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EmojiReactionsUpdatedEvent{
|
||||||
|
public final String id;
|
||||||
|
public final List<EmojiReaction> reactions;
|
||||||
|
public final boolean updateTextPadding;
|
||||||
|
public RecyclerView.ViewHolder viewHolder;
|
||||||
|
|
||||||
|
public EmojiReactionsUpdatedEvent(String id, List<EmojiReaction> reactions, boolean updateTextPadding, RecyclerView.ViewHolder viewHolder){
|
||||||
|
this.id=id;
|
||||||
|
this.reactions=reactions;
|
||||||
|
this.updateTextPadding=updateTextPadding;
|
||||||
|
this.viewHolder=viewHolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class FinishListCreationFragmentEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final String listID;
|
||||||
|
|
||||||
|
public FinishListCreationFragmentEvent(String accountID, String listID){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.listID=listID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
|
public class ListCreatedEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final FollowList list;
|
||||||
|
|
||||||
|
public ListCreatedEvent(String accountID, FollowList list){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.list=list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.joinmastodon.android.events;
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
public class ListDeletedEvent {
|
public class ListDeletedEvent{
|
||||||
public final String id;
|
public final String accountID;
|
||||||
|
public final String listID;
|
||||||
|
|
||||||
public ListDeletedEvent(String id) {
|
public ListDeletedEvent(String accountID, String listID){
|
||||||
this.id = id;
|
this.accountID=accountID;
|
||||||
|
this.listID=listID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package org.joinmastodon.android.events;
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
public class ListUpdatedCreatedEvent {
|
public class ListUpdatedCreatedEvent {
|
||||||
public final String id;
|
public final String id;
|
||||||
public final String title;
|
public final String title;
|
||||||
public final ListTimeline.RepliesPolicy repliesPolicy;
|
public final FollowList.RepliesPolicy repliesPolicy;
|
||||||
public final boolean exclusive;
|
public final boolean exclusive;
|
||||||
|
|
||||||
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, FollowList.RepliesPolicy repliesPolicy) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.exclusive = exclusive;
|
this.exclusive = exclusive;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
|
public class ListUpdatedEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final FollowList list;
|
||||||
|
|
||||||
|
public ListUpdatedEvent(String accountID, FollowList list){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.list=list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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;
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
import org.joinmastodon.android.model.ScheduledStatus;
|
|
||||||
|
|
||||||
public class ScheduledStatusDeletedEvent{
|
public class ScheduledStatusDeletedEvent{
|
||||||
public final String id;
|
public final String id;
|
||||||
public final String accountID;
|
public final String accountID;
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
package org.joinmastodon.android.events;
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.api.CacheController;
|
|
||||||
import org.joinmastodon.android.model.EmojiReaction;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class StatusCountersUpdatedEvent{
|
public class StatusCountersUpdatedEvent{
|
||||||
public String id;
|
public String id;
|
||||||
public long favorites, reblogs, replies;
|
public long favorites, reblogs, replies;
|
||||||
public boolean favorited, reblogged, bookmarked, pinned;
|
public boolean favorited, reblogged, bookmarked, pinned;
|
||||||
public List<EmojiReaction> reactions;
|
|
||||||
public Status status;
|
public Status status;
|
||||||
public RecyclerView.ViewHolder viewHolder;
|
|
||||||
|
|
||||||
public StatusCountersUpdatedEvent(Status s){
|
public StatusCountersUpdatedEvent(Status s){
|
||||||
this(s, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StatusCountersUpdatedEvent(Status s, RecyclerView.ViewHolder vh){
|
|
||||||
id=s.id;
|
id=s.id;
|
||||||
status=s;
|
status=s;
|
||||||
favorites=s.favouritesCount;
|
favorites=s.favouritesCount;
|
||||||
@@ -31,7 +18,5 @@ public class StatusCountersUpdatedEvent{
|
|||||||
reblogged=s.reblogged;
|
reblogged=s.reblogged;
|
||||||
bookmarked=s.bookmarked;
|
bookmarked=s.bookmarked;
|
||||||
pinned=s.pinned;
|
pinned=s.pinned;
|
||||||
reactions=new ArrayList<>(s.reactions);
|
|
||||||
viewHolder=vh;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ public class StatusCreatedEvent{
|
|||||||
public StatusCreatedEvent(Status status, String accountID){
|
public StatusCreatedEvent(Status status, String accountID){
|
||||||
this.status=status;
|
this.status=status;
|
||||||
this.accountID=accountID;
|
this.accountID=accountID;
|
||||||
|
status.fromStatusCreated=true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
public class StatusMuteChangedEvent{
|
||||||
|
public String id;
|
||||||
|
public boolean muted;
|
||||||
|
public Status status;
|
||||||
|
|
||||||
|
public StatusMuteChangedEvent(Status s){
|
||||||
|
id=s.id;
|
||||||
|
muted=s.muted;
|
||||||
|
status=s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,19 +9,16 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
|
||||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
@@ -55,15 +52,14 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
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){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if(getActivity()==null) return;
|
if(getActivity()==null) return;
|
||||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
boolean more=applyMaxID(result);
|
||||||
boolean empty=result.isEmpty();
|
|
||||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
|
||||||
onDataLoaded(result, !empty);
|
onDataLoaded(result, more);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.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){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Announcement> result){
|
public void onSuccess(List<Announcement> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
|
|
||||||
// get unread items first
|
// get unread items first
|
||||||
List<Announcement> data = result.stream().filter(a -> !a.read).collect(toList());
|
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.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
@@ -24,18 +23,28 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.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.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||||
import org.joinmastodon.android.model.Poll;
|
import org.joinmastodon.android.model.Poll;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.model.Translation;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
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.AccountStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
|
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||||
@@ -44,23 +53,30 @@ import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
|||||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.PreviewlessMediaGridStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
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.MediaAttachmentViewController;
|
||||||
|
import org.joinmastodon.android.ui.utils.PreviewlessMediaAttachmentViewController;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -90,7 +106,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||||
protected Rect tmpRect=new Rect();
|
protected Rect tmpRect=new Rect();
|
||||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||||
|
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, PreviewlessMediaAttachmentViewController> previewlessAttachmentViewsPool=new TypedObjectPool<>(this::makeNewPreviewlessMediaAttachmentView);
|
||||||
|
|
||||||
protected boolean currentlyScrolling;
|
protected boolean currentlyScrolling;
|
||||||
|
protected String maxID;
|
||||||
|
|
||||||
public BaseStatusListFragment(){
|
public BaseStatusListFragment(){
|
||||||
super(20);
|
super(20);
|
||||||
@@ -132,6 +151,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
for(T s:items){
|
for(T s:items){
|
||||||
displayItems.addAll(buildDisplayItems(s));
|
displayItems.addAll(buildDisplayItems(s));
|
||||||
}
|
}
|
||||||
|
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -153,10 +173,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
if(notify)
|
if(notify)
|
||||||
adapter.notifyItemRangeInserted(0, offset);
|
adapter.notifyItemRangeInserted(0, offset);
|
||||||
|
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getMaxID(){
|
protected String getMaxID(){
|
||||||
|
if(refreshing) return null;
|
||||||
|
if(maxID!=null) return maxID;
|
||||||
if(!preloadedData.isEmpty())
|
if(!preloadedData.isEmpty())
|
||||||
return preloadedData.get(preloadedData.size()-1).getID();
|
return preloadedData.get(preloadedData.size()-1).getID();
|
||||||
else if(!data.isEmpty())
|
else if(!data.isEmpty())
|
||||||
@@ -165,6 +188,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return null;
|
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 List<StatusDisplayItem> buildDisplayItems(T s);
|
||||||
protected abstract void addAccountToKnown(T s);
|
protected abstract void addAccountToKnown(T s);
|
||||||
|
|
||||||
@@ -201,7 +230,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
||||||
final Status status=_status.getContentStatus();
|
final Status status=_status.getContentStatus();
|
||||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||||
private MediaAttachmentViewController transitioningHolder;
|
private MediaAttachmentViewController transitioningHolder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -267,6 +296,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public void photoViewerDismissed(){
|
public void photoViewerDismissed(){
|
||||||
currentPhotoViewer=null;
|
currentPhotoViewer=null;
|
||||||
|
gridHolder.itemView.setHasTransientState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -278,6 +308,80 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return gridHolder.getViewController(index);
|
return gridHolder.getViewController(index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
gridHolder.itemView.setHasTransientState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
|
||||||
|
final Status status=_status.getContentStatus();
|
||||||
|
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||||
|
private PreviewlessMediaAttachmentViewController transitioningHolder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPhotoViewVisibility(int index, boolean visible){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||||
|
PreviewlessMediaAttachmentViewController holder=findPhotoViewHolder(index);
|
||||||
|
if(holder!=null && list!=null){
|
||||||
|
transitioningHolder=holder;
|
||||||
|
View view=transitioningHolder.inner;
|
||||||
|
int[] pos={0, 0};
|
||||||
|
view.getLocationOnScreen(pos);
|
||||||
|
outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight());
|
||||||
|
list.setClipChildren(false);
|
||||||
|
gridHolder.setClipChildren(false);
|
||||||
|
transitioningHolder.view.setElevation(1f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
|
||||||
|
View view=transitioningHolder.inner;
|
||||||
|
view.setTranslationX(translateX);
|
||||||
|
view.setTranslationY(translateY);
|
||||||
|
view.setScaleX(scale);
|
||||||
|
view.setScaleY(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endPhotoViewTransition(){
|
||||||
|
View view=transitioningHolder.inner;
|
||||||
|
view.setTranslationX(0f);
|
||||||
|
view.setTranslationY(0f);
|
||||||
|
view.setScaleX(1f);
|
||||||
|
view.setScaleY(1f);
|
||||||
|
transitioningHolder.view.setElevation(0f);
|
||||||
|
if(list!=null)
|
||||||
|
list.setClipChildren(true);
|
||||||
|
gridHolder.setClipChildren(true);
|
||||||
|
transitioningHolder=null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Drawable getPhotoViewCurrentDrawable(int index){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void photoViewerDismissed(){
|
||||||
|
currentPhotoViewer=null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissions(String[] permissions){
|
||||||
|
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreviewlessMediaAttachmentViewController findPhotoViewHolder(int index){
|
||||||
|
return gridHolder.getViewController(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -352,12 +456,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
list.addItemDecoration(new StatusListItemDecoration());
|
list.addItemDecoration(new StatusListItemDecoration());
|
||||||
|
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||||
private Rect tmpRect=new Rect();
|
private Rect tmpRect=new Rect();
|
||||||
@Override
|
@Override
|
||||||
public void getSelectorBounds(View view, Rect outRect){
|
public void getSelectorBounds(View view, Rect outRect){
|
||||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
if(list!=view.getParent()) return;
|
||||||
int lastIndex = -1, firstIndex = -1;
|
boolean hasDescendant=false, hasAncestor=false, isWarning=false;
|
||||||
|
int lastIndex=-1, firstIndex=-1;
|
||||||
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
||||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||||
}else{
|
}else{
|
||||||
@@ -462,10 +568,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
protected void updatePoll(String itemID, Status status, Poll poll){
|
protected void updatePoll(String itemID, Status status, Poll poll){
|
||||||
status.poll=poll;
|
status.poll=poll;
|
||||||
int firstOptionIndex=-1, footerIndex=-1;
|
int firstOptionIndex=-1, footerIndex=-1;
|
||||||
|
int spoilerFirstOptionIndex=-1, spoilerFooterIndex=-1;
|
||||||
|
SpoilerStatusDisplayItem spoilerItem=null;
|
||||||
int i=0;
|
int i=0;
|
||||||
for(StatusDisplayItem item:displayItems){
|
for(StatusDisplayItem item:displayItems){
|
||||||
if(item.parentID.equals(itemID)){
|
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;
|
firstOptionIndex=i;
|
||||||
}else if(item instanceof PollFooterStatusDisplayItem){
|
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||||
footerIndex=i;
|
footerIndex=i;
|
||||||
@@ -474,12 +584,39 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a temporary measure to deal with the app crashing when the poll isn't updated.
|
||||||
|
// This is needed because of a possible id mismatch that screws with things
|
||||||
|
if(firstOptionIndex==-1 || footerIndex==-1){
|
||||||
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
if(status.id.equals(itemID)){
|
||||||
|
if(item instanceof SpoilerStatusDisplayItem){
|
||||||
|
spoilerItem=(SpoilerStatusDisplayItem) item;
|
||||||
|
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||||
|
firstOptionIndex=i;
|
||||||
|
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||||
|
footerIndex=i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(firstOptionIndex==-1 || footerIndex==-1)
|
if(firstOptionIndex==-1 || footerIndex==-1)
|
||||||
throw new IllegalStateException("Can't find all poll items in displayItems");
|
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||||
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||||
int prevSize=pollItems.size();
|
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();
|
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()){
|
if(prevSize!=pollItems.size()){
|
||||||
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
|
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
|
||||||
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
|
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
|
||||||
@@ -491,7 +628,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
||||||
Poll poll=holder.getItem().poll;
|
Poll poll=holder.getItem().poll;
|
||||||
Poll.Option option=holder.getItem().option;
|
Poll.Option option=holder.getItem().option;
|
||||||
if(poll.multiple || GlobalUserPreferences.voteButtonForSingleChoice){
|
// MEGALODON: always show vote button
|
||||||
|
// if(poll.multiple){
|
||||||
if(poll.selectedOptions==null)
|
if(poll.selectedOptions==null)
|
||||||
poll.selectedOptions=new ArrayList<>();
|
poll.selectedOptions=new ArrayList<>();
|
||||||
boolean optionContained=poll.selectedOptions.contains(option);
|
boolean optionContained=poll.selectedOptions.contains(option);
|
||||||
@@ -506,7 +644,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
for(int i=0;i<list.getChildCount();i++){
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||||
if(!poll.multiple && vh instanceof PollOptionStatusDisplayItem.Holder item){
|
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(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
||||||
if(footer.getItemID().equals(holder.getItemID())){
|
if(footer.getItemID().equals(holder.getItemID())){
|
||||||
@@ -515,9 +653,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
// }else{
|
||||||
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
// submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
|
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
|
||||||
@@ -525,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()));
|
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){
|
protected void submitPollVote(String parentID, String pollID, List<Integer> choices){
|
||||||
if(refreshing)
|
if(refreshing)
|
||||||
return;
|
return;
|
||||||
@@ -546,37 +692,39 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
|
|
||||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||||
Status status=holder.getItem().status;
|
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) {
|
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
||||||
Status status = holder.getItem().status;
|
Status status = holder.getItem().status;
|
||||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
|
||||||
if (mediaGrid != null) {
|
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||||
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
if(mediaGrid!=null){
|
||||||
|
if(!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
||||||
else mediaGrid.hideSensitive();
|
else mediaGrid.hideSensitive();
|
||||||
} else {
|
}else{
|
||||||
// media grid's methods normally change the status' state - we still want to be able
|
status.sensitiveRevealed=false;
|
||||||
// to do this if the media grid is not bound, tho - so, doing it ourselves here
|
notifyItemChangedAfter(holder.getItem(), MediaGridStatusDisplayItem.class);
|
||||||
status.sensitiveRevealed = !status.sensitiveRevealed;
|
|
||||||
}
|
}
|
||||||
holder.rebind();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
|
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
|
||||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||||
if(header != null) header.rebind();
|
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;
|
status.spoilerRevealed=!status.spoilerRevealed;
|
||||||
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
|
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
|
||||||
status.sensitiveRevealed = false;
|
status.sensitiveRevealed = false;
|
||||||
|
|
||||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
List<SpoilerStatusDisplayItem.Holder> spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||||
if(spoiler!=null)
|
SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0);
|
||||||
spoiler.rebind();
|
if(spoiler!=null) spoiler.rebind();
|
||||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class);
|
||||||
|
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem());
|
||||||
|
|
||||||
int index=displayItems.indexOf(spoilerItem);
|
int index=displayItems.indexOf(spoilerItem);
|
||||||
if(status.spoilerRevealed){
|
if(status.spoilerRevealed){
|
||||||
@@ -587,50 +735,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||||
if(text!=null)
|
|
||||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
|
||||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||||
if(header!=null)
|
if(header!=null) header.rebind();
|
||||||
header.rebind();
|
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||||
|
|
||||||
list.invalidateItemDecorations();
|
list.invalidateItemDecorations();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
||||||
if (holder.getItem().status.textExpandable != expandable && list != null) {
|
Status s=holder.getItem().status;
|
||||||
holder.getItem().status.textExpandable = expandable;
|
if(s.textExpandable!=expandable && list!=null) {
|
||||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
s.textExpandable=expandable;
|
||||||
if (header != null) header.rebind();
|
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||||
|
if(header!=null) header.bindCollapseButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onToggleExpanded(Status status, String itemID) {
|
public void onToggleExpanded(Status status, String itemID) {
|
||||||
status.textExpanded = !status.textExpanded;
|
status.textExpanded = !status.textExpanded;
|
||||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||||
if (text != null) text.rebind();
|
if(header!=null) header.animateExpandToggle();
|
||||||
if (header != null) header.rebind();
|
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateEmojiReactions(Status status, String itemID){
|
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
|
||||||
|
|
||||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||||
int startPos = warning.getAbsoluteAdapterPosition();
|
WarningFilteredStatusDisplayItem filterItem=findItemOfType(warning.getItemID(), WarningFilteredStatusDisplayItem.class);
|
||||||
|
int startPos=displayItems.indexOf(filterItem);
|
||||||
displayItems.remove(startPos);
|
displayItems.remove(startPos);
|
||||||
displayItems.addAll(startPos, warning.filteredItems);
|
displayItems.addAll(startPos, warning.filteredItems);
|
||||||
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
||||||
if (startPos == 0) scrollToTop();
|
if (startPos == 0) scrollToTop();
|
||||||
warning.getItem().status.filterRevealed = true;
|
warning.getItem().status.filterRevealed = true;
|
||||||
|
list.invalidateItemDecorations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -647,6 +787,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void loadRelationships(Set<String> ids){
|
protected void loadRelationships(Set<String> ids){
|
||||||
|
if(ids.isEmpty())
|
||||||
|
return;
|
||||||
|
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
|
||||||
if(ids.isEmpty())
|
if(ids.isEmpty())
|
||||||
return;
|
return;
|
||||||
// TODO somehow manage these and cancel outstanding requests on refresh
|
// TODO somehow manage these and cancel outstanding requests on refresh
|
||||||
@@ -678,9 +821,61 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return null;
|
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
|
@Nullable
|
||||||
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){
|
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));
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
|
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
|
||||||
return type.cast(holder);
|
return type.cast(holder);
|
||||||
@@ -768,15 +963,116 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return new MediaAttachmentViewController(getActivity(), type);
|
return new MediaAttachmentViewController(getActivity(), type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PreviewlessMediaAttachmentViewController makeNewPreviewlessMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){
|
||||||
|
return new PreviewlessMediaAttachmentViewController(getActivity(), type);
|
||||||
|
}
|
||||||
|
|
||||||
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> getAttachmentViewsPool(){
|
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> getAttachmentViewsPool(){
|
||||||
return attachmentViewsPool;
|
return attachmentViewsPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, PreviewlessMediaAttachmentViewController> getPreviewlessAttachmentViewsPool(){
|
||||||
|
return previewlessAttachmentViewsPool;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onProvideAssistContent(AssistContent assistContent) {
|
public void onProvideAssistContent(AssistContent assistContent) {
|
||||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
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(){
|
public void rebuildAllDisplayItems(){
|
||||||
displayItems.clear();
|
displayItems.clear();
|
||||||
for(T item:data){
|
for(T item:data){
|
||||||
@@ -785,6 +1081,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void maybeShowPreReplySheet(Status status, Runnable proceed){
|
||||||
|
Relationship rel=getRelationship(status.account.id);
|
||||||
|
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
|
||||||
|
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
|
||||||
|
new NonMutualPreReplySheet(getActivity(), notAgain->{
|
||||||
|
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
|
||||||
|
proceed.run();
|
||||||
|
}, status.account, accountID).show();
|
||||||
|
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
|
||||||
|
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
|
||||||
|
new OldPostPreReplySheet(getActivity(), notAgain->{
|
||||||
|
if(notAgain)
|
||||||
|
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
|
||||||
|
proceed.run();
|
||||||
|
}, status).show();
|
||||||
|
}else{
|
||||||
|
proceed.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -792,7 +1108,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
if(getContext()==null) return;
|
if(getContext()==null) return;
|
||||||
super.onDataLoaded(d, more);
|
super.onDataLoaded(d, more);
|
||||||
// more available, but the page isn't even full yet? seems wrong, let's load some 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();
|
preloader.onScrolledToLastItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -848,7 +1164,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
{
|
{
|
||||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.showDividers ? R.attr.colorM3OutlineVariant : R.attr.colorM3Surface));
|
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.showDividers ? R.attr.colorM3OutlineVariant : R.attr.colorM3Surface));
|
||||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||||
dividerPaint.setStrokeWidth(V.dp(0.5f));
|
dividerPaint.setStrokeWidth(V.dp(1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
|||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Status> result){
|
public void onSuccess(HeaderPaginationList<Status> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
if(result.nextPageUri!=null)
|
if(result.nextPageUri!=null)
|
||||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDra
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorSet;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
@@ -75,7 +78,7 @@ import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
|||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.ContentType;
|
import org.joinmastodon.android.model.ContentType;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
@@ -123,11 +126,11 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.time.format.FormatStyle;
|
import java.time.format.FormatStyle;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -135,12 +138,14 @@ import java.util.stream.Collectors;
|
|||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID, CustomTransitionsFragment {
|
||||||
|
|
||||||
private static final int MEDIA_RESULT=717;
|
private static final int MEDIA_RESULT=717;
|
||||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||||
@@ -165,7 +170,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
public LinearLayout mainLayout;
|
public LinearLayout mainLayout;
|
||||||
private SizeListenerLinearLayout contentView;
|
private SizeListenerLinearLayout contentView;
|
||||||
private TextView selfName, selfUsername, selfExtraText, extraText, pronouns;
|
private TextView selfName, selfUsername, selfExtraText, extraText;
|
||||||
private ImageView selfAvatar;
|
private ImageView selfAvatar;
|
||||||
private Account self;
|
private Account self;
|
||||||
private String instanceDomain;
|
private String instanceDomain;
|
||||||
@@ -176,7 +181,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private int charCount, charLimit, trimmedCharCount;
|
private int charCount, charLimit, trimmedCharCount;
|
||||||
|
|
||||||
private Button publishButton, languageButton, scheduleTimeBtn;
|
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 ImageButton publishButtonRelocated, mediaBtn, pollBtn, emojiBtn, spoilerBtn, draftsBtn, scheduleDraftDismiss, contentTypeBtn;
|
||||||
private View sensitiveBtn;
|
private View sensitiveBtn;
|
||||||
private TextView replyText;
|
private TextView replyText;
|
||||||
@@ -215,7 +220,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
public Instance instance;
|
public Instance instance;
|
||||||
|
|
||||||
public Status editingStatus;
|
public Status editingStatus;
|
||||||
private ScheduledStatus scheduledStatus;
|
public ScheduledStatus scheduledStatus;
|
||||||
private boolean redraftStatus;
|
private boolean redraftStatus;
|
||||||
|
|
||||||
private Uri photoUri;
|
private Uri photoUri;
|
||||||
@@ -312,7 +317,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
@Override
|
@Override
|
||||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||||
creatingView=true;
|
creatingView=true;
|
||||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain, getAccountID());
|
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), accountID, customEmojis, instanceDomain);
|
||||||
emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){
|
emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){
|
||||||
@Override
|
@Override
|
||||||
public void onEmojiSelected(Emoji emoji){
|
public void onEmojiSelected(Emoji emoji){
|
||||||
@@ -364,7 +369,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
selfUsername=view.findViewById(R.id.self_username);
|
selfUsername=view.findViewById(R.id.self_username);
|
||||||
selfAvatar=view.findViewById(R.id.self_avatar);
|
selfAvatar=view.findViewById(R.id.self_avatar);
|
||||||
selfExtraText=view.findViewById(R.id.self_extra_text);
|
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);
|
selfUsername.setText('@'+self.username+'@'+instanceDomain);
|
||||||
if(self.avatar!=null)
|
if(self.avatar!=null)
|
||||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||||
@@ -465,7 +470,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
hasSpoiler=true;
|
hasSpoiler=true;
|
||||||
spoilerWrap.setVisibility(View.VISIBLE);
|
spoilerWrap.setVisibility(View.VISIBLE);
|
||||||
spoilerBtn.setSelected(true);
|
spoilerBtn.setSelected(true);
|
||||||
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
|
}else if(editingStatus!=null && editingStatus.hasSpoiler()){
|
||||||
hasSpoiler=true;
|
hasSpoiler=true;
|
||||||
spoilerWrap.setVisibility(View.VISIBLE);
|
spoilerWrap.setVisibility(View.VISIBLE);
|
||||||
spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText));
|
spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText));
|
||||||
@@ -527,7 +532,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
public void onLaunchAccountSearch(){
|
public void onLaunchAccountSearch(){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
View autocompleteView=autocompleteViewController.getView();
|
View autocompleteView=autocompleteViewController.getView();
|
||||||
@@ -685,7 +690,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
});
|
});
|
||||||
View originalPost=view.findViewById(R.id.original_post);
|
View originalPost=view.findViewById(R.id.original_post);
|
||||||
extraText=view.findViewById(R.id.extra_text);
|
extraText=view.findViewById(R.id.extra_text);
|
||||||
pronouns=view.findViewById(R.id.pronouns);
|
|
||||||
originalPost.setVisibility(View.VISIBLE);
|
originalPost.setVisibility(View.VISIBLE);
|
||||||
originalPost.setOnClickListener(v->{
|
originalPost.setOnClickListener(v->{
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
@@ -725,7 +729,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
moreBtn.setBackground(null);
|
moreBtn.setBackground(null);
|
||||||
|
|
||||||
TextView name = view.findViewById(R.id.name);
|
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);
|
UiUtils.loadCustomEmojiInTextView(name);
|
||||||
|
|
||||||
String time = status==null || status.editedAt==null
|
String time = status==null || status.editedAt==null
|
||||||
@@ -737,7 +741,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
view.findViewById(R.id.time).setVisibility(time==null ? View.GONE : View.VISIBLE);
|
view.findViewById(R.id.time).setVisibility(time==null ? View.GONE : View.VISIBLE);
|
||||||
if(time!=null) ((TextView) view.findViewById(R.id.time)).setText(time);
|
if(time!=null) ((TextView) view.findViewById(R.id.time)).setText(time);
|
||||||
|
|
||||||
if (status.spoilerText != null && !status.spoilerText.isBlank()) {
|
if (status.hasSpoiler()) {
|
||||||
TextView replyToSpoiler = view.findViewById(R.id.reply_to_spoiler);
|
TextView replyToSpoiler = view.findViewById(R.id.reply_to_spoiler);
|
||||||
replyToSpoiler.setVisibility(View.VISIBLE);
|
replyToSpoiler.setVisibility(View.VISIBLE);
|
||||||
replyToSpoiler.setText(status.spoilerText);
|
replyToSpoiler.setText(status.spoilerText);
|
||||||
@@ -759,7 +763,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
|
.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) {
|
int visibilityNameRes = switch (status.visibility) {
|
||||||
case PUBLIC -> R.string.visibility_public;
|
case PUBLIC -> R.string.visibility_public;
|
||||||
case UNLISTED -> R.string.sk_visibility_unlisted;
|
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 DIRECT -> R.string.visibility_private;
|
||||||
case LOCAL -> R.string.sk_local_only;
|
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->{
|
replyText.setOnClickListener(v->{
|
||||||
scrollView.smoothScrollTo(0, 0);
|
scrollView.smoothScrollTo(0, 0);
|
||||||
});
|
});
|
||||||
@@ -801,7 +806,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|
||||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
|
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
|
||||||
&& !status.spoilerText.startsWith("re: ") ? "re: " : "";
|
&& !status.spoilerText.startsWith("re: ") ? "re: " : "";
|
||||||
spoilerEdit.setText(prefix + replyTo.spoilerText);
|
spoilerEdit.setText(prefix + status.spoilerText);
|
||||||
spoilerBtn.setSelected(true);
|
spoilerBtn.setSelected(true);
|
||||||
}
|
}
|
||||||
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
|
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
|
||||||
@@ -862,6 +867,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu);
|
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));
|
charCounter.setText(String.valueOf(charLimit));
|
||||||
}
|
}
|
||||||
|
|
||||||
// draftsBtn = wrap.findViewById(R.id.drafts_btn);
|
// draftsBtn=wrap.findViewById(R.id.drafts_btn);
|
||||||
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
|
draftOptionsPopup=new PopupMenu(getContext(), draftsBtn);
|
||||||
draftOptionsPopup.inflate(R.menu.compose_more);
|
draftOptionsPopup.inflate(R.menu.compose_more);
|
||||||
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
|
Menu draftOptionsMenu=draftOptionsPopup.getMenu();
|
||||||
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
|
draftMenuItem=draftOptionsMenu.findItem(R.id.draft);
|
||||||
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
|
undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft);
|
||||||
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
|
scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule);
|
||||||
|
unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule);
|
||||||
|
draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma());
|
||||||
draftOptionsPopup.setOnMenuItemClickListener(i->{
|
draftOptionsPopup.setOnMenuItemClickListener(i->{
|
||||||
int id = i.getItemId();
|
int id=i.getItemId();
|
||||||
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
|
if(id==R.id.draft) updateScheduledAt(getDraftInstant());
|
||||||
else if (id == R.id.schedule) pickScheduledDateTime();
|
else if(id==R.id.schedule) pickScheduledDateTime();
|
||||||
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
|
else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null);
|
||||||
else navigateToUnsentPosts();
|
else if(id==R.id.drafts) navigateToUnsentPosts();
|
||||||
|
else if(id==R.id.preview) publish(true);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
|
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
|
||||||
@@ -909,23 +918,37 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
languageButton = wrap.findViewById(R.id.language_btn);
|
languageButton = wrap.findViewById(R.id.language_btn);
|
||||||
languageButton.setOnClickListener(v->showLanguageAlert());
|
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){
|
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setOnClickListener(v->{
|
||||||
publishButtonRelocated.setOnClickListener(v -> {
|
Consumer<Boolean> draftCheckComplete=(isDraft)->{
|
||||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
if(GlobalUserPreferences.altTextReminders && !isDraft) checkAltTextsAndPublish();
|
||||||
checkAltTextsAndPublish();
|
else publish();
|
||||||
else
|
};
|
||||||
publish();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
publishButton.setOnClickListener(v -> {
|
|
||||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
|
||||||
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.setOnClickListener(v-> draftOptionsPopup.show());
|
||||||
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
|
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
|
||||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
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.fromOrFallback(prefs.postingDefaultLanguage)
|
||||||
: languageResolver.getDefault());
|
: languageResolver.getDefault());
|
||||||
|
|
||||||
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
if(isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||||
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
|
if(isInstancePixelfed() || (editingStatus!=null && !redraftStatus)) {
|
||||||
// editing an already published post
|
// editing an already published post
|
||||||
draftsBtn.setVisibility(View.GONE);
|
draftsBtn.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@@ -1068,7 +1091,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getNavigationIconDrawableResource(){
|
protected int getNavigationIconDrawableResource(){
|
||||||
return R.drawable.ic_baseline_close_24;
|
return R.drawable.ic_fluent_dismiss_24_regular;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1127,6 +1150,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void publish(){
|
private void publish(){
|
||||||
|
publish(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(boolean preview){
|
||||||
sendingOverlay=new View(getActivity());
|
sendingOverlay=new View(getActivity());
|
||||||
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
||||||
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
|
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
|
||||||
@@ -1137,31 +1164,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
overlayParams.token=mainEditText.getWindowToken();
|
overlayParams.token=mainEditText.getWindowToken();
|
||||||
wm.addView(sendingOverlay, overlayParams);
|
wm.addView(sendingOverlay, overlayParams);
|
||||||
|
|
||||||
if(GlobalUserPreferences.relocatePublishButton){
|
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||||
publishButtonRelocated.setEnabled(false);
|
|
||||||
} else {
|
|
||||||
publishButton.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||||
|
|
||||||
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
|
mediaViewController.saveAltTextsBeforePublishing(
|
||||||
|
()->actuallyPublish(preview),
|
||||||
|
this::handlePublishError);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void actuallyPublish(){
|
private void actuallyPublish(boolean preview){
|
||||||
actuallyPublish(false);
|
|
||||||
}
|
|
||||||
private void actuallyPublish(boolean force){
|
|
||||||
String text=mainEditText.getText().toString();
|
String text=mainEditText.getText().toString();
|
||||||
CreateStatus.Request req=new CreateStatus.Request();
|
CreateStatus.Request req=new CreateStatus.Request();
|
||||||
if ("bottom".equals(postLang.encoding)) {
|
if("bottom".equals(postLang.encoding)){
|
||||||
text = new StatusTextEncoder(Bottom::encode).encode(text);
|
text=new StatusTextEncoder(Bottom::encode).encode(text);
|
||||||
req.spoilerText = "bottom-encoded emoji spam";
|
req.spoilerText="bottom-encoded emoji spam";
|
||||||
}
|
}
|
||||||
if (localOnly &&
|
if(localOnly &&
|
||||||
AccountSessionManager.get(accountID).getLocalPreferences().glitchInstance &&
|
AccountSessionManager.get(accountID).getLocalPreferences().glitchInstance &&
|
||||||
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) {
|
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()){
|
||||||
text += " " + GLITCH_LOCAL_ONLY_SUFFIX;
|
text+=" "+GLITCH_LOCAL_ONLY_SUFFIX;
|
||||||
}
|
}
|
||||||
req.status=text;
|
req.status=text;
|
||||||
req.localOnly=localOnly;
|
req.localOnly=localOnly;
|
||||||
@@ -1169,21 +1190,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
req.sensitive=sensitive;
|
req.sensitive=sensitive;
|
||||||
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
|
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
|
||||||
req.scheduledAt=scheduledAt;
|
req.scheduledAt=scheduledAt;
|
||||||
|
req.preview=preview;
|
||||||
if(!mediaViewController.isEmpty()){
|
if(!mediaViewController.isEmpty()){
|
||||||
req.mediaIds=mediaViewController.getAttachmentIDs();
|
req.mediaIds=mediaViewController.getAttachmentIDs();
|
||||||
}
|
if(editingStatus != null){
|
||||||
// ask whether to publish now when editing an existing draft
|
req.mediaAttributes=mediaViewController.getAttachmentAttributes();
|
||||||
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)){
|
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
|
||||||
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
|
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
|
||||||
@@ -1206,7 +1218,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
Callback<Status> resCallback=new Callback<>(){
|
Callback<Status> resCallback=new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
maybeDeleteScheduledPost(() -> {
|
if(preview){
|
||||||
|
openPreview(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeDeleteScheduledPost(()->{
|
||||||
wm.removeView(sendingOverlay);
|
wm.removeView(sendingOverlay);
|
||||||
sendingOverlay=null;
|
sendingOverlay=null;
|
||||||
if(editingStatus==null || redraftStatus){
|
if(editingStatus==null || redraftStatus){
|
||||||
@@ -1228,10 +1245,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
E.post(new StatusUpdatedEvent(editedStatus));
|
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);
|
Nav.finish(ComposeFragment.this);
|
||||||
}
|
}
|
||||||
if (getArguments().getBoolean("navigateToStatus", false)) {
|
if(getArguments().getBoolean("navigateToStatus", false)){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putParcelable("status", Parcels.wrap(result));
|
args.putParcelable("status", Parcels.wrap(result));
|
||||||
@@ -1247,11 +1264,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if(editingStatus!=null && !redraftStatus){
|
if(editingStatus!=null && !redraftStatus && !preview){
|
||||||
new EditStatus(req, editingStatus.id)
|
new EditStatus(req, editingStatus.id)
|
||||||
.setCallback(resCallback)
|
.setCallback(resCallback)
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}else if(req.scheduledAt == null){
|
}else if(req.scheduledAt == null || preview){
|
||||||
new CreateStatus(req, uuid)
|
new CreateStatus(req, uuid)
|
||||||
.setCallback(resCallback)
|
.setCallback(resCallback)
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
@@ -1281,7 +1298,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
.setPositiveButton(R.string.ok, (a, b)->{})
|
.setPositiveButton(R.string.ok, (a, b)->{})
|
||||||
.show();
|
.show();
|
||||||
handlePublishError(null);
|
handlePublishError(null);
|
||||||
publishButton.setEnabled(false);
|
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replyTo == null) updateRecentLanguages();
|
if (replyTo == null) updateRecentLanguages();
|
||||||
@@ -1291,7 +1308,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
wm.removeView(sendingOverlay);
|
wm.removeView(sendingOverlay);
|
||||||
sendingOverlay=null;
|
sendingOverlay=null;
|
||||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||||
publishButton.setEnabled(true);
|
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
|
||||||
if(error instanceof MastodonErrorResponse me){
|
if(error instanceof MastodonErrorResponse me){
|
||||||
new M3AlertDialogBuilder(getActivity())
|
new M3AlertDialogBuilder(getActivity())
|
||||||
.setTitle(R.string.post_failed)
|
.setTitle(R.string.post_failed)
|
||||||
@@ -1304,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() {
|
private void updateRecentLanguages() {
|
||||||
if (postLang == null || postLang.language == null) return;
|
if (postLang == null || postLang.language == null) return;
|
||||||
String language = postLang.language.getLanguage();
|
String language = postLang.language.getLanguage();
|
||||||
@@ -1379,20 +1415,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void confirmDiscardDraftAndFinish(){
|
private void confirmDiscardDraftAndFinish(){
|
||||||
boolean attachmentsPending = mediaViewController.areAnyAttachmentsNotDone();
|
boolean attachmentsPending=mediaViewController.areAnyAttachmentsNotDone();
|
||||||
if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
if(attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
||||||
.setTitle(R.string.sk_unfinished_attachments)
|
.setTitle(R.string.sk_unfinished_attachments)
|
||||||
.setMessage(R.string.sk_unfinished_attachments_message)
|
.setMessage(R.string.sk_unfinished_attachments_message)
|
||||||
.setPositiveButton(R.string.edit, (d, w) -> {})
|
.setPositiveButton(R.string.ok, (d, w)->{})
|
||||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
.setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
|
||||||
.show();
|
.show();
|
||||||
else new M3AlertDialogBuilder(getActivity())
|
else new M3AlertDialogBuilder(getActivity())
|
||||||
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
.setTitle(editingStatus!=null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||||
.setPositiveButton(R.string.save, (d, w) -> {
|
.setPositiveButton(R.string.save, (d, w)->{
|
||||||
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
|
updateScheduledAt(scheduledAt==null ? getDraftInstant() : scheduledAt);
|
||||||
publish();
|
publish();
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
.setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,7 +1446,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
boolean usePhotoPicker=photoPicker && UiUtils.isPhotoPickerAvailable();
|
boolean usePhotoPicker=photoPicker && UiUtils.isPhotoPickerAvailable();
|
||||||
if(usePhotoPicker){
|
if(usePhotoPicker){
|
||||||
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount());
|
if(mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount()>1)
|
||||||
|
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount());
|
||||||
}else{
|
}else{
|
||||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
@@ -1518,7 +1555,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
public void updateSensitive() {
|
public void updateSensitive() {
|
||||||
sensitiveBtn.setVisibility(View.GONE);
|
sensitiveBtn.setVisibility(View.GONE);
|
||||||
if (!mediaViewController.isEmpty() && !hasSpoiler) sensitiveBtn.setVisibility(View.VISIBLE);
|
if (!mediaViewController.isEmpty()) sensitiveBtn.setVisibility(View.VISIBLE);
|
||||||
if (mediaViewController.isEmpty()) sensitive = false;
|
if (mediaViewController.isEmpty()) sensitive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1529,8 +1566,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
.withMinute(0);
|
.withMinute(0);
|
||||||
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
|
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
|
||||||
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
|
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
|
||||||
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
|
LocalDateTime at=LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute);
|
||||||
.toInstant(OffsetDateTime.now().getOffset()));
|
updateScheduledAt(at.toInstant(ZoneId.systemDefault().getRules().getOffset(at)));
|
||||||
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
|
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
|
||||||
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
|
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
|
||||||
}
|
}
|
||||||
@@ -1589,15 +1626,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
} else {
|
} else {
|
||||||
draftsBtn.setImageDrawable(getContext().getDrawable(GlobalUserPreferences.relocatePublishButton ? R.drawable.ic_fluent_clock_24_regular : R.drawable.ic_fluent_clock_20_regular));
|
draftsBtn.setImageDrawable(getContext().getDrawable(GlobalUserPreferences.relocatePublishButton ? R.drawable.ic_fluent_clock_24_regular : R.drawable.ic_fluent_clock_20_regular));
|
||||||
if(GlobalUserPreferences.relocatePublishButton){
|
if(GlobalUserPreferences.relocatePublishButton){
|
||||||
publishButtonRelocated.setImageResource(R.drawable.ic_fluent_send_24_selector);
|
publishButtonRelocated.setImageResource(R.drawable.ic_fluent_send_24_regular);
|
||||||
}
|
}
|
||||||
resetPublishButtonText();
|
resetPublishButtonText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateHeaders() {
|
private void updateHeaders() {
|
||||||
UiUtils.setExtraTextInfo(getContext(), selfExtraText, null, false, false, localOnly || statusVisibility==StatusPrivacy.LOCAL, null);
|
UiUtils.setExtraTextInfo(getContext(), selfExtraText, false, false, localOnly, null);
|
||||||
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, pronouns, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
|
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildVisibilityPopup(View v){
|
private void buildVisibilityPopup(View v){
|
||||||
@@ -1625,7 +1662,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P) m.setGroupDividerEnabled(true);
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()) m.setGroupDividerEnabled(true);
|
||||||
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||||
@Override
|
@Override
|
||||||
public boolean onMenuItemClick(MenuItem item){
|
public boolean onMenuItemClick(MenuItem item){
|
||||||
@@ -1669,6 +1706,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentTypePopup.setOnMenuItemClickListener(i->{
|
contentTypePopup.setOnMenuItemClickListener(i->{
|
||||||
|
uuid=null;
|
||||||
int index=i.getItemId();
|
int index=i.getItemId();
|
||||||
contentType=ContentType.values()[index];
|
contentType=ContentType.values()[index];
|
||||||
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());
|
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());
|
||||||
@@ -1773,8 +1811,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sanitizeMediaDescription(String description){
|
||||||
|
if(description == null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Gboard android keyboard attaches this text whenever the user
|
||||||
|
// pastes something from the keyboard's suggestion bar.
|
||||||
|
// Due to different end user locales, the exact text may vary, but at
|
||||||
|
// least in version 13.4.08, all of the translations contained the
|
||||||
|
// string "Gboard".
|
||||||
|
if (description.contains("Gboard")){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||||
|
description = sanitizeMediaDescription(description);
|
||||||
return mediaViewController.addMediaAttachment(uri, description);
|
return mediaViewController.addMediaAttachment(uri, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1817,6 +1873,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
Editable e=mainEditText.getText();
|
Editable e=mainEditText.getText();
|
||||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||||
|
if(start==-1 || end==-1)
|
||||||
|
return;
|
||||||
e.replace(start, end, text+" ");
|
e.replace(start, end, text+" ");
|
||||||
finishAutocomplete();
|
finishAutocomplete();
|
||||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||||
@@ -1885,4 +1943,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
languageButton.setText(opt.language.getLanguageName());
|
languageButton.setText(opt.language.getLanguageName());
|
||||||
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
|
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Animator onCreateEnterTransition(View prev, View container){
|
||||||
|
AnimatorSet anim=new AnimatorSet();
|
||||||
|
if(getArguments().getBoolean("fromThreadFragment")){
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||||
|
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
anim.setDuration(300);
|
||||||
|
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Animator onCreateExitTransition(View prev, View container){
|
||||||
|
AnimatorSet anim=new AnimatorSet();
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
|
||||||
|
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
|
||||||
|
);
|
||||||
|
anim.setDuration(200);
|
||||||
|
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import android.graphics.Rect;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.media.MediaMetadataRetriever;
|
import android.media.MediaMetadataRetriever;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.style.BulletSpan;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -26,12 +23,12 @@ import android.widget.ImageView;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||||
import org.joinmastodon.android.ui.utils.ColorPalette;
|
import org.joinmastodon.android.ui.utils.ColorPalette;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
@@ -54,16 +51,17 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
accountID=getArguments().getString("account");
|
|
||||||
attachmentID=getArguments().getString("attachment");
|
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity){
|
public void onAttach(Activity activity){
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
|
accountID=getArguments().getString("account");
|
||||||
|
attachmentID=getArguments().getString("attachment");
|
||||||
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
|
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);
|
setTitle(R.string.add_alt_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,20 +132,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item){
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
if(item.getItemId()==R.id.help){
|
if(item.getItemId()==R.id.help){
|
||||||
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
|
|
||||||
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
|
|
||||||
for(BulletSpan span:spans){
|
|
||||||
BulletSpan betterSpan;
|
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
|
|
||||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
|
|
||||||
else
|
|
||||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
|
|
||||||
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
|
|
||||||
msg.removeSpan(span);
|
|
||||||
}
|
|
||||||
new M3AlertDialogBuilder(themeWrapper)
|
new M3AlertDialogBuilder(themeWrapper)
|
||||||
.setTitle(R.string.what_is_alt_text)
|
.setTitle(R.string.what_is_alt_text)
|
||||||
.setMessage(msg)
|
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
@@ -184,7 +171,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||||||
fakeAttachment.meta.width=width;
|
fakeAttachment.meta.width=width;
|
||||||
fakeAttachment.meta.height=height;
|
fakeAttachment.meta.height=height;
|
||||||
|
|
||||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
|
||||||
@Override
|
@Override
|
||||||
public void setPhotoViewVisibility(int index, boolean visible){
|
public void setPhotoViewVisibility(int index, boolean visible){
|
||||||
image.setAlpha(visible ? 1f : 0f);
|
image.setAlpha(visible ? 1f : 0f);
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewStub;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||||
|
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||||
|
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
|
||||||
|
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||||
|
import org.joinmastodon.android.ui.views.CurlyArrowEmptyView;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
|
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||||
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
|
|
||||||
|
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
|
||||||
|
private FollowList followList;
|
||||||
|
private Button nextButton;
|
||||||
|
private View buttonBar;
|
||||||
|
private FragmentRootLinearLayout rootView;
|
||||||
|
private FrameLayout searchFragmentContainer;
|
||||||
|
private FrameLayout fragmentContentWrap;
|
||||||
|
private AddNewListMembersFragment searchFragment;
|
||||||
|
private WindowInsets lastInsets;
|
||||||
|
private boolean dismissingSearchFragment;
|
||||||
|
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.manage_list_members);
|
||||||
|
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
|
||||||
|
setLayout(R.layout.fragment_login);
|
||||||
|
setEmptyText(R.string.list_no_members);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
|
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||||
|
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
|
||||||
|
loadData();
|
||||||
|
}else{
|
||||||
|
onDataLoaded(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetListAccounts(followList.id, null, 0)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
for(Account acc:result)
|
||||||
|
accountIDsInList.add(acc.id);
|
||||||
|
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||||
|
View view=super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
FrameLayout wrapper=new FrameLayout(getActivity());
|
||||||
|
wrapper.addView(view);
|
||||||
|
rootView=(FragmentRootLinearLayout) view;
|
||||||
|
fragmentContentWrap=wrapper;
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
nextButton=view.findViewById(R.id.btn_next);
|
||||||
|
nextButton.setOnClickListener(this::onNextClick);
|
||||||
|
nextButton.setText(R.string.done);
|
||||||
|
buttonBar=view.findViewById(R.id.button_bar);
|
||||||
|
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
lastInsets=insets;
|
||||||
|
if(searchFragment!=null)
|
||||||
|
searchFragment.onApplyWindowInsets(insets);
|
||||||
|
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
|
||||||
|
rootView.dispatchApplyWindowInsets(insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<View> getViewsForElevationEffect(){
|
||||||
|
return List.of(getToolbar(), buttonBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
MenuItem item=menu.add(R.string.add_list_member);
|
||||||
|
item.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if(searchFragmentContainer!=null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
searchFragmentContainer=new FrameLayout(getActivity());
|
||||||
|
searchFragmentContainer.setId(R.id.search_fragment);
|
||||||
|
fragmentContentWrap.addView(searchFragmentContainer);
|
||||||
|
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
args.putBoolean("_can_go_back", true);
|
||||||
|
searchFragment=new AddNewListMembersFragment(this);
|
||||||
|
searchFragment.setArguments(args);
|
||||||
|
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
|
||||||
|
getChildFragmentManager().executePendingTransactions();
|
||||||
|
if(lastInsets!=null)
|
||||||
|
searchFragment.onApplyWindowInsets(lastInsets);
|
||||||
|
searchFragmentContainer.setTranslationX(V.dp(100));
|
||||||
|
searchFragmentContainer.setAlpha(0f);
|
||||||
|
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||||
|
rootView.setVisibility(View.GONE);
|
||||||
|
}).start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializeEmptyView(View contentView){
|
||||||
|
ViewStub emptyStub=contentView.findViewById(R.id.empty);
|
||||||
|
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
|
||||||
|
super.initializeEmptyView(contentView);
|
||||||
|
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
|
||||||
|
emptySecondary.setText(R.string.list_find_users);
|
||||||
|
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
|
||||||
|
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setStatusBarColor(int color){
|
||||||
|
rootView.setStatusBarColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setNavigationBarColor(int color){
|
||||||
|
rootView.setNavigationBarColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissSearchFragment(){
|
||||||
|
if(searchFragment==null || dismissingSearchFragment)
|
||||||
|
return;
|
||||||
|
dismissingSearchFragment=true;
|
||||||
|
rootView.setVisibility(View.VISIBLE);
|
||||||
|
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||||
|
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||||
|
getChildFragmentManager().executePendingTransactions();
|
||||||
|
fragmentContentWrap.removeView(searchFragmentContainer);
|
||||||
|
searchFragmentContainer=null;
|
||||||
|
searchFragment=null;
|
||||||
|
dismissingSearchFragment=false;
|
||||||
|
}).start();
|
||||||
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNextClick(View v){
|
||||||
|
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
|
||||||
|
Nav.finish(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onBackPressed(){
|
||||||
|
if(searchFragment!=null){
|
||||||
|
dismissSearchFragment();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountInList(AccountViewModel account){
|
||||||
|
return accountIDsInList.contains(account.account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAccountToList(AccountViewModel account, Runnable onDone){
|
||||||
|
new AddAccountsToList(followList.id, Set.of(account.account.id))
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
accountIDsInList.add(account.account.id);
|
||||||
|
if(onDone!=null)
|
||||||
|
onDone.run();
|
||||||
|
int i=0;
|
||||||
|
for(AccountViewModel acc:data){
|
||||||
|
if(acc.account.id.equals(account.account.id)){
|
||||||
|
list.getAdapter().notifyItemChanged(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
int pos=data.size();
|
||||||
|
data.add(account);
|
||||||
|
list.getAdapter().notifyItemInserted(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
|
||||||
|
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
accountIDsInList.remove(account.account.id);
|
||||||
|
if(onDone!=null)
|
||||||
|
onDone.run();
|
||||||
|
int i=0;
|
||||||
|
for(AccountViewModel acc:data){
|
||||||
|
if(acc.account.id.equals(account.account.id)){
|
||||||
|
list.getAdapter().notifyItemChanged(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||||
|
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||||
|
holder.setOnLongClickListener(vh->false);
|
||||||
|
Button button=holder.getButton();
|
||||||
|
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||||
|
button.setMinimumWidth(0);
|
||||||
|
button.setMinWidth(0);
|
||||||
|
button.setOnClickListener(v->{
|
||||||
|
holder.setActionProgressVisible(true);
|
||||||
|
holder.itemView.setHasTransientState(true);
|
||||||
|
Runnable onDone=()->{
|
||||||
|
holder.setActionProgressVisible(false);
|
||||||
|
holder.itemView.setHasTransientState(false);
|
||||||
|
};
|
||||||
|
AccountViewModel account=holder.getItem();
|
||||||
|
if(isAccountInList(account)){
|
||||||
|
removeAccountAccountFromList(account, onDone);
|
||||||
|
}else{
|
||||||
|
addAccountToList(account, onDone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindViewHolder(AccountViewHolder holder){
|
||||||
|
Button button=holder.getButton();
|
||||||
|
int textRes, styleRes;
|
||||||
|
if(isAccountInList(holder.getItem())){
|
||||||
|
textRes=R.string.remove;
|
||||||
|
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||||
|
}else{
|
||||||
|
textRes=R.string.add;
|
||||||
|
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||||
|
}
|
||||||
|
button.setText(textRes);
|
||||||
|
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||||
|
button.setBackground(ta.getDrawable(0));
|
||||||
|
ta.recycle();
|
||||||
|
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||||
|
button.setTextColor(ta.getColorStateList(0));
|
||||||
|
ta.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base){
|
||||||
|
// TODO this
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||||
|
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
|
||||||
|
public class CreateListFragment extends BaseEditListFragment{
|
||||||
|
private Button nextButton;
|
||||||
|
private View buttonBar;
|
||||||
|
private FollowList followList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.create_list);
|
||||||
|
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
|
||||||
|
setLayout(R.layout.fragment_login);
|
||||||
|
if(savedInstanceState!=null)
|
||||||
|
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getNavigationIconDrawableResource(){
|
||||||
|
return R.drawable.ic_baseline_arrow_drop_down_18;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wantsCustomNavigationIcon(){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
nextButton=view.findViewById(R.id.btn_next);
|
||||||
|
nextButton.setOnClickListener(this::onNextClick);
|
||||||
|
nextButton.setText(R.string.create);
|
||||||
|
buttonBar=view.findViewById(R.id.button_bar);
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<View> getViewsForElevationEffect(){
|
||||||
|
return List.of(getToolbar(), buttonBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState){
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNextClick(View v){
|
||||||
|
String title=titleEdit.getText().toString().trim();
|
||||||
|
if(TextUtils.isEmpty(title)){
|
||||||
|
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(followList==null){
|
||||||
|
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(FollowList result){
|
||||||
|
followList=result;
|
||||||
|
proceed(false);
|
||||||
|
E.post(new ListCreatedEvent(accountID, result));
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().addList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
|
||||||
|
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(FollowList result){
|
||||||
|
followList=result;
|
||||||
|
proceed(true);
|
||||||
|
E.post(new ListUpdatedEvent(accountID, result));
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}else{
|
||||||
|
proceed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void proceed(boolean needLoadMembers){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(followList));
|
||||||
|
args.putBoolean("needLoadMembers", needLoadMembers);
|
||||||
|
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
|
||||||
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
|
||||||
|
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
|
||||||
|
Nav.finish(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
|
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
|
|
||||||
public interface DomainDisplay {
|
|
||||||
|
|
||||||
default String getDomain(){
|
|
||||||
AccountSession session = AccountSessionManager.getInstance().getLastActiveAccount();
|
|
||||||
if (session != null)
|
|
||||||
return session.domain;
|
|
||||||
else
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
|
||||||
|
public class EditListFragment extends BaseEditListFragment{
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.edit_list);
|
||||||
|
loadMembers();
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
menu.add(R.string.delete_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.delete_list)
|
||||||
|
.setMessage(getString(R.string.delete_list_confirm, followList.title))
|
||||||
|
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
String newTitle=titleEdit.getText().toString();
|
||||||
|
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
|
||||||
|
boolean newExclusive=exclusiveItem.checked;
|
||||||
|
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
|
||||||
|
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(FollowList result){
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||||
|
E.post(new ListUpdatedEvent(accountID, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
// TODO handle errors somehow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,17 +41,14 @@ import org.joinmastodon.android.api.requests.lists.GetLists;
|
|||||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.model.CustomLocalTimeline;
|
import org.joinmastodon.android.model.CustomLocalTimeline;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -67,86 +64,86 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private TimelinesAdapter adapter;
|
private TimelinesAdapter adapter;
|
||||||
private final ItemTouchHelper itemTouchHelper;
|
private final ItemTouchHelper itemTouchHelper;
|
||||||
private Menu optionsMenu;
|
private Menu optionsMenu;
|
||||||
private boolean updated;
|
private boolean updated;
|
||||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
|
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
||||||
private final List<ListTimeline> listTimelines = new ArrayList<>();
|
private final List<FollowList> followLists =new ArrayList<>();
|
||||||
private final List<Hashtag> hashtags = new ArrayList<>();
|
private final List<Hashtag> hashtags=new ArrayList<>();
|
||||||
private MenuItem addHashtagItem;
|
private MenuItem addHashtagItem;
|
||||||
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
||||||
|
|
||||||
public EditTimelinesFragment() {
|
public EditTimelinesFragment(){
|
||||||
super(10);
|
super(10);
|
||||||
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
|
ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback();
|
||||||
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
|
itemTouchHelper=new ItemTouchHelper(itemTouchCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
setTitle(R.string.sk_timelines);
|
setTitle(R.string.sk_timelines);
|
||||||
accountID = getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
|
|
||||||
new GetLists().setCallback(new Callback<>() {
|
new GetLists().setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> result) {
|
public void onSuccess(List<FollowList> result){
|
||||||
listTimelines.addAll(result);
|
followLists.addAll(result);
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error){
|
||||||
error.showToast(getContext());
|
error.showToast(getContext());
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).exec(accountID);
|
||||||
|
|
||||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
new GetFollowedHashtags().setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Hashtag> result) {
|
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||||
hashtags.addAll(result);
|
hashtags.addAll(result);
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error){
|
||||||
error.showToast(getContext());
|
error.showToast(getContext());
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onShown(){
|
protected void onShown(){
|
||||||
super.onShown();
|
super.onShown();
|
||||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
itemTouchHelper.attachToRecyclerView(list);
|
itemTouchHelper.attachToRecyclerView(list);
|
||||||
refreshLayout.setEnabled(false);
|
refreshLayout.setEnabled(false);
|
||||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
this.optionsMenu = menu;
|
this.optionsMenu=menu;
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
if (item.getItemId() == R.id.menu_back) {
|
if(item.getItemId()==R.id.menu_back){
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (item.getItemId() == R.id.menu_add_local_timelines) {
|
if (item.getItemId() == R.id.menu_add_local_timelines) {
|
||||||
addNewLocalTimeline();
|
addNewLocalTimeline();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -161,14 +158,14 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTimeline(TimelineDefinition tl) {
|
private void addTimeline(TimelineDefinition tl){
|
||||||
data.add(tl.copy());
|
data.add(tl.copy());
|
||||||
adapter.notifyItemInserted(data.size());
|
adapter.notifyItemInserted(data.size());
|
||||||
saveTimelines();
|
saveTimelines();
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addNewLocalTimeline() {
|
private void addNewLocalTimeline() {
|
||||||
FrameLayout inputWrap = new FrameLayout(getContext());
|
FrameLayout inputWrap = new FrameLayout(getContext());
|
||||||
EditText input = new EditText(getContext());
|
EditText input = new EditText(getContext());
|
||||||
input.setHint(R.string.sk_example_domain);
|
input.setHint(R.string.sk_example_domain);
|
||||||
@@ -194,313 +191,323 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||||||
timelineByMenuItem.put(item, tl);
|
timelineByMenuItem.put(item, tl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
|
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
|
||||||
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
|
MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||||
item.setIcon(icon);
|
item.setIcon(icon);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOptionsMenu() {
|
private void updateOptionsMenu(){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
optionsMenu.clear();
|
optionsMenu.clear();
|
||||||
timelineByMenuItem.clear();
|
timelineByMenuItem.clear();
|
||||||
|
|
||||||
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
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().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
|
||||||
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
|
SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
|
||||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||||
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
|
SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
|
||||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||||
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
|
SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
|
||||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
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);
|
addLocalTimelines.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
|
||||||
makeBackItem(timelinesMenu);
|
makeBackItem(timelinesMenu);
|
||||||
makeBackItem(listsMenu);
|
makeBackItem(listsMenu);
|
||||||
makeBackItem(hashtagsMenu);
|
makeBackItem(hashtagsMenu);
|
||||||
|
|
||||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
||||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
followLists.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
|
||||||
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
|
||||||
|
|
||||||
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
|
timelinesMenu.getItem().setVisible(timelinesMenu.size()>0);
|
||||||
listsMenu.getItem().setVisible(listsMenu.size() > 0);
|
listsMenu.getItem().setVisible(listsMenu.size()>0);
|
||||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.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() {
|
private void saveTimelines(){
|
||||||
updated=true;
|
updated=true;
|
||||||
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
||||||
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
|
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
|
||||||
prefs.timelines=data;
|
prefs.timelines=data;
|
||||||
prefs.save();
|
prefs.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeTimeline(int position) {
|
private void removeTimeline(int position){
|
||||||
data.remove(position);
|
data.remove(position);
|
||||||
adapter.notifyItemRemoved(position);
|
adapter.notifyItemRemoved(position);
|
||||||
saveTimelines();
|
saveTimelines();
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||||
updateOptionsMenu();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
|
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter(){
|
||||||
return adapter = new TimelinesAdapter();
|
return adapter=new TimelinesAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void scrollToTop() {
|
public void scrollToTop(){
|
||||||
smoothScrollRecyclerViewToTop(list);
|
smoothScrollRecyclerViewToTop(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy(){
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (updated) UiUtils.restartApp();
|
if(updated) UiUtils.restartApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
|
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags){
|
||||||
if (tags == null || tags.isEmpty()) return false;
|
if(tags==null || tags.isEmpty()) return false;
|
||||||
editText.setText(String.join(",", tags));
|
editText.setText(tags);
|
||||||
editText.chipifyAllUnterminatedTokens();
|
editText.chipifyAllUnterminatedTokens();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NachoTextView prepareChipTextView(NachoTextView nacho) {
|
private NachoTextView prepareChipTextView(NachoTextView nacho){
|
||||||
nacho.addChipTerminator(',', BEHAVIOR_CHIPIFY_ALL);
|
//I’ll Be Back
|
||||||
nacho.addChipTerminator('\n', BEHAVIOR_CHIPIFY_ALL);
|
nacho.setChipTerminators(
|
||||||
nacho.addChipTerminator(' ', BEHAVIOR_CHIPIFY_ALL);
|
Map.of(
|
||||||
nacho.addChipTerminator(';', BEHAVIOR_CHIPIFY_ALL);
|
',', BEHAVIOR_CHIPIFY_ALL,
|
||||||
nacho.enableEditChipOnTouch(true, true);
|
'\n', BEHAVIOR_CHIPIFY_ALL,
|
||||||
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
|
' ', BEHAVIOR_CHIPIFY_ALL,
|
||||||
return nacho;
|
';', BEHAVIOR_CHIPIFY_ALL
|
||||||
}
|
)
|
||||||
|
);
|
||||||
|
nacho.enableEditChipOnTouch(true, true);
|
||||||
|
nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens());
|
||||||
|
return nacho;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) {
|
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove){
|
||||||
Context ctx = getContext();
|
Context ctx=getContext();
|
||||||
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||||
|
|
||||||
View divider = view.findViewById(R.id.divider);
|
View divider=view.findViewById(R.id.divider);
|
||||||
Button advancedBtn = view.findViewById(R.id.advanced);
|
Button advancedBtn=view.findViewById(R.id.advanced);
|
||||||
EditText editText = view.findViewById(R.id.input);
|
EditText editText=view.findViewById(R.id.input);
|
||||||
if (item != null) editText.setText(item.getCustomTitle());
|
if(item!=null) editText.setText(item.getCustomTitle());
|
||||||
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||||
|
|
||||||
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
|
LinearLayout tagWrap=view.findViewById(R.id.tag_wrap);
|
||||||
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
|
boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG;
|
||||||
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
|
advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||||
advancedBtn.setOnClickListener(l -> {
|
advancedBtn.setOnClickListener(l->{
|
||||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||||
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
|
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
|
||||||
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
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);
|
UiUtils.beginLayoutTransition((ViewGroup) view);
|
||||||
});
|
});
|
||||||
|
|
||||||
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
|
Switch localOnlySwitch=view.findViewById(R.id.local_only_switch);
|
||||||
view.findViewById(R.id.local_only)
|
view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||||
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
|
||||||
|
|
||||||
EditText tagMain = view.findViewById(R.id.tag_main);
|
EditText tagMain=view.findViewById(R.id.tag_main);
|
||||||
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any));
|
NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||||
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all));
|
NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||||
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
|
NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||||
if (item != null) {
|
|
||||||
tagMain.setText(item.getHashtagName());
|
if(item!=null && hashtagOptionsAvailable){
|
||||||
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
tagMain.setText(item.getHashtagName());
|
||||||
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||||
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||||
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||||
if (item.isHashtagLocalOnly()) {
|
hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||||
localOnlySwitch.setChecked(true);
|
if(item.isHashtagLocalOnly()){
|
||||||
hasAdvanced = true;
|
localOnlySwitch.setChecked(true);
|
||||||
}
|
hasAdvanced=true;
|
||||||
if (hasAdvanced) {
|
}
|
||||||
advancedBtn.setSelected(true);
|
if(hasAdvanced){
|
||||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
advancedBtn.setSelected(true);
|
||||||
|
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||||
tagWrap.setVisibility(View.VISIBLE);
|
tagWrap.setVisibility(View.VISIBLE);
|
||||||
divider.setVisibility(View.VISIBLE);
|
divider.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageButton btn = view.findViewById(R.id.button);
|
ImageButton btn=view.findViewById(R.id.button);
|
||||||
PopupMenu popup = new PopupMenu(ctx, btn);
|
PopupMenu popup=new PopupMenu(ctx, btn);
|
||||||
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||||
btn.setImageResource(currentIcon.iconRes);
|
btn.setImageResource(currentIcon.iconRes);
|
||||||
btn.setTag(currentIcon.ordinal());
|
btn.setTag(currentIcon.ordinal());
|
||||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||||
btn.setOnClickListener(l -> popup.show());
|
btn.setOnClickListener(l->popup.show());
|
||||||
|
|
||||||
Menu menu = popup.getMenu();
|
Menu menu=popup.getMenu();
|
||||||
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||||
if (!currentIcon.equals(defaultIcon)) {
|
if(!currentIcon.equals(defaultIcon)){
|
||||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||||
}
|
}
|
||||||
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
|
for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){
|
||||||
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue;
|
if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue;
|
||||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||||
}
|
}
|
||||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener(menuItem -> {
|
popup.setOnMenuItemClickListener(menuItem->{
|
||||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||||
btn.setImageResource(icon.iconRes);
|
btn.setImageResource(icon.iconRes);
|
||||||
btn.setTag(menuItem.getItemId());
|
btn.setTag(menuItem.getItemId());
|
||||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx)
|
AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx)
|
||||||
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
.setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(R.string.save, (d, which) -> {
|
.setPositiveButton(R.string.save, (d, which)->{
|
||||||
tagsAny.chipifyAllUnterminatedTokens();
|
String name=editText.getText().toString().trim();
|
||||||
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)) {
|
|
||||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
|
||||||
onSave.accept(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name);
|
String mainHashtag=tagMain.getText().toString().trim();
|
||||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
if(item != null && item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||||
tl.setIcon(icon);
|
tagsAny.chipifyAllUnterminatedTokens();
|
||||||
tl.setTitle(name);
|
tagsAll.chipifyAllUnterminatedTokens();
|
||||||
tl.setTagOptions(
|
tagsNone.chipifyAllUnterminatedTokens();
|
||||||
mainHashtag,
|
if(TextUtils.isEmpty(mainHashtag)){
|
||||||
tagsAny.getChipValues(),
|
mainHashtag=name;
|
||||||
tagsAll.getChipValues(),
|
name=null;
|
||||||
tagsNone.getChipValues(),
|
}
|
||||||
localOnlySwitch.isChecked()
|
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(tl);
|
onSave.accept(null);
|
||||||
})
|
return;
|
||||||
.setNegativeButton(R.string.cancel, (d, which) -> {});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run());
|
||||||
btn.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
builder.show();
|
||||||
@NonNull
|
btn.requestFocus();
|
||||||
@Override
|
}
|
||||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
|
||||||
return new TimelineViewHolder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
|
@NonNull
|
||||||
holder.bind(data.get(position));
|
@Override
|
||||||
}
|
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||||
|
return new TimelineViewHolder();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){
|
||||||
return data.size();
|
holder.bind(data.get(position));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
@Override
|
||||||
private final TextView title;
|
public int getItemCount(){
|
||||||
private final ImageView dragger;
|
return data.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public TimelineViewHolder(){
|
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||||
super(getActivity(), R.layout.item_text, list);
|
private final TextView title;
|
||||||
title=findViewById(R.id.title);
|
private final ImageView dragger;
|
||||||
dragger=findViewById(R.id.dragger_thingy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
public TimelineViewHolder(){
|
||||||
@Override
|
super(getActivity(), R.layout.item_text, list);
|
||||||
public void onBind(TimelineDefinition item) {
|
title=findViewById(R.id.title);
|
||||||
title.setText(item.getTitle(getContext()));
|
dragger=findViewById(R.id.dragger_thingy);
|
||||||
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 onSave(TimelineDefinition tl) {
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
saveTimelines();
|
@Override
|
||||||
rebind();
|
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() {
|
private void onSave(TimelineDefinition tl){
|
||||||
removeTimeline(getAbsoluteAdapterPosition());
|
saveTimelines();
|
||||||
}
|
rebind();
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
private void onRemove(){
|
||||||
@Override
|
removeTimeline(getAbsoluteAdapterPosition());
|
||||||
public void onClick() {
|
}
|
||||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
public ItemTouchHelperCallback() {
|
@Override
|
||||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
public void onClick(){
|
||||||
}
|
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{
|
||||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
public ItemTouchHelperCallback(){
|
||||||
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
|
super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
|
||||||
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
|
@Override
|
||||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
|
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
|
||||||
viewHolder.itemView.animate().alpha(0.65f);
|
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
|
@Override
|
||||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
|
||||||
super.clearView(recyclerView, viewHolder);
|
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){
|
||||||
viewHolder.itemView.animate().alpha(1f);
|
viewHolder.itemView.animate().alpha(0.65f);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||||
int position = viewHolder.getAbsoluteAdapterPosition();
|
super.clearView(recyclerView, viewHolder);
|
||||||
removeTimeline(position);
|
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){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Status> result){
|
public void onSuccess(HeaderPaginationList<Status> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
if(result.nextPageUri!=null)
|
if(result.nextPageUri!=null)
|
||||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(String id){
|
public void onItemClick(String id){
|
||||||
UiUtils.openHashtagTimeline(getActivity(), accountID, id, data.stream().filter(h -> Objects.equals(h.name, id)).findAny().map(h -> h.following).orElse(null));
|
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import android.widget.TextView;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
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.Account;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
@@ -81,7 +82,7 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
|||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Account> result){
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
if(result.nextPageUri!=null)
|
if(result.nextPageUri!=null)
|
||||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
else
|
else
|
||||||
@@ -269,7 +270,7 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
|||||||
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
followingLabel.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);
|
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){
|
if(relationship==null || !relationship.followedBy){
|
||||||
actionWrap.setVisibility(View.GONE);
|
actionWrap.setVisibility(View.GONE);
|
||||||
@@ -357,15 +358,16 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
|
|||||||
|
|
||||||
public AccountWrapper(Account account){
|
public AccountWrapper(Account account){
|
||||||
this.account=account;
|
this.account=account;
|
||||||
if(!TextUtils.isEmpty(account.avatar))
|
avaRequest=new UrlImageLoaderRequest(
|
||||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
TextUtils.isEmpty(account.avatar) ? AccountSessionManager.get(getAccountID()).getDefaultAvatarUrl() : account.avatar,
|
||||||
|
V.dp(50), V.dp(50));
|
||||||
if(!TextUtils.isEmpty(account.header))
|
if(!TextUtils.isEmpty(account.header))
|
||||||
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
|
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
|
||||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||||
if(account.emojis.isEmpty()){
|
if(account.emojis.isEmpty()){
|
||||||
parsedName=account.displayName;
|
parsedName= account.getDisplayName();
|
||||||
}else{
|
}else{
|
||||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
|
||||||
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
|
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag>
|
|||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
if(result.nextPageUri!=null)
|
if(result.nextPageUri!=null)
|
||||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
else
|
else
|
||||||
@@ -122,7 +122,7 @@ public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +1,170 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
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.R;
|
||||||
import org.joinmastodon.android.api.requests.tags.GetHashtag;
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
|
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.GetTag;
|
||||||
|
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
||||||
import org.joinmastodon.android.events.HashtagUpdatedEvent;
|
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.FilterContext;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
|
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
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;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
||||||
private String hashtag;
|
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> any;
|
||||||
private List<String> all;
|
private List<String> all;
|
||||||
private List<String> none;
|
private List<String> none;
|
||||||
private boolean following;
|
private boolean following;
|
||||||
private boolean localOnly;
|
private boolean localOnly;
|
||||||
private MenuItem followButton;
|
private Menu optionsMenu;
|
||||||
|
private MenuInflater optionsMenuInflater;
|
||||||
|
|
||||||
|
private Optional<Filter> filter = Optional.empty();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean wantsComposeButton() {
|
protected boolean wantsComposeButton() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity){
|
public void onAttach(Activity activity){
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
updateTitle(getArguments().getString("hashtag"));
|
|
||||||
following=getArguments().getBoolean("following", false);
|
following=getArguments().getBoolean("following", false);
|
||||||
localOnly=getArguments().getBoolean("localOnly", false);
|
localOnly=getArguments().getBoolean("localOnly", false);
|
||||||
any=getArguments().getStringArrayList("any");
|
any=getArguments().getStringArrayList("any");
|
||||||
all=getArguments().getStringArrayList("all");
|
all=getArguments().getStringArrayList("all");
|
||||||
none=getArguments().getStringArrayList("none");
|
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);
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTitle(String hashtagName) {
|
private void updateMuteState(boolean newMute) {
|
||||||
hashtag = hashtagName;
|
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
|
||||||
setTitle('#'+hashtag);
|
muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFollowingState(boolean newFollowing) {
|
private void showMuteDialog(boolean mute) {
|
||||||
this.following = newFollowing;
|
UiUtils.showConfirmationAlert(getContext(),
|
||||||
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
|
mute ? R.string.mo_unmute_hashtag : R.string.mo_mute_hashtag,
|
||||||
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
|
mute ? R.string.mo_confirm_to_unmute_hashtag : R.string.mo_confirm_to_mute_hashtag,
|
||||||
E.post(new HashtagUpdatedEvent(hashtag, following));
|
mute ? R.string.do_unmute : R.string.do_mute,
|
||||||
|
mute ? R.drawable.ic_fluent_speaker_2_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
|
||||||
|
mute ? this::unmuteHashtag : this::muteHashtag
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
private void unmuteHashtag() {
|
||||||
@Override
|
//safe to get, this only called if filter is present
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
new DeleteFilter(filter.get().id).setCallback(new Callback<>(){
|
||||||
inflater.inflate(R.menu.hashtag_timeline, menu);
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
followButton = menu.findItem(R.id.follow_hashtag);
|
|
||||||
updateFollowingState(following);
|
|
||||||
|
|
||||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Hashtag hashtag) {
|
public void onSuccess(Void result){
|
||||||
if (getActivity() == null) return;
|
filter=Optional.empty();
|
||||||
updateTitle(hashtag.name);
|
updateMuteState(false);
|
||||||
updateFollowingState(hashtag.following);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error){
|
||||||
error.showToast(getActivity());
|
error.showToast(getContext());
|
||||||
|
}
|
||||||
|
}).exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void muteHashtag() {
|
||||||
|
FilterKeyword hashtagFilter=new FilterKeyword();
|
||||||
|
hashtagFilter.wholeWord=true;
|
||||||
|
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);
|
||||||
|
updateMuteState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getContext());
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).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;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TimelineDefinition makeTimelineDefinition() {
|
protected TimelineDefinition makeTimelineDefinition() {
|
||||||
return TimelineDefinition.ofHashtag(hashtag);
|
return TimelineDefinition.ofHashtag(hashtagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
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){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
boolean more=applyMaxID(result);
|
||||||
onDataLoaded(result, !result.isEmpty());
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||||
|
onDataLoaded(result, more);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
@@ -146,16 +177,46 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
|||||||
loadData();
|
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
|
@Override
|
||||||
public boolean onFabLongClick(View v) {
|
public boolean onFabLongClick(View v) {
|
||||||
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
|
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtagName+' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFabClick(View v){
|
public void onFabClick(View v){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putString("prefilledText", '#'+hashtag+' ');
|
args.putString("prefilledText", '#'+hashtagName+' ');
|
||||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +232,219 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uri getWebUri(Uri.Builder base) {
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
|
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + 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.NotificationManager;
|
||||||
import android.app.assist.AssistContent;
|
import android.app.assist.AssistContent;
|
||||||
import android.graphics.drawable.RippleDrawable;
|
import android.graphics.drawable.RippleDrawable;
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Outline;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.service.notification.StatusBarNotification;
|
import android.service.notification.StatusBarNotification;
|
||||||
@@ -35,10 +33,9 @@ import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
|||||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Instance;
|
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TabBar;
|
import org.joinmastodon.android.ui.views.TabBar;
|
||||||
@@ -75,7 +72,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
private TextView notificationsBadge;
|
private TextView notificationsBadge;
|
||||||
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private boolean isAkkoma;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -83,8 +79,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
setTitle(R.string.mo_app_name);
|
setTitle(R.string.mo_app_name);
|
||||||
|
|
||||||
isAkkoma = getInstance().map(Instance::isAkkoma).orElse(false);
|
|
||||||
|
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
|
|
||||||
@@ -94,7 +88,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
homeTabFragment=new HomeTabFragment();
|
homeTabFragment=new HomeTabFragment();
|
||||||
homeTabFragment.setArguments(args);
|
homeTabFragment.setArguments(args);
|
||||||
args=new Bundle(args);
|
args=new Bundle(args);
|
||||||
args.putBoolean("disableDiscover", isAkkoma);
|
|
||||||
args.putBoolean("noAutoLoad", true);
|
args.putBoolean("noAutoLoad", true);
|
||||||
discoverFragment=new DiscoverFragment();
|
discoverFragment=new DiscoverFragment();
|
||||||
discoverFragment.setArguments(args);
|
discoverFragment.setArguments(args);
|
||||||
@@ -189,6 +182,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tabBar.selectTab(currentTab);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -269,7 +263,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
private void onTabSelected(@IdRes int tab){
|
private void onTabSelected(@IdRes int tab){
|
||||||
Fragment newFragment=fragmentForTab(tab);
|
Fragment newFragment=fragmentForTab(tab);
|
||||||
if(tab==currentTab){
|
if(tab==currentTab){
|
||||||
if (tab == R.id.tab_search)
|
if (tab == R.id.tab_search && GlobalUserPreferences.doubleTapToSearch)
|
||||||
discoverFragment.openSearch();
|
discoverFragment.openSearch();
|
||||||
else if(newFragment instanceof ScrollableToTop scrollable)
|
else if(newFragment instanceof ScrollableToTop scrollable)
|
||||||
scrollable.scrollToTop();
|
scrollable.scrollToTop();
|
||||||
@@ -303,22 +297,19 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
if(tab==R.id.tab_profile){
|
if(tab==R.id.tab_profile){
|
||||||
ArrayList<String> options=new ArrayList<>();
|
ArrayList<String> options=new ArrayList<>();
|
||||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
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();
|
new AccountSwitcherSheet(getActivity(), this).show();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(tab==R.id.tab_search){
|
if(tab==R.id.tab_search){
|
||||||
onTabSelected(R.id.tab_search);
|
if(currentTab!=R.id.tab_search){
|
||||||
tabBar.selectTab(R.id.tab_search);
|
onTabSelected(R.id.tab_search);
|
||||||
|
tabBar.selectTab(R.id.tab_search);
|
||||||
|
}
|
||||||
discoverFragment.openSearch();
|
discoverFragment.openSearch();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(tab==R.id.tab_home){
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
|||||||
import org.joinmastodon.android.model.Announcement;
|
import org.joinmastodon.android.model.Announcement;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
@@ -95,7 +95,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
private ImageView collapsedChevron;
|
private ImageView collapsedChevron;
|
||||||
private TextView timelineTitle;
|
private TextView timelineTitle;
|
||||||
private PopupMenu switcherPopup;
|
private PopupMenu switcherPopup;
|
||||||
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
|
private final Map<Integer, FollowList> listItems = new HashMap<>();
|
||||||
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
||||||
private List<TimelineDefinition> timelinesList;
|
private List<TimelineDefinition> timelinesList;
|
||||||
private int count;
|
private int count;
|
||||||
@@ -237,21 +237,25 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
|
|
||||||
ViewTreeObserver vto = getToolbar().getViewTreeObserver();
|
ViewTreeObserver vto = getToolbar().getViewTreeObserver();
|
||||||
if (vto.isAlive()) {
|
if (vto.isAlive()) {
|
||||||
vto.addOnGlobalLayoutListener(() -> {
|
vto.addOnGlobalLayoutListener(()->{
|
||||||
Toolbar t = getToolbar();
|
Toolbar t=getToolbar();
|
||||||
if (t == null) return;
|
if(t==null) return;
|
||||||
int toolbarWidth = t.getWidth();
|
int toolbarWidth=t.getWidth();
|
||||||
if (toolbarWidth == 0) return;
|
if(toolbarWidth==0) return;
|
||||||
|
|
||||||
int toolbarFrameWidth = toolbarFrame.getWidth();
|
int toolbarFrameWidth=toolbarFrame.getWidth();
|
||||||
int padding = toolbarWidth - toolbarFrameWidth;
|
int actionsWidth=toolbarWidth-toolbarFrameWidth;
|
||||||
FrameLayout parent = ((FrameLayout) toolbarShowNewPostsBtn.getParent());
|
// margin (4) + padding (12) + icon (24) + margin (8) + chevron (16) + padding (12)
|
||||||
if (padding == parent.getPaddingStart()) return;
|
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.
|
// toolbar frame goes from screen edge to beginning of right-aligned option buttons.
|
||||||
// centering button by applying the same space on the left
|
// centering button by applying the same space on the left
|
||||||
parent.setPaddingRelative(padding, 0, 0, 0);
|
parent.setPaddingRelative(paddingMax, 0, paddingEnd, 0);
|
||||||
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth - padding * 2);
|
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth-paddingMax*2);
|
||||||
|
|
||||||
switcher.setPivotX(V.dp(28)); // padding + half of icon
|
switcher.setPivotX(V.dp(28)); // padding + half of icon
|
||||||
switcher.setPivotY(switcher.getHeight() / 2f);
|
switcher.setPivotY(switcher.getHeight() / 2f);
|
||||||
@@ -266,7 +270,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
|
|
||||||
new GetLists().setCallback(new Callback<>() {
|
new GetLists().setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> lists) {
|
public void onSuccess(List<FollowList> lists) {
|
||||||
updateList(lists, listItems);
|
updateList(lists, listItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +295,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
new GetAnnouncements(false).setCallback(new Callback<>() {
|
new GetAnnouncements(false).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Announcement> result) {
|
public void onSuccess(List<Announcement> result) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
if (result.stream().anyMatch(a -> !a.read)) {
|
if (result.stream().anyMatch(a -> !a.read)) {
|
||||||
announcementsBadged = true;
|
announcementsBadged = true;
|
||||||
announcements.setVisible(false);
|
announcements.setVisible(false);
|
||||||
@@ -385,7 +389,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateOverflowMenu() {
|
private void updateOverflowMenu() {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
Menu m = overflowPopup.getMenu();
|
Menu m = overflowPopup.getMenu();
|
||||||
m.clear();
|
m.clear();
|
||||||
overflowPopup.inflate(R.menu.home_overflow);
|
overflowPopup.inflate(R.menu.home_overflow);
|
||||||
@@ -404,9 +408,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
addListsToOverflowMenu();
|
addListsToOverflowMenu();
|
||||||
addHashtagsToOverflowMenu();
|
addHashtagsToOverflowMenu();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !UiUtils.isEMUI()) {
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
|
||||||
m.setGroupDividerEnabled(true);
|
m.setGroupDividerEnabled(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -509,7 +512,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
ListTimeline list;
|
FollowList list;
|
||||||
Hashtag hashtag;
|
Hashtag hashtag;
|
||||||
|
|
||||||
if (item.getItemId() == R.id.menu_back) {
|
if (item.getItemId() == R.id.menu_back) {
|
||||||
@@ -528,9 +531,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
|
if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
|
||||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||||
} else if ((hashtag = hashtagsItems.get(id)) != null) {
|
} else if ((hashtag = hashtagsItems.get(id)) != null) {
|
||||||
args.putString("hashtag", hashtag.name);
|
UiUtils.openHashtagTimeline(getContext(), accountID, hashtag);
|
||||||
args.putBoolean("following", hashtag.following);
|
|
||||||
Nav.go(getActivity(), HashtagTimelineFragment.class, args);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -700,13 +701,13 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||||
handleListEvent(listItems, l -> l.id.equals(event.id), false, null);
|
handleListEvent(listItems, l -> l.id.equals(event.listID), false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||||
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
|
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
|
||||||
ListTimeline list = new ListTimeline();
|
FollowList list = new FollowList();
|
||||||
list.id = event.id;
|
list.id = event.id;
|
||||||
list.title = event.title;
|
list.title = event.title;
|
||||||
list.repliesPolicy = event.repliesPolicy;
|
list.repliesPolicy = event.repliesPolicy;
|
||||||
|
|||||||
@@ -11,27 +11,26 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
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.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.TimelineMarkers;
|
import org.joinmastodon.android.model.TimelineMarkers;
|
||||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.utils.V;
|
|
||||||
|
|
||||||
public class HomeTimelineFragment extends StatusListFragment {
|
public class HomeTimelineFragment extends StatusListFragment {
|
||||||
private HomeTabFragment parent;
|
private HomeTabFragment parent;
|
||||||
@@ -50,16 +49,6 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
loadData();
|
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
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
AccountSessionManager.getInstance()
|
AccountSessionManager.getInstance()
|
||||||
@@ -67,11 +56,12 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
List<Status> filteredItems = filterPosts(result.items);
|
boolean empty=result.items.isEmpty();
|
||||||
maxID=result.maxID;
|
maxID=result.maxID;
|
||||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
AccountSessionManager.get(accountID).filterStatuses(result.items, getFilterContext());
|
||||||
if(result.isFromCache())
|
onDataLoaded(result.items, !empty);
|
||||||
|
if(result.isFromCache() && GlobalUserPreferences.loadNewPosts)
|
||||||
loadNewPosts();
|
loadNewPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -84,7 +74,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||||
@Override
|
@Override
|
||||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
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();
|
parent.hideNewPostsButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +87,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
if(!getArguments().getBoolean("noAutoLoad")){
|
if(!getArguments().getBoolean("noAutoLoad")){
|
||||||
if(!loaded && !dataLoading){
|
if(!loaded && !dataLoading){
|
||||||
loadData();
|
loadData();
|
||||||
}else if(!dataLoading){
|
}else if(!dataLoading && GlobalUserPreferences.loadNewPosts){
|
||||||
loadNewPosts();
|
loadNewPosts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,38 +117,46 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onStatusCreated(Status status){
|
public void onStatusCreated(Status status){
|
||||||
|
if(status.reblog!=null) return;
|
||||||
prependItems(Collections.singletonList(status), true);
|
prependItems(Collections.singletonList(status), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadNewPosts(){
|
private void loadNewPosts(){
|
||||||
if (!GlobalUserPreferences.loadNewPosts) return;
|
|
||||||
dataLoading=true;
|
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,
|
// 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
|
// 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.
|
// 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)
|
currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
currentRequest=null;
|
currentRequest=null;
|
||||||
dataLoading=false;
|
dataLoading=false;
|
||||||
result = filterPosts(result);
|
refreshDone();
|
||||||
if(result.isEmpty() || getActivity()==null)
|
if(result.isEmpty() || getActivity()==null)
|
||||||
return;
|
return;
|
||||||
Status last=result.get(result.size()-1);
|
Status last=result.get(result.size()-1);
|
||||||
List<Status> toAdd;
|
List<Status> toAdd;
|
||||||
if(!data.isEmpty() && last.id.equals(data.get(0).id)){ // This part intersects with the existing one
|
if(!dataFromTimeline.isEmpty() && last.id.equals(dataFromTimeline.get(0).id)){ // This part intersects with the existing one
|
||||||
toAdd=result.subList(0, result.size()-1); // Remove the already known last post
|
toAdd=new ArrayList<>(result.subList(0, result.size()-1)); // Remove the already known last post
|
||||||
}else{
|
}else{
|
||||||
result.get(result.size()-1).hasGapAfter=true;
|
last.hasGapAfter=last.id;
|
||||||
toAdd=result;
|
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());
|
AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
|
||||||
if(!toAdd.isEmpty()){
|
if(!toAdd.isEmpty()){
|
||||||
prependItems(toAdd, true);
|
prependItems(toAdd, true);
|
||||||
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
|
if(parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +164,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
currentRequest=null;
|
currentRequest=null;
|
||||||
dataLoading=false;
|
dataLoading=false;
|
||||||
|
refreshDone();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
@@ -176,15 +175,23 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGapClick(GapStatusDisplayItem.Holder item){
|
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){
|
||||||
if(dataLoading)
|
if(dataLoading)
|
||||||
return;
|
return;
|
||||||
item.getItem().loading=true;
|
|
||||||
V.setVisibilityAnimated(item.progress, View.VISIBLE);
|
|
||||||
V.setVisibilityAnimated(item.text, View.GONE);
|
|
||||||
GapStatusDisplayItem gap=item.getItem();
|
GapStatusDisplayItem gap=item.getItem();
|
||||||
|
gap.loading=true;
|
||||||
dataLoading=true;
|
dataLoading=true;
|
||||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null, getLocalPrefs().timelineReplyVisibility)
|
|
||||||
|
String maxID=null;
|
||||||
|
String minID=null;
|
||||||
|
if (downwards) {
|
||||||
|
maxID=item.getItem().getMaxID();
|
||||||
|
} else {
|
||||||
|
int gapPos=displayItems.indexOf(gap);
|
||||||
|
StatusDisplayItem nextItem=displayItems.get(gapPos + 1);
|
||||||
|
minID=nextItem.parentID;
|
||||||
|
}
|
||||||
|
currentRequest=new GetHomeTimeline(maxID, minID, 20, null, getLocalPrefs().timelineReplyVisibility)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
@@ -195,61 +202,108 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
int gapPos=displayItems.indexOf(gap);
|
int gapPos=displayItems.indexOf(gap);
|
||||||
if(gapPos==-1)
|
if(gapPos==-1)
|
||||||
return;
|
return;
|
||||||
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||||
if(result.isEmpty()){
|
if(result.isEmpty()){
|
||||||
displayItems.remove(gapPos);
|
displayItems.remove(gapPos);
|
||||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||||
Status gapStatus=getStatusByID(gap.parentID);
|
Status gapStatus=getStatusByID(gap.parentID);
|
||||||
if(gapStatus!=null){
|
if(gapStatus!=null){
|
||||||
gapStatus.hasGapAfter=false;
|
gapStatus.hasGapAfter=null;
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
Set<String> idsBelowGap=new HashSet<>();
|
// TODO: refactor this code. it's too long. incomprehensible, even
|
||||||
boolean belowGap=false;
|
if(downwards) {
|
||||||
int gapPostIndex=0;
|
Set<String> idsBelowGap=new HashSet<>();
|
||||||
for(Status s:data){
|
boolean belowGap=false;
|
||||||
if(belowGap){
|
int gapPostIndex=0;
|
||||||
idsBelowGap.add(s.id);
|
for(Status s:data){
|
||||||
}else if(s.id.equals(gap.parentID)){
|
if(belowGap){
|
||||||
belowGap=true;
|
idsBelowGap.add(s.id);
|
||||||
s.hasGapAfter=false;
|
}else if(s.id.equals(gap.parentID)){
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
belowGap=true;
|
||||||
}else{
|
s.hasGapAfter=null;
|
||||||
gapPostIndex++;
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||||
|
}else{
|
||||||
|
gapPostIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
int endIndex=0;
|
||||||
int endIndex=0;
|
for(Status s:result){
|
||||||
for(Status s:result){
|
endIndex++;
|
||||||
endIndex++;
|
if(idsBelowGap.contains(s.id))
|
||||||
if(idsBelowGap.contains(s.id))
|
break;
|
||||||
break;
|
}
|
||||||
}
|
if(endIndex==result.size()){
|
||||||
if(endIndex==result.size()){
|
Status last=result.get(result.size()-1);
|
||||||
result.get(result.size()-1).hasGapAfter=true;
|
last.hasGapAfter=last.id;
|
||||||
}else{
|
}else{
|
||||||
result=result.subList(0, endIndex);
|
result=result.subList(0, endIndex);
|
||||||
}
|
}
|
||||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||||
targetList.clear();
|
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
targetList.clear();
|
||||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||||
for(Status s:result){
|
for(Status s:result){
|
||||||
if(idsBelowGap.contains(s.id))
|
if(idsBelowGap.contains(s.id))
|
||||||
break;
|
break;
|
||||||
if(typeFilterPredicate(s) && filterPredicate.test(s)){
|
|
||||||
targetList.addAll(buildDisplayItems(s));
|
targetList.addAll(buildDisplayItems(s));
|
||||||
insertedPosts.add(s);
|
insertedPosts.add(s);
|
||||||
}
|
}
|
||||||
|
if(targetList.isEmpty()){
|
||||||
|
// oops. We didn't add new posts, but at least we know there are none.
|
||||||
|
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||||
|
}else{
|
||||||
|
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||||
|
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
||||||
|
}
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||||
|
} else {
|
||||||
|
String aboveGapID = gap.parentID;
|
||||||
|
int gapPostIndex = 0;
|
||||||
|
for (;gapPostIndex<data.size();gapPostIndex++){
|
||||||
|
if (Objects.equals(aboveGapID, data.get(gapPostIndex).id)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// find if there's an overlap between the new data and the current data
|
||||||
|
int indexOfGapInResponse = 0;
|
||||||
|
for (;indexOfGapInResponse<result.size();indexOfGapInResponse++){
|
||||||
|
if (Objects.equals(aboveGapID, result.get(indexOfGapInResponse).id)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// there is an overlap between new and current data
|
||||||
|
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||||
|
if(indexOfGapInResponse<result.size()){
|
||||||
|
result=result.subList(indexOfGapInResponse+1,result.size());
|
||||||
|
Optional<Status> gapStatus=data.stream()
|
||||||
|
.filter(s->Objects.equals(s.id, gap.parentID))
|
||||||
|
.findFirst();
|
||||||
|
if (gapStatus.isPresent()) {
|
||||||
|
gapStatus.get().hasGapAfter=null;
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus.get()), false);
|
||||||
|
}
|
||||||
|
targetList.clear();
|
||||||
|
} else {
|
||||||
|
gap.loading=false;
|
||||||
|
}
|
||||||
|
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||||
|
for(Status s:result){
|
||||||
|
targetList.addAll(buildDisplayItems(s));
|
||||||
|
insertedPosts.add(s);
|
||||||
|
}
|
||||||
|
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, FilterContext.HOME);
|
||||||
|
if(targetList.isEmpty()){
|
||||||
|
// oops. We didn't add new posts, but at least we know there are none.
|
||||||
|
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||||
|
}else{
|
||||||
|
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||||
|
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
||||||
|
}
|
||||||
|
list.scrollToPosition(getMainAdapterOffset()+gapPos+targetList.size());
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||||
}
|
}
|
||||||
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, getFilterContext());
|
|
||||||
if(targetList.isEmpty()){
|
|
||||||
// oops. We didn't add new posts, but at least we know there are none.
|
|
||||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
|
||||||
}else{
|
|
||||||
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
|
||||||
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
|
||||||
}
|
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +332,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
currentRequest=null;
|
currentRequest=null;
|
||||||
dataLoading=false;
|
dataLoading=false;
|
||||||
}
|
}
|
||||||
if (parent != null) parent.hideNewPostsButton();
|
if(parent!=null) parent.hideNewPostsButton();
|
||||||
super.onRefresh();
|
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.GetList;
|
||||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
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.ListDeletedEvent;
|
||||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.ListEditor;
|
import org.joinmastodon.android.ui.views.ListEditor;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
@@ -40,7 +39,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
private String listID;
|
private String listID;
|
||||||
private String listTitle;
|
private String listTitle;
|
||||||
@Nullable
|
@Nullable
|
||||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
private FollowList.RepliesPolicy repliesPolicy;
|
||||||
private boolean exclusive;
|
private boolean exclusive;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,19 +54,19 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
listID = args.getString("listID");
|
listID = args.getString("listID");
|
||||||
listTitle = args.getString("listTitle");
|
listTitle = args.getString("listTitle");
|
||||||
exclusive = args.getBoolean("listIsExclusive");
|
exclusive = args.getBoolean("listIsExclusive");
|
||||||
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
repliesPolicy = FollowList.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||||
|
|
||||||
setTitle(listTitle);
|
setTitle(listTitle);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
new GetList(listID).setCallback(new Callback<>() {
|
new GetList(listID).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ListTimeline listTimeline) {
|
public void onSuccess(FollowList followList) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
// TODO: save updated info
|
// TODO: save updated info
|
||||||
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
|
if (!followList.title.equals(listTitle)) setTitle(followList.title);
|
||||||
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
|
if (followList.repliesPolicy != null && !followList.repliesPolicy.equals(repliesPolicy)) {
|
||||||
repliesPolicy = listTimeline.repliesPolicy;
|
repliesPolicy = followList.repliesPolicy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +97,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
.setPositiveButton(R.string.save, (d, which) -> {
|
.setPositiveButton(R.string.save, (d, which) -> {
|
||||||
String newTitle = editor.getTitle().trim();
|
String newTitle = editor.getTitle().trim();
|
||||||
setTitle(newTitle);
|
setTitle(newTitle);
|
||||||
new UpdateList(listID, newTitle, editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
new UpdateList(listID, newTitle, editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ListTimeline list) {
|
public void onSuccess(FollowList list) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
setTitle(list.title);
|
setTitle(list.title);
|
||||||
listTitle = list.title;
|
listTitle = list.title;
|
||||||
repliesPolicy = list.repliesPolicy;
|
repliesPolicy = list.repliesPolicy;
|
||||||
@@ -120,7 +119,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
.show();
|
.show();
|
||||||
} else if (item.getItemId() == R.id.delete) {
|
} else if (item.getItemId() == R.id.delete) {
|
||||||
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
||||||
E.post(new ListDeletedEvent(listID));
|
E.post(new ListDeletedEvent(accountID, listID));
|
||||||
Nav.finish(this);
|
Nav.finish(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,13 +133,14 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count) {
|
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) {
|
.setCallback(new SimpleCallback<>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result) {
|
public void onSuccess(List<Status> result) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
boolean more=applyMaxID(result);
|
||||||
onDataLoaded(result, !result.isEmpty());
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||||
|
onDataLoaded(result, more);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.views.ListEditor;
|
import org.joinmastodon.android.ui.views.ListEditor;
|
||||||
@@ -42,7 +43,7 @@ import me.grishka.appkit.api.SimpleCallback;
|
|||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
public class ListsFragment extends MastodonRecyclerFragment<FollowList> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private String profileAccountId;
|
private String profileAccountId;
|
||||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||||
@@ -97,9 +98,9 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||||
.setView(editor)
|
.setView(editor)
|
||||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||||
new CreateList(editor.getTitle(), editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
new CreateList(editor.getTitle(), editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ListTimeline list) {
|
public void onSuccess(FollowList list) {
|
||||||
data.add(0, list);
|
data.add(0, list);
|
||||||
adapter.notifyItemRangeInserted(0, 1);
|
adapter.notifyItemRangeInserted(0, 1);
|
||||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
|
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
|
||||||
@@ -120,10 +121,10 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
private void saveListMembership(String listId, boolean isMember) {
|
private void saveListMembership(String listId, boolean isMember) {
|
||||||
userInList.put(listId, isMember);
|
userInList.put(listId, isMember);
|
||||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
ResultlessMastodonAPIRequest req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||||
req.setCallback(new Callback<>() {
|
req.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Object o) {}
|
public void onSuccess(Void o) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
@@ -139,19 +140,19 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||||
.setCallback(new SimpleCallback<>(this) {
|
.setCallback(new SimpleCallback<>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> lists) {
|
public void onSuccess(List<FollowList> lists) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
for (FollowList l : lists) userInListBefore.put(l.id, true);
|
||||||
userInList.putAll(userInListBefore);
|
userInList.putAll(userInListBefore);
|
||||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||||
if (profileAccountId == null) return;
|
if (profileAccountId == null) return;
|
||||||
|
|
||||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<ListTimeline> allLists) {
|
public void onSuccess(List<FollowList> allLists) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
List<ListTimeline> newLists = new ArrayList<>();
|
List<FollowList> newLists = new ArrayList<>();
|
||||||
for (ListTimeline l : allLists) {
|
for (FollowList l : allLists) {
|
||||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||||
if (!userInListBefore.containsKey(l.id)) {
|
if (!userInListBefore.containsKey(l.id)) {
|
||||||
userInListBefore.put(l.id, false);
|
userInListBefore.put(l.id, false);
|
||||||
@@ -169,8 +170,8 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||||
for (int i = 0; i < data.size(); i++) {
|
for (int i = 0; i < data.size(); i++) {
|
||||||
ListTimeline item = data.get(i);
|
FollowList item = data.get(i);
|
||||||
if (item.id.equals(event.id)) {
|
if (item.id.equals(event.listID)) {
|
||||||
data.remove(i);
|
data.remove(i);
|
||||||
adapter.notifyItemRemoved(i);
|
adapter.notifyItemRemoved(i);
|
||||||
break;
|
break;
|
||||||
@@ -181,7 +182,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||||
for (int i = 0; i < data.size(); i++) {
|
for (int i = 0; i < data.size(); i++) {
|
||||||
ListTimeline item = data.get(i);
|
FollowList item = data.get(i);
|
||||||
if (item.id.equals(event.id)) {
|
if (item.id.equals(event.id)) {
|
||||||
item.title = event.title;
|
item.title = event.title;
|
||||||
item.repliesPolicy = event.repliesPolicy;
|
item.repliesPolicy = event.repliesPolicy;
|
||||||
@@ -230,7 +231,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
private class ListViewHolder extends BindableViewHolder<FollowList> implements UsableRecyclerView.Clickable{
|
||||||
private final TextView title;
|
private final TextView title;
|
||||||
private final CheckBox listToggle;
|
private final CheckBox listToggle;
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(ListTimeline item) {
|
public void onBind(FollowList item) {
|
||||||
title.setText(item.title);
|
title.setText(item.title);
|
||||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
||||||
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
|
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||||
|
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||||
|
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class ManageFollowedHashtagsFragment extends BaseSettingsFragment<Hashtag> implements ListItemWithOptionsMenu.OptionsMenuListener<Hashtag>{
|
||||||
|
private String maxID;
|
||||||
|
|
||||||
|
public ManageFollowedHashtagsFragment(){
|
||||||
|
super(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.manage_hashtags);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetFollowedTags(offset>0 ? maxID : null, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||||
|
maxID=null;
|
||||||
|
if(result.nextPageUri!=null)
|
||||||
|
maxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
|
onDataLoaded(result.stream().map(t->{
|
||||||
|
int posts=t.getWeekPosts();
|
||||||
|
return new ListItemWithOptionsMenu<>(t.name, getResources().getQuantityString(R.plurals.x_posts_recently, posts, posts), ManageFollowedHashtagsFragment.this,
|
||||||
|
R.drawable.ic_fluent_tag_24_regular, ManageFollowedHashtagsFragment.this::onItemClick, t, false);
|
||||||
|
}).collect(Collectors.toList()), maxID!=null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<Hashtag> item, Menu menu){
|
||||||
|
menu.clear();
|
||||||
|
menu.add(getString(R.string.unfollow_user, "#"+item.parentObject.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListItemOptionSelected(ListItemWithOptionsMenu<Hashtag> item, MenuItem menuItem){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(getString(R.string.unfollow_confirmation, "#"+item.parentObject.name))
|
||||||
|
.setPositiveButton(R.string.unfollow, (dlg, which)->doUnfollow(item))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onItemClick(ListItemWithOptionsMenu<Hashtag> item){
|
||||||
|
UiUtils.openHashtagTimeline(getActivity(), accountID, item.parentObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doUnfollow(ListItemWithOptionsMenu<Hashtag> item){
|
||||||
|
new SetTagFollowed(item.parentObject.name, false)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Hashtag result){
|
||||||
|
int index=data.indexOf(item);
|
||||||
|
if(index==-1)
|
||||||
|
return;
|
||||||
|
data.remove(index);
|
||||||
|
list.getAdapter().notifyItemRemoved(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||||
|
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
|
||||||
|
private ImageButton fab;
|
||||||
|
|
||||||
|
public ManageListsFragment(){
|
||||||
|
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setTitle(R.string.manage_lists);
|
||||||
|
loadData();
|
||||||
|
setRefreshEnabled(true);
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
E.unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
Callback<List<FollowList>> callback=new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> result){
|
||||||
|
onDataLoaded(result.stream().map(ManageListsFragment.this::makeItem).collect(Collectors.toList()), false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(refreshing){
|
||||||
|
AccountSessionManager.get(accountID)
|
||||||
|
.getCacheController()
|
||||||
|
.reloadLists(callback);
|
||||||
|
}else{
|
||||||
|
AccountSessionManager.get(accountID)
|
||||||
|
.getCacheController()
|
||||||
|
.getLists(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListItem<FollowList> makeItem(FollowList l){
|
||||||
|
return new ListItemWithOptionsMenu<>(l.title, null, ManageListsFragment.this, R.drawable.ic_list_alt_24px, ManageListsFragment.this::onListClick, l, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onListClick(ListItemWithOptionsMenu<FollowList> item){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||||
|
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<FollowList> item, Menu menu){
|
||||||
|
menu.add(0, R.id.edit, 0, R.string.edit_list);
|
||||||
|
menu.add(0, R.id.delete, 1, R.string.delete_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListItemOptionSelected(ListItemWithOptionsMenu<FollowList> item, MenuItem menuItem){
|
||||||
|
int id=menuItem.getItemId();
|
||||||
|
if(id==R.id.edit){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||||
|
Nav.go(getActivity(), EditListFragment.class, args);
|
||||||
|
}else if(id==R.id.delete){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.delete_list)
|
||||||
|
.setMessage(getString(R.string.delete_list_confirm, item.parentObject.title))
|
||||||
|
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList(item.parentObject))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
fab=view.findViewById(R.id.fab);
|
||||||
|
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
|
||||||
|
fab.setContentDescription(getString(R.string.create_list));
|
||||||
|
fab.setOnClickListener(v->onFabClick());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets){
|
||||||
|
super.onApplyWindowInsets(insets);
|
||||||
|
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doDeleteList(FollowList list){
|
||||||
|
new DeleteList(list.id)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
for(int i=0;i<data.size();i++){
|
||||||
|
if(data.get(i).parentObject==list){
|
||||||
|
data.remove(i);
|
||||||
|
itemsAdapter.notifyItemRemoved(i);
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().deleteList(list.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
Activity activity=getActivity();
|
||||||
|
if(activity==null)
|
||||||
|
return;
|
||||||
|
error.showToast(activity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onListUpdated(ListUpdatedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
for(ListItem<FollowList> item:data){
|
||||||
|
if(item.parentObject.id.equals(ev.list.id)){
|
||||||
|
item.parentObject=ev.list;
|
||||||
|
item.title=ev.list.title;
|
||||||
|
rebindItem(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onListDeleted(ListDeletedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
int i=0;
|
||||||
|
for(ListItem<FollowList> item:data){
|
||||||
|
if(item.parentObject.id.equals(ev.listID)){
|
||||||
|
data.remove(i);
|
||||||
|
itemsAdapter.notifyItemRemoved(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onListCreated(ListCreatedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
ListItem<FollowList> item=makeItem(ev.list);
|
||||||
|
data.add(item);
|
||||||
|
((List<ListItem<FollowList>>)data).sort(Comparator.comparing(l->l.parentObject.title));
|
||||||
|
itemsAdapter.notifyItemInserted(data.indexOf(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFabClick(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Nav.go(getActivity(), CreateListFragment.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ package org.joinmastodon.android.fragments;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
|
|||||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||||
if(refreshLayout!=null)
|
if(refreshLayout!=null)
|
||||||
setRefreshLayoutColors(refreshLayout);
|
setRefreshLayoutColors(refreshLayout);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.assist.AssistContent;
|
import android.app.assist.AssistContent;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -15,10 +16,6 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
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.events.FollowRequestHandledEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
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.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||||
@@ -39,6 +38,11 @@ import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
|||||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
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.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
@@ -46,7 +50,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
|
|
||||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener {
|
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener, HasAccountID {
|
||||||
|
|
||||||
TabLayout tabLayout;
|
TabLayout tabLayout;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
@@ -54,7 +58,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
private View tabsDivider;
|
private View tabsDivider;
|
||||||
private TabLayoutMediator tabLayoutMediator;
|
private TabLayoutMediator tabLayoutMediator;
|
||||||
String unreadMarker, realUnreadMarker;
|
String unreadMarker, realUnreadMarker;
|
||||||
private MenuItem markAllReadItem;
|
private MenuItem markAllReadItem, filterItem;
|
||||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||||
private ElevationOnScrollListener elevationOnScrollListener;
|
private ElevationOnScrollListener elevationOnScrollListener;
|
||||||
|
|
||||||
@@ -92,9 +96,10 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
inflater.inflate(R.menu.notifications, menu);
|
inflater.inflate(R.menu.notifications, menu);
|
||||||
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
|
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);
|
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||||
updateMarkAllReadButton();
|
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
|
@Override
|
||||||
@@ -116,11 +121,61 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
if (getCurrentFragment() instanceof NotificationsListFragment nlf) {
|
if (getCurrentFragment() instanceof NotificationsListFragment nlf) {
|
||||||
nlf.resetUnreadBackground();
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
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(){
|
void markAsRead(){
|
||||||
if(allNotificationsFragment.getData().isEmpty()) return;
|
if(allNotificationsFragment.getData().isEmpty()) return;
|
||||||
String id=allNotificationsFragment.getData().get(0).id;
|
String id=allNotificationsFragment.getData().get(0).id;
|
||||||
@@ -184,6 +239,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
public void onPageSelected(int position){
|
public void onPageSelected(int position){
|
||||||
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f)
|
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f)
|
||||||
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
|
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
|
||||||
|
filterItem.setVisible(position==0);
|
||||||
if(position==0)
|
if(position==0)
|
||||||
return;
|
return;
|
||||||
Fragment _page=getFragmentForPage(position);
|
Fragment _page=getFragmentForPage(position);
|
||||||
@@ -254,7 +310,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Account> accounts) {
|
public void onSuccess(HeaderPaginationList<Account> accounts) {
|
||||||
if (getActivity() == null) return;
|
if(getActivity()==null) return;
|
||||||
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
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);
|
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccountID(){
|
||||||
|
return accountID;
|
||||||
|
}
|
||||||
|
|
||||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import android.graphics.Rect;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
@@ -14,7 +18,9 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
@@ -27,6 +33,7 @@ import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
|||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
@@ -36,6 +43,7 @@ import org.parceler.Parcels;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@@ -43,14 +51,14 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
|
||||||
|
|
||||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
|
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||||
private boolean onlyMentions;
|
private boolean onlyMentions;
|
||||||
private boolean onlyPosts;
|
|
||||||
private String maxID;
|
private String maxID;
|
||||||
private boolean reloadingFromCache;
|
private boolean reloadingFromCache;
|
||||||
private DiscoverInfoBannerHelper bannerHelper;
|
private DiscoverInfoBannerHelper bannerHelper;
|
||||||
|
private String unreadMarker, realUnreadMarker;
|
||||||
|
private MenuItem markAllReadItem;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean wantsComposeButton() {
|
protected boolean wantsComposeButton() {
|
||||||
@@ -61,13 +69,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
E.register(this);
|
E.register(this);
|
||||||
if(savedInstanceState!=null){
|
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
|
setHasOptionsMenu(true);
|
||||||
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
|
|
||||||
}
|
|
||||||
if (onlyPosts) {
|
|
||||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -79,31 +82,63 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity){
|
public void onAttach(Activity activity){
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
|
||||||
onlyPosts=getArguments().getBoolean("onlyPosts", false);
|
|
||||||
setTitle(R.string.notifications);
|
setTitle(R.string.notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||||
|
if(!onlyMentions){
|
||||||
|
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;
|
NotificationHeaderStatusDisplayItem titleItem;
|
||||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||||
titleItem=null;
|
titleItem=null;
|
||||||
}else{
|
}else{
|
||||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||||
}
|
}
|
||||||
if (n.type == Notification.Type.FOLLOW_REQUEST) {
|
if (n.type == Notification.Type.FOLLOW_REQUEST || n.type == Notification.Type.FOLLOW) {
|
||||||
ArrayList<StatusDisplayItem> items = new ArrayList<>();
|
ArrayList<StatusDisplayItem> items = new ArrayList<>();
|
||||||
items.add(titleItem);
|
items.add(titleItem);
|
||||||
items.add(new AccountCardStatusDisplayItem(n.id, this, n.account, n));
|
items.add(new AccountCardStatusDisplayItem(n.id, this, accountID, n.account, n));
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
if(n.status!=null){
|
if(n.status!=null){
|
||||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); // | StatusDisplayItem.FLAG_NO_HEADER);
|
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); // | StatusDisplayItem.FLAG_NO_HEADER);
|
||||||
if (GlobalUserPreferences.spectatorMode)
|
if (GlobalUserPreferences.spectatorMode)
|
||||||
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
|
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
|
||||||
if (!getLocalPrefs().showEmojiReactionsInLists)
|
if (!GlobalUserPreferences.showMediaPreview)
|
||||||
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
|
flags |= StatusDisplayItem.FLAG_NO_MEDIA_PREVIEW;
|
||||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
|
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
|
||||||
if(titleItem!=null)
|
if(titleItem!=null)
|
||||||
items.add(0, titleItem);
|
items.add(0, titleItem);
|
||||||
@@ -128,13 +163,21 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
AccountSessionManager.getInstance()
|
AccountSessionManager.getInstance()
|
||||||
.getAccount(accountID).getCacheController()
|
.getAccount(accountID).getCacheController()
|
||||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, false, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
Set<String> needRelationships=result.items.stream()
|
||||||
|
.filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id))
|
||||||
|
.map(ntf->ntf.account.id)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
loadRelationships(needRelationships);
|
||||||
|
|
||||||
maxID=result.maxID;
|
maxID=result.maxID;
|
||||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||||
|
if(bannerHelper!=null) bannerHelper.onBannerBecameVisible();
|
||||||
reloadingFromCache=false;
|
reloadingFromCache=false;
|
||||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||||
nf.updateMarkAllReadButton();
|
nf.updateMarkAllReadButton();
|
||||||
@@ -143,16 +186,24 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRelationshipsLoaded(){
|
||||||
|
if(getActivity()==null)
|
||||||
|
return;
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
|
||||||
|
accountHolder.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onShown(){
|
protected void onShown(){
|
||||||
super.onShown();
|
super.onShown();
|
||||||
if(!dataLoading){
|
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||||
if(onlyMentions){
|
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||||
refresh();
|
reloadingFromCache=true;
|
||||||
}else{
|
refresh();
|
||||||
reloadingFromCache=true;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +225,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
|
||||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||||
private Paint paint=new Paint();
|
private Paint paint=new Paint();
|
||||||
private Rect tmpRect=new Rect();
|
private Rect tmpRect=new Rect();
|
||||||
@@ -214,13 +264,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState){
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putBoolean("onlyMentions", onlyMentions);
|
|
||||||
outState.putBoolean("onlyPosts", onlyPosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Notification getNotificationByID(String id){
|
private Notification getNotificationByID(String id){
|
||||||
for(Notification n:data){
|
for(Notification n:data){
|
||||||
if(n.id.equals(id))
|
if(n.id.equals(id))
|
||||||
@@ -238,7 +281,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
continue;
|
continue;
|
||||||
Status contentStatus=ntf.status.getContentStatus();
|
Status contentStatus=ntf.status.getContentStatus();
|
||||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
||||||
updatePoll(ntf.id, ntf.status, ev.poll);
|
updatePoll(ntf.id, contentStatus, ev.poll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,8 +291,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
|
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
|
||||||
for(Notification n:data){
|
for(Notification n:data){
|
||||||
if (n.status == null) continue;
|
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
|
||||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
|
||||||
n.status.getContentStatus().update(ev);
|
n.status.getContentStatus().update(ev);
|
||||||
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
||||||
for(int i=0;i<list.getChildCount();i++){
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
@@ -258,15 +300,36 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
footer.rebind();
|
footer.rebind();
|
||||||
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||||
footer.rebind();
|
footer.rebind();
|
||||||
}else if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && ev.viewHolder!=holder){
|
|
||||||
reactions.rebind();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(Notification n:preloadedData){
|
for(Notification n:preloadedData){
|
||||||
if (n.status == null) continue;
|
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
|
||||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
n.status.getContentStatus().update(ev);
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){
|
||||||
|
for(Notification n : data){
|
||||||
|
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
|
||||||
|
n.status.getContentStatus().update(ev);
|
||||||
|
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
||||||
|
for(int i=0; i<list.getChildCount(); i++){
|
||||||
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && reactions.getItem().status==n.status.getContentStatus() && ev.viewHolder!=holder){
|
||||||
|
reactions.rebind();
|
||||||
|
}else if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(n.getID())){
|
||||||
|
text.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(Notification n : preloadedData){
|
||||||
|
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
|
||||||
n.status.getContentStatus().update(ev);
|
n.status.getContentStatus().update(ev);
|
||||||
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
|
||||||
}
|
}
|
||||||
@@ -306,11 +369,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||||
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
|
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
inflater.inflate(R.menu.notifications, menu);
|
||||||
|
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if(item.getItemId()==R.id.mark_all_read){
|
||||||
|
markAsRead();
|
||||||
|
resetUnreadBackground();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markAsRead(){
|
||||||
|
if(data.isEmpty())
|
||||||
|
return;
|
||||||
|
String id=data.get(0).id;
|
||||||
|
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||||
|
new SaveMarkers(null, id).exec(accountID);
|
||||||
|
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||||
|
realUnreadMarker=id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void resetUnreadBackground(){
|
void resetUnreadBackground(){
|
||||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||||
nf.unreadMarker=nf.realUnreadMarker;
|
nf.unreadMarker=nf.realUnreadMarker;
|
||||||
@@ -322,13 +412,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
public void onRefresh(){
|
public void onRefresh(){
|
||||||
super.onRefresh();
|
super.onRefresh();
|
||||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||||
if (!onlyMentions && !onlyPosts) nf.markAsRead();
|
if (!onlyMentions) nf.markAsRead();
|
||||||
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||||
nf.unreadMarker=nf.realUnreadMarker=m;
|
nf.unreadMarker=nf.realUnreadMarker=m;
|
||||||
nf.updateMarkAllReadButton();
|
nf.updateMarkAllReadButton();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resetUnreadBackground();
|
resetUnreadBackground();
|
||||||
|
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||||
|
unreadMarker=realUnreadMarker=m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMarkAllReadButton(){
|
||||||
|
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -346,4 +443,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
? "/users/" + getSession().self.username + "/interactions"
|
? "/users/" + getSession().self.username + "/interactions"
|
||||||
: "/notifications").build();
|
: "/notifications").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canRefreshWithoutUpsettingUser(){
|
||||||
|
// TODO maybe reload notifications the same way we reload the home timelines, i.e. with gaps and stuff
|
||||||
|
if(data.size()<=itemsPerPage)
|
||||||
|
return true;
|
||||||
|
for(int i=list.getChildCount()-1;i>=0;i--){
|
||||||
|
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
|
||||||
|
String id=itemHolder.getItemID();
|
||||||
|
for(int j=0;j<data.size();j++){
|
||||||
|
if(data.get(j).id.equals(id))
|
||||||
|
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.Bundle;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
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.Account;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
@@ -35,6 +36,8 @@ public class PinnedPostsListFragment extends StatusListFragment{
|
|||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
if(getActivity()==null) return;
|
||||||
|
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||||
onDataLoaded(result, false);
|
onDataLoaded(result, false);
|
||||||
}
|
}
|
||||||
}).exec(accountID);
|
}).exec(accountID);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user