Compare commits
1113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dffbff97c | ||
|
|
efb8cd565b | ||
|
|
1f5bdb975b | ||
|
|
22dfc33974 | ||
|
|
6915d19fb4 | ||
|
|
ad2ef39ace | ||
|
|
3cff655e6f | ||
|
|
ed86a5a3e8 | ||
|
|
f329435f51 | ||
|
|
6a6a80bcd7 | ||
|
|
62e4983f02 | ||
|
|
6dfd991e87 | ||
|
|
e205462bf4 | ||
|
|
03f341f6f8 | ||
|
|
b9b08c5ea7 | ||
|
|
2b5498ff5d | ||
|
|
84b058873d | ||
|
|
fcf5c0822e | ||
|
|
53c3da6a3d | ||
|
|
68371c9a0f | ||
|
|
e7295aac07 | ||
|
|
ae7f65954a | ||
|
|
350a73c3eb | ||
|
|
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 | ||
|
|
97ca2634a0 | ||
|
|
6630f0f8da | ||
|
|
129b253176 | ||
|
|
c2382d065e | ||
|
|
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 | ||
|
|
f4854061ea | ||
|
|
bf7607674e | ||
|
|
137a8ca27b | ||
|
|
b9ed4e0ee2 | ||
|
|
bc04672d32 | ||
|
|
70c668ecf1 | ||
|
|
64bbe2c438 | ||
|
|
32209e766e | ||
|
|
99349cff0a | ||
|
|
74ca1961e0 | ||
|
|
ef6ba7fe0c | ||
|
|
9ace2b71cc | ||
|
|
0c54654b8b | ||
|
|
bf686309fb | ||
|
|
ce4f46537b | ||
|
|
4c43207f17 | ||
|
|
afe5bcd1f3 | ||
|
|
3bda81bd43 | ||
|
|
7339b2325f | ||
|
|
ee84a9ee7e | ||
|
|
fef594150a | ||
|
|
10371f69cb | ||
|
|
75cf3d76fb | ||
|
|
51a7d00c47 | ||
|
|
9ac8261cc4 | ||
|
|
1f4ad80b7d | ||
|
|
4b090f0d68 | ||
|
|
4002bcde26 | ||
|
|
ded3777b40 | ||
|
|
7236066003 | ||
|
|
033f07ea09 | ||
|
|
283c0cba4b | ||
|
|
e3a1fc2fbb | ||
|
|
95de9e2917 | ||
|
|
a82ebeed11 | ||
|
|
3a3aa0be1c | ||
|
|
e72491c2d1 | ||
|
|
36dede1f93 | ||
|
|
ed15daf9e9 | ||
|
|
c6052c841d | ||
|
|
ce39c7ca8f | ||
|
|
b7723dcb98 | ||
|
|
ad0774f8a5 | ||
|
|
9172feb72b | ||
|
|
a297bd3281 | ||
|
|
e713a9cfc3 | ||
|
|
195395a22d | ||
|
|
7b6a62b047 | ||
|
|
ada1c9ff6d | ||
|
|
5a0a14ed56 | ||
|
|
cad3879646 | ||
|
|
5d961991d4 | ||
|
|
e27536743f | ||
|
|
9f8d4a0f34 | ||
|
|
67b6a89fd9 | ||
|
|
dabc4058ba | ||
|
|
6c468602c6 | ||
|
|
9c5d29a860 | ||
|
|
da5e2a6b50 | ||
|
|
a194569fd4 | ||
|
|
78a4ace9b2 | ||
|
|
9a664088cd | ||
|
|
1d2e6f880b | ||
|
|
2cd2918d53 | ||
|
|
9b49db6677 | ||
|
|
9f6c61e5c0 | ||
|
|
b6a2bb7881 | ||
|
|
62262010b9 | ||
|
|
72fe9a04a6 | ||
|
|
d8cf55ae21 | ||
|
|
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 | ||
|
|
58f746a285 | ||
|
|
a6bba42a49 | ||
|
|
519d6868b2 | ||
|
|
5322120097 | ||
|
|
2c88c86480 | ||
|
|
55f32fd45b | ||
|
|
f39f0b03d1 | ||
|
|
ff2f1a4955 | ||
|
|
b283e216a7 | ||
|
|
4328d568b3 | ||
|
|
8edc47703f | ||
|
|
92ce906163 | ||
|
|
6e141e360e | ||
|
|
d1aba87e13 | ||
|
|
723853079e | ||
|
|
cd0742c093 | ||
|
|
52d5de5aec | ||
|
|
4f8a5ae5db | ||
|
|
616f2463c7 | ||
|
|
d3b711a966 | ||
|
|
827fe34709 | ||
|
|
4b6c0242d5 | ||
|
|
c3cbc16084 | ||
|
|
36493bfc88 | ||
|
|
66ce93a3ff | ||
|
|
957bc76dbb | ||
|
|
1f5a28fb33 | ||
|
|
045c58ce66 | ||
|
|
e2dde7239f | ||
|
|
512ad93eea | ||
|
|
19759023a4 | ||
|
|
714d3399ce | ||
|
|
e1850e5282 | ||
|
|
a05c917b2c | ||
|
|
8f3a9c265c | ||
|
|
6f1a33b76e | ||
|
|
8f0451175f | ||
|
|
37a3a4f1c0 | ||
|
|
bd85746726 | ||
|
|
96265010bf | ||
|
|
4209951ce3 | ||
|
|
f1cbd95439 | ||
|
|
d63382c6d9 | ||
|
|
20697fb334 | ||
|
|
1090d1ca42 | ||
|
|
bec4acdf51 | ||
|
|
800b78bfd8 | ||
|
|
52b01b7bbe | ||
|
|
8b71764207 | ||
|
|
a5d7a75f32 | ||
|
|
8839bcb7aa | ||
|
|
bcaf71760d | ||
|
|
1d95204648 | ||
|
|
83bf2a808f | ||
|
|
7ffb0a01c6 | ||
|
|
0710113148 | ||
|
|
7c43e9a1af | ||
|
|
f9e768c378 | ||
|
|
2063dbd0b0 | ||
|
|
057683c72f | ||
|
|
4963a0e722 | ||
|
|
1d66d288bd | ||
|
|
b8f49157c3 | ||
|
|
d77a62ef6f | ||
|
|
c531150483 | ||
|
|
991f41c531 | ||
|
|
b5fb7dd2ec | ||
|
|
4fe8532971 | ||
|
|
4ae1e7d33e | ||
|
|
43fa4526a4 | ||
|
|
fc4b1da323 | ||
|
|
843755f4e4 | ||
|
|
80e02f7520 | ||
|
|
af8f52e589 | ||
|
|
bc3f48dec9 | ||
|
|
74ee832507 | ||
|
|
da1b2d09b1 | ||
|
|
99f8607211 | ||
|
|
ef293088e1 | ||
|
|
e08e72ccb0 | ||
|
|
b692440bab | ||
|
|
7061abc64b | ||
|
|
0dd5064abb | ||
|
|
a1aafff6ce | ||
|
|
1f88f154af | ||
|
|
3d1e0364c6 | ||
|
|
0f1b5431bb | ||
|
|
0369d3fa62 | ||
|
|
154e3a732a | ||
|
|
9c979db043 | ||
|
|
0af089db89 | ||
|
|
1335613860 | ||
|
|
cb86bfd8dc | ||
|
|
a0d32ae493 | ||
|
|
f7e56a6c40 | ||
|
|
56613c75f7 | ||
|
|
fb3c35c0a0 | ||
|
|
4b3dc0a59f | ||
|
|
7855615a7b | ||
|
|
ff6576f4da | ||
|
|
931fa9a9b0 | ||
|
|
77a70967f2 | ||
|
|
e5506d952c | ||
|
|
2c2dbd0761 | ||
|
|
e6f5ecd496 | ||
|
|
73cea2d83c | ||
|
|
835a576f44 | ||
|
|
0a090341cc | ||
|
|
453671abfb | ||
|
|
cfa7daa984 | ||
|
|
88f913f586 | ||
|
|
5b4aeb4923 | ||
|
|
19133a2913 | ||
|
|
293035b7c8 | ||
|
|
d06723de5c | ||
|
|
bc45d0c499 | ||
|
|
c320cccf6f | ||
|
|
e3aebbd145 | ||
|
|
e15d378e46 | ||
|
|
b6720d10fb | ||
|
|
83a2dbe8a1 | ||
|
|
5b8592a99d | ||
|
|
18a094c06c | ||
|
|
a319ff3dc0 | ||
|
|
0cb46eca1a | ||
|
|
d85c814cba | ||
|
|
f49e660f29 | ||
|
|
afa407e7d1 | ||
|
|
37e0f5ecea | ||
|
|
5000fdcfea | ||
|
|
2ec7489dbf | ||
|
|
05965cea6e | ||
|
|
279e22ccb3 | ||
|
|
6a6fc1ca8b | ||
|
|
b6b5426297 | ||
|
|
e332ddda74 | ||
|
|
2cd4cfb883 | ||
|
|
ef12d09d35 | ||
|
|
1e365a8a7c | ||
|
|
e9363b41fd | ||
|
|
5e99df137a | ||
|
|
c0d0b45e24 | ||
|
|
3340b4cdfa | ||
|
|
d4afcc3383 | ||
|
|
dad423eb04 | ||
|
|
7b275d7e3d | ||
|
|
5274ecb721 | ||
|
|
e45367a482 | ||
|
|
83532edaab | ||
|
|
793d28da6a | ||
|
|
a8e575f680 | ||
|
|
98b0b3f9dd | ||
|
|
2e6d9c296a | ||
|
|
a07dc96ef9 | ||
|
|
8ba097a68a | ||
|
|
b1dd990fea | ||
|
|
ba7864b910 | ||
|
|
5d8fa343cd | ||
|
|
3fc49c431b | ||
|
|
79b6e65ce3 | ||
|
|
9f457d0d76 | ||
|
|
aa2ff62db4 | ||
|
|
73fffca569 | ||
|
|
45589fc033 | ||
|
|
79b81ed932 | ||
|
|
d1242870df | ||
|
|
e0dbbc4bc0 | ||
|
|
bf89791817 | ||
|
|
e3197f6dc1 | ||
|
|
e6317aa898 | ||
|
|
c73dc326fd | ||
|
|
287e250357 | ||
|
|
9673a14420 | ||
|
|
3333fdc8d7 | ||
|
|
9fb4b8bb6e | ||
|
|
7b10ed13f4 | ||
|
|
c528bd797d | ||
|
|
264529705c | ||
|
|
4669e3dfc7 | ||
|
|
eff3798964 | ||
|
|
78c526c25b | ||
|
|
ec13415d1f | ||
|
|
96622184ae | ||
|
|
3742c1c862 | ||
|
|
a0c7757428 | ||
|
|
15f9f4906a | ||
|
|
d577cd9b21 | ||
|
|
cf610cbb87 | ||
|
|
1e1095204d | ||
|
|
3fb6a13a3a | ||
|
|
2826655fe2 | ||
|
|
0ccf450b28 | ||
|
|
d28b9460af | ||
|
|
3e1bdf98c2 | ||
|
|
3f87764230 | ||
|
|
25e8e2e9e1 | ||
|
|
3faf2ce9b9 | ||
|
|
cbe243fc9e | ||
|
|
a438f633be | ||
|
|
37ef67d7ac | ||
|
|
67d631b0f0 | ||
|
|
fc302ffa5f | ||
|
|
8c28556a94 | ||
|
|
45cc531eec | ||
|
|
5c9ad9286d | ||
|
|
ad1c9486d7 | ||
|
|
ad6a03b712 | ||
|
|
36bb8010bc | ||
|
|
2200da7a16 | ||
|
|
688c0e2e85 | ||
|
|
714345a65d | ||
|
|
34a1c7e408 | ||
|
|
6255221d6a | ||
|
|
58364de72a | ||
|
|
6d64df4ee4 | ||
|
|
7bac2f206b | ||
|
|
75e1a17a2c | ||
|
|
47b13384a8 | ||
|
|
77b9efa7d1 | ||
|
|
be5f3b18af | ||
|
|
d5d12a7ce5 | ||
|
|
d7726d7755 | ||
|
|
0cd0d37eff | ||
|
|
4521def103 | ||
|
|
5c70f0a758 | ||
|
|
c12c2c0416 | ||
|
|
db45c422e7 | ||
|
|
affd9a95c5 | ||
|
|
7baf25869a | ||
|
|
12096fb427 | ||
|
|
ef7136cb81 | ||
|
|
3c4baf0126 | ||
|
|
f0b87c62a5 | ||
|
|
a319435e91 | ||
|
|
5bd0e988e3 | ||
|
|
b2be669b9e | ||
|
|
51952b0485 | ||
|
|
2b0c5e7fac | ||
|
|
3e6cea1a6a | ||
|
|
1aec7c0999 | ||
|
|
5da98809a5 | ||
|
|
49695614b7 | ||
|
|
3fbbc104b7 | ||
|
|
2fe7c0b85e | ||
|
|
09d0e82216 | ||
|
|
d208fcea7d | ||
|
|
cc0674db34 | ||
|
|
1d5b84943d | ||
|
|
14fe992ca5 | ||
|
|
15232bddaf | ||
|
|
160ef25621 | ||
|
|
2afb8688a3 | ||
|
|
9d1af035ea | ||
|
|
fb7574d814 | ||
|
|
201a3cb9e3 | ||
|
|
cc735ee6a1 | ||
|
|
0165e14ea0 | ||
|
|
97d19605d5 | ||
|
|
bc490218f9 | ||
|
|
6dac05a21d | ||
|
|
fd3fff6322 | ||
|
|
edb64fff2e | ||
|
|
fe0e854e72 | ||
|
|
06c85fb203 | ||
|
|
69926c4ae1 | ||
|
|
ef44b0a412 | ||
|
|
8577ac1027 | ||
|
|
32da050106 | ||
|
|
526b74b3ef | ||
|
|
97ab328a9c | ||
|
|
21603eedcf | ||
|
|
7fbef273a1 | ||
|
|
9e19716504 | ||
|
|
b473642ab4 | ||
|
|
fba55f01a0 | ||
|
|
015e63ba66 | ||
|
|
d92e2407f3 | ||
|
|
a4f84fb8cd | ||
|
|
bfe88745ca | ||
|
|
0d334237ba | ||
|
|
fd5cff3fea | ||
|
|
af5b82e9fd | ||
|
|
d3561748c8 | ||
|
|
791a1d804b | ||
|
|
2442424e3b | ||
|
|
0ecedd2820 | ||
|
|
958d62ec0c | ||
|
|
400cfb2141 | ||
|
|
52b860dd8f | ||
|
|
4d57d8d576 | ||
|
|
9a098accd8 | ||
|
|
62f3b2522c | ||
|
|
9b48cd2037 | ||
|
|
69776d45d1 | ||
|
|
b8fb2660a4 | ||
|
|
941281298d | ||
|
|
8afc4511a6 | ||
|
|
f43ef325ae | ||
|
|
bbdc323204 | ||
|
|
8ed9fb6276 | ||
|
|
27cbb70352 | ||
|
|
f5b10b516c | ||
|
|
5580308968 | ||
|
|
901c70efc3 | ||
|
|
3d44e5d2cc | ||
|
|
33ea3da84d | ||
|
|
572901ec9d | ||
|
|
965239d215 | ||
|
|
ac1e5e991e | ||
|
|
e97203a6e3 | ||
|
|
66b7b127f9 | ||
|
|
b3f2987b14 | ||
|
|
c7426453a5 | ||
|
|
664d5cc4c3 | ||
|
|
a44e0e036a | ||
|
|
5d54c1bae4 | ||
|
|
2ef17ba051 | ||
|
|
f63daf3a4e | ||
|
|
52b079be2a | ||
|
|
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 | ||
|
|
498078b6e0 | ||
|
|
526f5e319b | ||
|
|
d419dba44a | ||
|
|
fd98159fce | ||
|
|
f5b98009dd | ||
|
|
cf0b66d852 | ||
|
|
197d0caf44 | ||
|
|
a4a082f76a | ||
|
|
84026afb92 | ||
|
|
4dea7d2a52 | ||
|
|
2df1b7dd61 | ||
|
|
89042113a5 | ||
|
|
48665ebcce | ||
|
|
2528d48010 | ||
|
|
5456d71979 | ||
|
|
e36aae3cf3 | ||
|
|
6d12e2dd72 | ||
|
|
f117249bb5 | ||
|
|
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 | ||
|
|
22aac3d943 | ||
|
|
53afc120f3 | ||
|
|
a75ce70615 | ||
|
|
4a3b948760 | ||
|
|
f81283c892 | ||
|
|
7eae879037 | ||
|
|
1b0ce5d893 | ||
|
|
c17745368d | ||
|
|
e78b518654 | ||
|
|
55a8634be2 | ||
|
|
ac891eea53 | ||
|
|
74fa2a3081 | ||
|
|
6c1c5b7759 | ||
|
|
1f4152b588 | ||
|
|
70386ea1b2 | ||
|
|
cbce90c461 | ||
|
|
74ae3bf706 | ||
|
|
1feccdc26d | ||
|
|
c38c2a425b | ||
|
|
f43352b790 | ||
|
|
c5b52b2781 | ||
|
|
b91840fb95 | ||
|
|
fc10fbffb0 | ||
|
|
e40841c128 | ||
|
|
98a02e874b | ||
|
|
b06df8c3d0 | ||
|
|
a00afd5d7f | ||
|
|
9a41a2d6fb | ||
|
|
2cd98a6620 | ||
|
|
283b56be5b | ||
|
|
6d56771aba | ||
|
|
1724d8a532 | ||
|
|
b4cdf35d36 | ||
|
|
cad0ad7a59 | ||
|
|
ca60003c39 | ||
|
|
0f030e0bac | ||
|
|
6d4f212a18 | ||
|
|
183b39bc24 | ||
|
|
27ad0c6fcf | ||
|
|
b5f661f1af | ||
|
|
0015f3f0bf | ||
|
|
c5d0fdd645 | ||
|
|
2d09ad44fb | ||
|
|
667fffd124 | ||
|
|
699233d8c7 | ||
|
|
56aabdc4a6 | ||
|
|
443e2c7a6f | ||
|
|
985b0f6e63 | ||
|
|
cc86edf276 | ||
|
|
4071b9342d | ||
|
|
f71d1bc5d3 | ||
|
|
6bcdbaba34 | ||
|
|
a2beead3a5 | ||
|
|
e7a25e353d | ||
|
|
af04a01130 | ||
|
|
fe1cfa1d7b | ||
|
|
b248797bb0 | ||
|
|
f24eba08d3 | ||
|
|
0e89559a47 | ||
|
|
d02a72e079 | ||
|
|
3be57d1b0b | ||
|
|
bed550e97c | ||
|
|
7e2619ea75 | ||
|
|
4b22f1d3a7 | ||
|
|
9dcc7e293f | ||
|
|
6a68cf5e41 | ||
|
|
29297be4a3 | ||
|
|
90b87529e0 | ||
|
|
39af05524d | ||
|
|
e3fb2cd03c | ||
|
|
90f84d628a | ||
|
|
b89e0b5c5a | ||
|
|
aac89c354c | ||
|
|
a032f9af10 | ||
|
|
642aaec6da | ||
|
|
ff667d6aed | ||
|
|
5e98496ea6 | ||
|
|
972fe1d15b | ||
|
|
26eaa36faa | ||
|
|
c517f41595 | ||
|
|
56a6d7243f | ||
|
|
18e43dfc22 | ||
|
|
816f6370ef | ||
|
|
ebc2b2e59d | ||
|
|
c9a796dbfe | ||
|
|
1ba185ea9c | ||
|
|
a78be8bc1d | ||
|
|
abfb497577 | ||
|
|
a10b184508 | ||
|
|
f0ea6660e6 | ||
|
|
a829f25d56 | ||
|
|
deff3dd8e0 | ||
|
|
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 | ||
|
|
645af12c3f | ||
|
|
fadc42d72b | ||
|
|
fc831e7d42 | ||
|
|
2998ee9145 | ||
|
|
971c4e5879 | ||
|
|
b396ee7987 | ||
|
|
0f803cd4fa | ||
|
|
167a14b8db | ||
|
|
81cbc2d10c | ||
|
|
9bd8aff99b | ||
|
|
a770828165 | ||
|
|
ab457035ff | ||
|
|
f886e4c1d2 | ||
|
|
be73c9e81c | ||
|
|
1c2183bf1a | ||
|
|
1789d90dc3 | ||
|
|
57306ff7fe | ||
|
|
f0eb6573f4 | ||
|
|
e7f5dd3357 | ||
|
|
8101bb9ea1 | ||
|
|
228fdc8ffe | ||
|
|
e9df125cde | ||
|
|
16ef577a7a | ||
|
|
734b3bced6 | ||
|
|
5f6f3c94c9 | ||
|
|
09ba42a974 | ||
|
|
d76e823489 | ||
|
|
900b204bb0 | ||
|
|
17d679901a | ||
|
|
d8036779f8 | ||
|
|
3f6bda28b3 | ||
|
|
c0b4f4dd79 | ||
|
|
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 | ||
|
|
e5bdeba1d7 | ||
|
|
8d7db7774f | ||
|
|
78d22c670c | ||
|
|
0a679109f5 | ||
|
|
e843142b7e | ||
|
|
72e728f655 | ||
|
|
ef56792f56 | ||
|
|
504a6959e8 | ||
|
|
b8e3060887 | ||
|
|
1aa1ede421 | ||
|
|
480dba7629 | ||
|
|
9b9c66a149 | ||
|
|
0f5eb923ee | ||
|
|
90ed28e7a0 | ||
|
|
d2b45c1c84 | ||
|
|
a119ba5f80 | ||
|
|
8c1191a08f | ||
|
|
4275d596e6 | ||
|
|
cc83f2baf3 | ||
|
|
728496b831 | ||
|
|
bbc99162c6 | ||
|
|
eed3af9e3e | ||
|
|
50187ff376 | ||
|
|
5f30919fb4 | ||
|
|
14c3cfac85 | ||
|
|
e978f02765 | ||
|
|
8d877c480f | ||
|
|
c53efee9a1 | ||
|
|
148c461e86 | ||
|
|
fcadb9883d | ||
|
|
bb6491e10a | ||
|
|
6248ccf376 | ||
|
|
c9e08f36fa | ||
|
|
10b95d753b | ||
|
|
c3989083cf | ||
|
|
01db585094 | ||
|
|
cc67cb330c | ||
|
|
52ed3c5a04 | ||
|
|
5976f6230a | ||
|
|
3553f03a95 | ||
|
|
d6e2d889c3 | ||
|
|
a777b3b450 | ||
|
|
9957efbea0 | ||
|
|
22e7b9730f | ||
|
|
91470b8509 | ||
|
|
c9d5327328 | ||
|
|
1aa61b72e5 | ||
|
|
3ca5edc3fc | ||
|
|
a092ebaeb3 | ||
|
|
5b9e84c255 | ||
|
|
9c058b926f | ||
|
|
4f2d2ae6e8 | ||
|
|
75aa26a018 | ||
|
|
0f795254e5 | ||
|
|
33592f0a83 | ||
|
|
d6fd01eaca | ||
|
|
ee6e0ff26c | ||
|
|
4d9574bf38 | ||
|
|
813be9a2be | ||
|
|
cc76ebfafb | ||
|
|
7989ee0243 | ||
|
|
3aa1997cfd | ||
|
|
c3da15552e | ||
|
|
a014fe9443 | ||
|
|
92551d4ca3 | ||
|
|
8010858e85 | ||
|
|
4efb4875b0 | ||
|
|
c5d041e46d | ||
|
|
53c2223aae | ||
|
|
25034ac0ae | ||
|
|
ac9de72b75 | ||
|
|
1f48ad93f2 | ||
|
|
38f7f7aa00 | ||
|
|
fe8175c63a | ||
|
|
2d9e01bbc1 | ||
|
|
022a227b08 | ||
|
|
a2228259f1 | ||
|
|
a61af7c56f | ||
|
|
5d6a646976 | ||
|
|
628d0d7492 | ||
|
|
b76c8745ec | ||
|
|
51e67bc441 | ||
|
|
8887f75b70 | ||
|
|
9436a838c0 | ||
|
|
ef120fa36f | ||
|
|
8c6385e2c5 | ||
|
|
0bd85d9905 | ||
|
|
ce0dab7b28 | ||
|
|
ddcc5670ce | ||
|
|
86afa184e2 | ||
|
|
77f341f139 | ||
|
|
918b5d99c2 | ||
|
|
7a098d6eff | ||
|
|
71f81283f5 | ||
|
|
058c7c3c33 | ||
|
|
870e33879b | ||
|
|
3ca82bdfc5 | ||
|
|
4721bad286 | ||
|
|
f040cf2f07 | ||
|
|
8d50717c90 | ||
|
|
2512ad3c95 | ||
|
|
bc7e007634 | ||
|
|
1f3c87e0c7 | ||
|
|
4bb255e0bb | ||
|
|
73e08faee9 | ||
|
|
02dc7711e4 | ||
|
|
67b4d80e5b | ||
|
|
5168d2bb39 | ||
|
|
57190a75bf | ||
|
|
f10e865895 | ||
|
|
91b4dc412b | ||
|
|
c896fd8df8 | ||
|
|
72cd987284 | ||
|
|
d868d05080 | ||
|
|
9dfb039a69 | ||
|
|
f2920e877b | ||
|
|
ecb0b3f9d7 | ||
|
|
67c128be69 | ||
|
|
1ecbbc2d4b | ||
|
|
82c481b014 | ||
|
|
e7da6d7897 | ||
|
|
e9f1e3038b | ||
|
|
a591096819 | ||
|
|
f1ef60475f | ||
|
|
ce75bb3984 | ||
|
|
d59235e04c | ||
|
|
42f8f7e58f | ||
|
|
f9cbc9ae27 | ||
|
|
a50d6599bf | ||
|
|
cc578b496e | ||
|
|
662944d246 | ||
|
|
42810df4a5 | ||
|
|
c3a058d2e1 | ||
|
|
ce5d835ae5 | ||
|
|
60dd561729 | ||
|
|
08c9f9ad7d | ||
|
|
d47907906d | ||
|
|
935f0f6e05 | ||
|
|
ac0b21d574 | ||
|
|
9b5d05369f | ||
|
|
489f9f5e59 | ||
|
|
e62f7c23c9 | ||
|
|
91ed7d49b5 | ||
|
|
c9e9abd811 | ||
|
|
b1e5023f62 | ||
|
|
604690b3f5 | ||
|
|
13ada6ecc6 | ||
|
|
b897eb913e | ||
|
|
c25602a650 | ||
|
|
2defc9af3f | ||
|
|
446525389b | ||
|
|
756b30d04f | ||
|
|
51ec842815 | ||
|
|
c38822849e | ||
|
|
3c69201f67 | ||
|
|
ed9d701406 | ||
|
|
e70c5aa2e9 | ||
|
|
0c4589b257 | ||
|
|
84d08392fb | ||
|
|
8ff117308d | ||
|
|
b6c703adbc | ||
|
|
22e6934de5 | ||
|
|
1b8a1d69ac | ||
|
|
b6ae83937b | ||
|
|
7115556663 | ||
|
|
cb3296661e | ||
|
|
6dd20a6df9 | ||
|
|
71c1d0e59a | ||
|
|
2b275e1ff7 | ||
|
|
a7fcae1033 | ||
|
|
19bd189b33 | ||
|
|
2d5089c047 | ||
|
|
be7469bd54 | ||
|
|
146d8daa6e | ||
|
|
f3928d9e09 | ||
|
|
d4090d459d | ||
|
|
7dd7554c08 | ||
|
|
9de9a1d97d | ||
|
|
04ee366fbe | ||
|
|
c8784150fc | ||
|
|
7b7bccb37a | ||
|
|
84e2636bca | ||
|
|
dc73613b56 | ||
|
|
fd8868ef4d | ||
|
|
127df0b8e0 | ||
|
|
c2989df902 | ||
|
|
3f9ee99b69 | ||
|
|
7204c4e804 | ||
|
|
f1131cf8e7 | ||
|
|
5b9a8beb07 | ||
|
|
9e18e35c66 | ||
|
|
9db3dfa955 | ||
|
|
4a3e56d300 | ||
|
|
3bb4125c50 | ||
|
|
ce58883618 | ||
|
|
e7d3c60bac | ||
|
|
08e90139ad | ||
|
|
0d4dc34453 | ||
|
|
fe142c4626 | ||
|
|
d8dfa6017d | ||
|
|
b7a5d4296b | ||
|
|
85d4c1fc24 | ||
|
|
66489d79be | ||
|
|
30a66a26c6 | ||
|
|
fbc3081e68 | ||
|
|
7a66c94907 | ||
|
|
4e38bc5769 | ||
|
|
0dc428dbd6 | ||
|
|
6ac7fc94ea | ||
|
|
af28ed1783 | ||
|
|
00daf084f2 | ||
|
|
5518848e28 | ||
|
|
6ded856b2f | ||
|
|
d302f5132e | ||
|
|
012e29ee3a | ||
|
|
dbe9579d7f | ||
|
|
c8d0221d9b | ||
|
|
885c663d93 |
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: mastodon
|
||||
open_collective: # Replace with a single Open Collective username e.g., user1
|
||||
ko_fi: # Replace with a single Ko-fi username e.g., user1
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username e.g., user1
|
||||
issuehunt: # Replace with a single IssueHunt username e.g., user1
|
||||
otechie: # Replace with a single Otechie username e.g., user1
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
اِنضم إلَى مُجتَمع وأنشئ مِلَفَّكَ التَّعريفِيّ. ابحث عن أشخاص رائعين، تابعهم واقرأ منشوراتهم في خطٍّ زمني خالٍ من الإعلانات. عبِّر عَن نَفسِكَ باِستخدام رُموزٍ تَعبيرِيَّةٍ مُخصَّصَة، أو صُوَر، أو صُوَرٍ مُتحَرِّكَة، أو مَقاطِعٍ مَرئِّيَة أو مَقاطِعٍ صَوتِيَّةٍ فِي مَنشوراتٍ ذَاتُ خَمسِمائَة حَرف. رُدّ على سَلاسِلِ المَنشوراتِ، وأعِد تَدوينَ مَنشُوراتِ أيِّ شَخصٍ لِمُشارَكَةِ الأُمُورِ الرَّائِعَة. اِبحَث عَن حِساباتٍ جَديدَةٍ لِمُتابَعَتِها، وَعَن وُسُومٍ شَائِعَةٍ لِتَوسيعِ شَبَكَتِك.
|
||||
|
||||
ماستودون مبني بتركيز على الأمان والخصوصيَّة. حدِّد ما إذا أردتَ مُشارَكَةَ مَنشُوراتِكَ مَعَ مُتابِعيك، أو الأشخاصِ الَّذينَ أشَرتَ إليهِم فَقَط أو العالَمَ بأسرِه. تتيح لك تحذيرات المحتوى إخفاء المنشورات التي تحتوي على مواد حساسة أو محفِّزَة حتى تكون مستعد للتفاعل مع محتواها. لكل مجتمع إرشاداته الخاصة ومشرفيه الخاصين للحفاظ على أمان أعضائه، كما تُساعد أدوات الحظر والإبلاغ القوية في منع إساءة الاستخدام.
|
||||
ماستدون مبني بتركيزٍ على الأمان والخصوصيَّة. حدِّد ما إذا أردتَ مُشارَكَةَ مَنشُوراتِكَ مَعَ مُتابِعيك، أو الأشخاصِ الَّذينَ أشَرتَ إليهِم فَقَط أو العالَمَ بأسرِه. تتيح لك تحذيرات المحتوى إخفاء المنشورات التي تحتوي على مواد حساسة أو محفِّزَة حتى تكون مستعد للتفاعل مع محتواها. لكل مجتمع إرشاداته الخاصة ومشرفيه الخاصين للحفاظ على أمان أعضائه، كما تُساعد أدوات الحظر والإبلاغ القوية في منع إساءة الاستخدام.
|
||||
|
||||
مَزيدٌ مِنَ المَزايَا:
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon er det største decentraliserede sociale netværk på internettet. I stedet for et enkelt website, er det et netværk af millioner af brugere i uafhængige fællesskaber som alle sammen kan interagere med hinanden. Uanset hvad du interesserer dig for, kan du møde engagerede mennesker som skriver om det på Mastodon!
|
||||
Mastodon er det største decentraliserede sociale netværk på internet. Frem for ét enkelt website, er det i stedet et netværk af millioner af brugere i uafhængige fællesskaber, som alle kan interagere med hinanden. Uanset interessesfære, kan man møde engagerede personer, som skriver herom på Mastodon!
|
||||
|
||||
Find et fællesskab og opret din profil. Find og følg fascinerende mennesker og læs deres indlæg i en reklamefri, kronologisk tidslinje. Udtryk dig selv med emojis, billeder, GIFs, videoer og lyd i indlæg på op til 500 tegn. Svar på tråde og del alt det gode ved at booste indlæg fra andre. Find nye brugere at følge og aktuelle hashtags så dit netværk udvides.
|
||||
Find et fællesskab og opret din profil. Find og følg fascinerende folk og læs deres indlæg i en reklamefri, kronologisk tidslinje. Udtryk dig selv med tilpassede emojis, billeder, GIF'er, videoer og lyd i 500-tegns indlæg. Svar på tråde og genpost indlæg fra enhver for dele alt det gode. Find nye konti at følge, og populære hashtags, for at udvide dit netværk.
|
||||
|
||||
Mastodon er bygget med fokus på privatliv og sikkerhed. Beslut om dine indlæg skal deles med dine følgere, bare dem du nævner eller hele verden. Indholdsadvarsler giver dig mulighed for at gemme indlæg med sensitivt eller triggende indhold indtil du er klar til at læse dem. Hvert fællesskab har sine egne regler og moderatorer som holder øje og sikrer medlemmerne mod spam og trolde. De har robuste blokerings- og rapporteringsredskaber til deres rådighed.
|
||||
Mastodon er bygget med fokus på fortrolighed og sikkerhed. Afgør, hvorvidt dine indlæg skal deles med Følgere, blot dem du nævner eller hele verden. Indholdsadvarsler muliggør at skjule indlæg med sensitivt eller udløsende indhold, indtil du er klar til at læse dem. Hvert fællesskab har deres egne retningslinjer og moderatorer til at holde deres medlemmer sikre, og robuste blokerings- og anmeldelsesværktøjer hjælper med at forhindre misbrug.
|
||||
|
||||
Flere funktioner:
|
||||
|
||||
• Mørk Mode: Læs indlæg i lys, mørk eller ægte sort tilstand
|
||||
• Afstemninger: Spørg tilhængere om deres mening og stemme
|
||||
• Udforsk: Populære hashtags og konti er et tryk væk
|
||||
• Notifikationer: Få besked om nye følgere, svar og boosts
|
||||
• Deling: Send direkte til Mastodon fra en hvilken som app
|
||||
• Nuttethedsfaktor: Vores maskot er en yndig elefant, og du vil se dem dukke op fra tid til anden
|
||||
• Mørk tilstand: Læs indlæg i lys, mørk eller ægte sort tilstand
|
||||
• Afstemninger: Spørg Følgere om deres mening og stemme
|
||||
• Udforsk: Populære hashtags og konti er ét tryk væk
|
||||
• Notifikationer: Få besked om nye Følgere, svar og genpostninger
|
||||
• Deling: Post direkte til Mastodon fra enhver apps delingsfunktion
|
||||
• Nuttethed: Vores maskot er en yndig elefant, som du vil se dukke op fra tid til anden
|
||||
|
||||
Mastodon er en registreret nonprofit og udvikling understøttes direkte af dine donationer. Der er ingen reklame, ingen indtægtsgenerering og ingen risikovillig kapital, og sådan forbliver det.
|
||||
Mastodon er en registreret nonprofit, hvis udvikling direkte understøttes af dine donationer. Der er ingen annoncering, ingen indtægtsgenerering og ingen risikovillig kapital, og intentionen er, at det forbliver sådan.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 596 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 776 KiB After Width: | Height: | Size: 748 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 790 KiB After Width: | Height: | Size: 722 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -1,6 +1,6 @@
|
||||
Mastodon Interneteko sare sozial deszentralizatu handiena da. Webgune bakar bat izan ordez, beren artean elkarreragin dezaketen komunitate independenteetako milioika erabiltzailek osatutako sarea da. Zure interesak direnak direla ere, jende interesgarria aurkituko duzu Mastodonen!
|
||||
|
||||
Batu komunitate batera eta sortu zure profila. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Adierazi nahi duzuna 500 karaktereko bidalketetan emoji pertsonalizatuak, irudiak, GIFak, bideoak eta audioak erabiliz. Erantzun edozeinen hariak eta eman bultzada bidalketei edukiak partekatzeko. Bilatu jarraitzeko kontu berriak eta traolen joerak zure sarea zabaltzeko.
|
||||
Batu komunitate batera eta sortu zure profila. Bilatu eta jarraitu jende zoragarria eta irakurri beren bidalketak, publizitaterik gabeko denbora-lerro kronologikoan. Adierazi nahi duzuna 500 karaktereko bidalketetan emoji pertsonalizatuak, irudiak, GIFak, bideoak eta audioak erabiliz. Erantzun edozeinen hariak eta eman bultzada bidalketei edukiak partekatzeko. Bilatu jarraitzeko kontu berriak eta traolen joerak zure sarea zabaltzeko.
|
||||
|
||||
Mastodon pribatutasunean eta segurtasunean arreta jarriz eraikia dago. Erabaki zure bidalketak norekin partekatu: zure jarraitzaileekin, aipatzen dituzunekin edo mundu osoarekin. Edukiaren abisuek aukera ematen dute eduki sentibera edo zuregan eragina izan dezaketen bidalketak zuk erabaki arte ezkutatzeko. Komunitate bakoitzak bere gidalerro eta moderatzaileak ditu, bertako kideak seguru mantentzeko. Baita blokeatzeko eta salatzeko tresna sendoak ere abusuak galarazteko.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata intohimoisia ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
|
||||
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata samanmielisiä ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
|
||||
|
||||
Liity yhteisöön ja luo itsellesi tili. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja trendaavia hashtageja laajentaaksesi verkostoasi.
|
||||
Liity yhteisöön ja luo itsellesi tili. Löydä ja seuraa kiehtovia ihmisiä ja lue heidän julkaisunsa ilman mainoksia, kronologisella aikajanalla. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja suosittuja aihetunnisteita laajentaaksesi verkostoasi.
|
||||
|
||||
Mastodon on rakennettu keskittyen yksityisyyteen ja turvallisuuteen. Päätä, jaetaanko julkaisusi seuraajille, vain mainitsemillesi ihmisille vai koko maailmalle. Sisältövaroitusten avulla, voit piilottaa julkaisut, jotka sisältävät arkaluontoista tai laukaisevaa materiaalia, kunnes olet valmis käsittelemään niitä. Jokaisella yhteisöllä on omat ohjeistonsa ja valvojansa, jotka pitävät jäsenensä turvassa, ja tehokkaat esto- ja ilmiantotyökalut auttavat torjumaan väärinkäytöksiä.
|
||||
|
||||
@@ -8,8 +8,8 @@ Lisää ominaisuuksia:
|
||||
|
||||
• Tumma tila: Lue julkaisut vaaleassa, tummassa tai mustan tummassa tilassa
|
||||
• Kyselyt: Kysy seuraajilta heidän mielipidettään ja laske äänet
|
||||
• Tutustu: Trendaavat hashtagit ja tilit ovat vain napsautuksen päässä
|
||||
• Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja edelleen jaoista
|
||||
• Tutustu: Suositut aihetunnisteet ja tilit ovat vain napsautuksen päässä
|
||||
• Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja tehostuksista
|
||||
• Jakaminen: Julkaise suoraan Mastodoniin minkä tahansa sovelluksen jakovalikon kautta
|
||||
• Suloisuus: Maskottimme on ihastuttava mastodontti ja näet sen ajoittain
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon इंटरनेट का सबसे बड़ा डिसेंट्रलाइज़्ड सोशल नेटवर्क है। एक सिंगल वेबसाइट के जगह, ये हज़ारों आज़ाद ग्रुपों के लाखों यूज़रों का एक नेटवर्क है जो एक दूसरे से आसानी से बात करते है। चाहे आपकी जो भी दिलचस्पी हो, आपको उसके ऊपर पोस्ट करनेवाले लोग Mastodon पे ज़रूर मिलेंगे!
|
||||
|
||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
कोई ग्रुप जॉइन करें और अपना प्रोफाइल बनाएं। दिलचस्प लोगों को ढूंढ़ें और फॉलो करें और उनके पोस्ट पढ़ें बिना किसी ऐड के। Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
एक्स्ट्रा फीचर:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• डार्क मोड: पोस्ट लाइट, डार्क, या प्योर ब्लैक मोड में पढ़ें
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• क्यूटपन: हमारा मैस्कॉट एक प्यारा हाथी है, और आप उसे समय-समय पे देखेंगे
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
डिसेंट्रलाइज़्ड सोशल नेटवर्क
|
||||
@@ -1 +1 @@
|
||||
Decentralizált szociális hálózat
|
||||
Decentralizált közösségi hálózat
|
||||
@@ -1,16 +1,16 @@
|
||||
Մաստոդոնը համացանցի ամենամեծ ապակենտրոնացված սոցցանցն է։ Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Մաստոդոնը համացանցի ամենամեծ ապակենտրոնացված սոցցանցն է։ Այն մի կայք չէ, այլ իրար հետ կապակցված անկախ համայնքների միլիոնավոր օգտատերերից կազմված ցանց։ No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Միացեք համայնքին և ստեղծեք հաշիվ։ Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Զգայուն կամ հրահրող թեմաներով գրառումները կարելի է թաքցնել նախազգուշացումներով։ Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
Ավելին՝
|
||||
|
||||
• Մուգ տարբերակ՝ կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
|
||||
• Հարցումներ՝ իմացեք ձեր հետևորդների կարծիքը և հաշվեք քվեները
|
||||
• Հարցումներ՝ իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Մաստոդոնը գրանցված շահույթ չհետապնդող կազմակերպություն է, և աջակցվում է ձեր նվիրաբերություններով։ There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
Ապակենտրոնացված սոցիալական ցանց
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon adalah jejaring sosial terdesentralisasi terbesar di internet. Daripada sebuah satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat berinteraksi antar sesama, tanpa masalah. Tanpa memedulikan apa yang Anda minat, Anda dapat bertemu orang-orang yang mengirimkan apa yang mereka minat di Mastodon!
|
||||
Mastodon dalah jaringan sosial terdesentralisasi terbesar di internet. Bukan hanya satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat saling interaksi antar sesama, tanpa batasan. Apapun yang kamu minati, kamu dapat bertemu orang-orang baru yang mengirimkan apa yang mereka minati di Mastodon!
|
||||
|
||||
Bergabung sebuah komunitas dan buat profil Anda. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam lini masa yang kronologis dan bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
||||
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam linimasa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
||||
|
||||
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodonは、インターネット上で最大の分散型ソーシャルネットワークです。 Mastodonは単一のウェブサイトではなく、それぞれ独立したコミュニティに参加している何百万人ものユーザーによって構成されたネットワークなのです。ユーザーたちはその中で、誰もがお互いとシームレスにやり取りできます。 あなたの興味関心がどんな分野にあっても、きっとMastodonのどこかで同じ情熱を投稿している仲間がいることでしょう。
|
||||
Mastodon は、インターネット上で最大の分散型ソーシャルネットワークです。 Mastodon は単一のウェブサイトではなく、それぞれ独立したコミュニティに参加している何百万人ものユーザーによって構成されたネットワークなのです。ユーザーたちはその中で、誰もがお互いとシームレスにやり取りできます。 あなたの興味関心がどんな分野にあっても、きっと Mastodon のどこかで同じ情熱を投稿している仲間がいることでしょう。
|
||||
|
||||
まずはコミュニティに参加して、自分のプロフィールを作成しましょう。 魅力的なユーザーを見つけたら、フォローしてタイムラインで投稿を見てみましょう。タイムラインに広告は出てきません。順番も時系列順です。 そして、500文字まで使える投稿で自分を表現してみましょう。カスタム絵文字や画像、GIF、動画、音声も使用できます。 スレッドに返事したり、他の誰かの面白い投稿をブーストして共有したりすることもできます。 興味のあるアカウントとホットなハッシュタグをどんどん見つけて、あなた自身のネットワークを広げていきましょう。
|
||||
まずはコミュニティに参加して、自分のプロフィールを作成しましょう。 魅力的なユーザーを見つけたら、フォローしてタイムラインで投稿を見てみましょう。タイムラインに広告は出てきません。順番も時系列順です。 そして、500 文字まで使える投稿で自分を表現してみましょう。カスタム絵文字や画像、GIF、動画、音声も使用できます。 スレッドに返事したり、他の誰かの面白い投稿をブーストして共有したりすることもできます。 興味のあるアカウントとホットなハッシュタグをどんどん見つけて、あなた自身のネットワークを広げていきましょう。
|
||||
|
||||
Mastondonはプライバシーと安全性を重視しています。 自分の投稿をフォロワーだけに限定公開にするのか、メンションした特定のユーザーだけに共有するのか、全世界に放流するのか、自由に決められます。 また、入力中の投稿について「ちょっとセンシティブな内容だな」と思ったら、閲覧注意機能で内容を伏せることで、見たくない人に配慮した投稿が作成できます。 そして、各コミュニティにはそれぞれのガイドラインと管理者・モデレーターが存在し、コミュニティメンバーの安全を守っています。強力なブロック・通報機能も、不正利用の防止をお手伝いします。
|
||||
Mastondon はプライバシーと安全性を重視しています。 自分の投稿をフォロワーだけに限定公開にするのか、メンションした特定のユーザーだけに共有するのか、全世界に放流するのか、自由に決められます。 また、入力中の投稿について「ちょっとセンシティブな内容だな」と思ったら、閲覧注意機能で内容を伏せることで、見たくない人に配慮した投稿が作成できます。 そして、各コミュニティにはそれぞれのガイドラインと管理者・モデレーターが存在し、コミュニティメンバーの安全を守っています。強力なブロック・通報機能も、不正利用の防止をお手伝いします。
|
||||
|
||||
その他の機能:
|
||||
その他の機能:
|
||||
|
||||
• ダークモード対応:ライトモードだけでなく、ダークモードや「真っ黒」モードで投稿を閲覧
|
||||
• 投票機能:フォロワーたちの意見を投票形式で集計
|
||||
• 探索:話題のハッシュタグやアカウントに1タップでアクセス
|
||||
• 通知設定:新しいフォローやリプライ、ブーストがあった時に通知
|
||||
• 共有:どのアプリからでも、「共有」メニューを通じてMastodonへ直接投稿
|
||||
• 癒し:Mastodonが誇る象のマスコット(かわいい)が、画面にお邪魔したり、しなかったり
|
||||
• ダークモード: ライトモードだけでなく、ダークモードや「真っ黒」モードで投稿を閲覧
|
||||
• アンケート: フォロワーたちの意見を投票形式で集計
|
||||
• 探索: 話題のハッシュタグやアカウントに 1 タップでアクセス
|
||||
• 通知: 新しいフォローやリプライ、ブーストがあった時に通知
|
||||
• 共有: どのアプリからでも「共有」メニューを通じて Mastodon へ直接投稿
|
||||
• 癒し: Mastodon が誇る象のマスコット(かわいい)が、画面にお邪魔したり、しなかったり
|
||||
|
||||
Mastodonは公認の非営利アプリです。開発は全てユーザーの寄付から成り立っています。 広告なし、アフィリエイトなし、第三者組織による出資なし。今でも、そしてこれからもそんなアプリであり続けるために、我々は日々努力し続けています。
|
||||
Mastodon は公認の非営利アプリです。開発は全てユーザーの寄付から成り立っています。 広告なし、アフィリエイトなし、第三者組織による出資なし。今でも、そしてこれからもそんなアプリであり続けるために、我々は日々努力し続けています。
|
||||
|
||||
16
fastlane/metadata/android/ka-GE/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
1
fastlane/metadata/android/ka-GE/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralized social network
|
||||
1
fastlane/metadata/android/ka-GE/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
16
fastlane/metadata/android/lt-LT/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon – didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
|
||||
|
||||
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
|
||||
|
||||
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
|
||||
|
||||
Daugiau funkcijų:
|
||||
|
||||
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
|
||||
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
|
||||
• Naršyti: tendencingos saitažodžiai ir paskyros – vos nuo vieno prisilietimo
|
||||
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
|
||||
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
|
||||
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
|
||||
|
||||
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.
|
||||
1
fastlane/metadata/android/lt-LT/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralizuotas socialinis tinklas
|
||||
1
fastlane/metadata/android/lt-LT/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,4 +1,4 @@
|
||||
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon! ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
|
||||
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
|
||||
|
||||
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในเส้นเวลาที่ไม่มีโฆษณาและเรียงตามลำดับเวลา แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
|
||||
|
||||
@@ -13,4 +13,4 @@ Mastodon สร้างขึ้นโดยเน้นความเป็
|
||||
• การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
|
||||
• ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
|
||||
|
||||
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
|
||||
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbiri ile kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğun önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsin!
|
||||
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbiri ile kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğunuz önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsiniz!
|
||||
|
||||
Bir topluluğa katıl ve profilini oluştur. Olağanüstü kişileri bul ve takip et; gönderilerini reklamsız ve kronolojik bir akışta oku. Gönderilerinde 500 karakter sınırlamasıyla kendini emojiler, görseller, GIFler, videolar ve sesler ile ifade et. Harika içerikler paylaşmak için başlıklara yanıt yaz, insanların gönderilerini yeniden paylaş. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bul.
|
||||
Bir topluluğa katılın ve profilinizi oluşturun. Olağanüstü kişileri bulun ve takip edin; gönderilerini reklamsız ve kronolojik bir zaman çizelgesinde okuyun. Gönderilerinizde şimdilik 500 karakter sınırlamasıyla kendinizi emojiler, görseller, GIFler, videolar ve sesler ile ifade edin. Harika içerikler paylaşmak için başlıklara yanıt yazın, insanların gönderilerini yineleyin. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bulun.
|
||||
|
||||
Mastodon gizlilik ve güvenlik odaklı yapılmıştır. Her postunuz için takipçilerinizle mi, bahsettiğiniz kişilerle mi ya da tüm dünyayla mı paylaşılacağına karar verin. Gönderi uyarıları, hassas ve tetikleyici olabilecek içerikleri kişi görmeyi hazır olana kadar gizler. Her topluluk, üyelerini güvende tutmak için kendi kurallarına ve moderatörlerine; istismarı önlemek için de güçlü engelleme ve bildirme araçlarına sahiptir.
|
||||
Mastodon, mahremiyet ve güvenliğe odaklanılarak inşa edilmiştir. Gönderilerinizi takipçilerinizle mi, sadece bahsettiğiniz kişilerle mi yoksa tüm dünyayla mı paylaşılacağına karar verin. İçerik uyarıları, hassas veya tetikleyici materyal içeren gönderileri, siz onlarla etkileşim kurmaya hazır olana kadar gizlemenize olanak tanır. Her topluluk, üyelerini güvende tutmak için kendi kurallarına ve moderatörlerine; istismarı önlemek için de güçlü engelleme ve bildirme araçlarına sahiptir.
|
||||
|
||||
Diğer özellikler:
|
||||
|
||||
• Koyu Mod: Gönderileri aydınlık, karanlık ya da gerçek karanlık modunda okuyabilirsin
|
||||
• Anketler: Takipçilerine görüşlerini sor ve oylarını gör
|
||||
• Keşfet: Trend hashtagler ve hesaplar bir tık uzağında
|
||||
• Bildirimler: Yeni takipçilerden, yanıtlardan ve yeniden paylaşımlardan haberin olsun
|
||||
• Paylaşım: Doğrudan Mastodon'a herhangi bir tipte gönderi paylaş
|
||||
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksin
|
||||
• Karanlık Mod: Gönderileri aydınlık, karanlık ya da gerçek karanlık modunda okuyabilirsin
|
||||
• Anketler: Takipçilere fikirlerini sorun ve oylarını görün
|
||||
• Keşfet: Öne çıkan etiketlerler ve hesaplar bir tık uzağınızda
|
||||
• Bildirimler: Yeni takipçilerden, yanıtlardan ve yeniden paylaşımlardan haberiniz olsun
|
||||
• Paylaşım: Doğrudan Mastodon'a herhangi bir türde gönderi paylaş
|
||||
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksiniz
|
||||
|
||||
Mastodon kar amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam, para kazanma amacı, risk sermayesi yoktur ve bunu böyle tutmayı planlıyoruz.
|
||||
Mastodon kar amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam yok, para kazanma güdüsü yok, risk sermayesi yok ve bu şekilde devam etmeyi planlıyoruz.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Merkezsizleştirilmiş sosyal ağ
|
||||
Merkezi olmayan sosyal ağ
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Замість одного сайту це мережа мільйонів користувачів у незалежних спільнотах, які можуть взаємодіяти один з одним. Незалежно від того, чим ви займаєтеся, ви можете зустріти захоплених людей, які пишуть про це на Mastodon!
|
||||
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Це не один сайт, а мережа з мільйонів користувачів у незалежних спільнотах, які взаємодіють одна з одною. Незалежно від того, чим ви займаєтеся, ви можете зустріти людей зі спільними інтересами, які пишуть про це на Mastodon!
|
||||
|
||||
Приєднуйтесь до спільноти і створіть свій профіль. Знайдіть і підпишіться на цікавих людей і читайте пости у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо з 500-символьними постами. Відповідайте на теми й робіть репости постів від будь-кого, щоб ділитися гарними матеріалами. Знаходьте нові облікові записи, щоб підписатися і популярні хештеги для розширення вашої мережі.
|
||||
Приєднуйтесь до спільноти та створіть свій профіль. Знайдіть і підпишіться на цікавих людей та читайте дописи у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо у дописах, обмеженими 500 символами. Відповідайте на теми та поширюйте дописи від будь-кого, щоб ділитися цікавим. Знаходьте нових користувачів, щоб підписатися та слідкуйте за популярними хештегами, щоб розширити свій кругозір.
|
||||
|
||||
Mastodon будується з акцентом на конфіденційність та безпеці. Вирішіть, чи будуть ваші пости тільки для підписників, або для людей, яких ви згадали, чи для цілого світу. Попередження щодо вмісту дозволяють приховати публікації, що містять конфіденційний або провокаційний матеріал, доки ви не будете готові до нього. Кожна спільнота має свої правила і модераторів, щоб залишити учасників в безпеці, а також надійне блокування та інструменти для скарг, щоб запобігти зловживання.
|
||||
Mastodon будується з акцентом на конфіденційність та безпеку. Вирішіть, чи будуть ваші дописи доступні лише підписникам, або для людей, яких ви згадали, чи взагалі для всіх. Попередження про вміст дозволяють приховати дописи, що містять чутливі або провокаційні матеріали, доки ви не будете готові взаємодіяти з ними. Кожна спільнота має власні правила та модераторів, які забезпечують безпеку учасників, а надійні інструменти блокування та система скарг допомагають запобігти зловживанням.
|
||||
|
||||
Більше можливостей:
|
||||
|
||||
• Темна Тема: Читайте у світлій, темній, або справжній чорній темі
|
||||
• Опитування: запитуйте думку підписникіна та підраховуйте голоси
|
||||
Досліджуйте: Популярні Хештеги й Користувачі за одним дотиком
|
||||
• Сповіщення: отримуйте сповіщення про нових підписників, відповіді та репости
|
||||
Діліться: Публікуйте безпосередньо в Mastodon з будь-якого меню "поділитися" в будь-якому додатку
|
||||
• Привабливість: Нашим талісманом є чарівний слон, і ви побачите, як він з'являється час від часу
|
||||
• Темна тема: Читайте дописи у світлому, темному або повністю чорному режимі
|
||||
• Опитування: Запитайте у підписників їхню думку та підраховуйте голоси
|
||||
• Досліджуйте: Популярні хештеги та користувачі на відстані одного дотику
|
||||
• Сповіщення: Отримуйте сповіщення про нові підписки, відповіді та поширення
|
||||
• Діліться: Публікуйте безпосередньо в Mastodon через будь-яке меню "поділитися" у будь-якому додатку
|
||||
• Миле: Наш талісман - чарівне слоненя, яке час від часу ви помічатимете
|
||||
|
||||
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу, і плануємо так тримати.
|
||||
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу. І ми плануємо, що так буде й надалі.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。
|
||||
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
|
||||
|
||||
加入一个社区节点并创建你的账户。 Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. 用自定义表情、图像、GIF、视频和音频在 500 个字符的帖子里写下你的想法。 回复及转发其他人的帖子来共同分享美好的事物。 寻找新的账户以及热门的话题来拓展你的网络。
|
||||
加入一个社区节点并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频,在最多 500 字的帖文中表达自我。 通过回复或转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
|
||||
|
||||
Mastodon 以隐私和安全为首要目标。 自由选择你的帖子是分享给关注者,或是你提到的人,亦或是整个世界。 内容警告允许让你隐藏可能剧透的内容,让其他人在交互之前做好充分准备。 每个社区都有自己的规则和管理员,让其用户保持安全。通过强有力的屏蔽和举报工具防止滥用。
|
||||
Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分享分享对象,可以是你的关注者、你提到的人或是整个世界。 在你做好充足的互动准备之前,内容警告可以隐藏包含敏感或刺激内容的帖文。 每个社区都有自己的规则和管理员来保证其成员安全,同时还有强力的屏蔽和举报工具来避免滥用。
|
||||
|
||||
更多功能:
|
||||
|
||||
• 暗色模式:以浅色、深色、纯黑模式下进行阅读
|
||||
• 暗色模式:在浅色、深色或纯黑模式下阅读帖文
|
||||
• 投票:询问关注者的意见并统计他们的投票
|
||||
• 探索:热门的话题以及账号
|
||||
• 通知:获得新关注、回复和转发的通知
|
||||
• 探索:热门的话题标签及账号只有一触之遥
|
||||
• 通知:获取关注、回复和转发相关的通知提醒
|
||||
• 分享:从其他应用中的分享菜单中直接发布到 Mastodon
|
||||
• 吉祥物:你会不时地看到我们可爱的长毛象
|
||||
|
||||
Mastodon 是一个注册的非营利开发项目,直接由您的捐赠支持。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。
|
||||
Mastodon 是一个直接由用户捐赠支持、已注册非营利开发项目。 它没有广告、没有商业化,也没有风险资本,并且我们也计划保持这种方式。
|
||||
|
||||
279
mastodon/.editorconfig
Normal file
@@ -0,0 +1,279 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
insert_final_newline = false
|
||||
max_line_length = 300
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = none
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
ij_java_align_consecutive_variable_declarations = false
|
||||
ij_java_align_group_field_declarations = false
|
||||
ij_java_align_multiline_annotation_parameters = false
|
||||
ij_java_align_multiline_array_initializer_expression = false
|
||||
ij_java_align_multiline_assignment = false
|
||||
ij_java_align_multiline_binary_operation = false
|
||||
ij_java_align_multiline_chained_methods = false
|
||||
ij_java_align_multiline_extends_list = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_align_multiline_method_parentheses = false
|
||||
ij_java_align_multiline_parameters = true
|
||||
ij_java_align_multiline_parameters_in_calls = false
|
||||
ij_java_align_multiline_parenthesized_expression = false
|
||||
ij_java_align_multiline_records = true
|
||||
ij_java_align_multiline_resources = true
|
||||
ij_java_align_multiline_ternary_operation = false
|
||||
ij_java_align_multiline_text_blocks = false
|
||||
ij_java_align_multiline_throws_list = false
|
||||
ij_java_align_subsequent_simple_methods = false
|
||||
ij_java_align_throws_keyword = false
|
||||
ij_java_align_types_in_multi_catch = true
|
||||
ij_java_annotation_parameter_wrap = off
|
||||
ij_java_array_initializer_new_line_after_left_brace = false
|
||||
ij_java_array_initializer_right_brace_on_new_line = false
|
||||
ij_java_array_initializer_wrap = off
|
||||
ij_java_assert_statement_colon_on_next_line = false
|
||||
ij_java_assert_statement_wrap = off
|
||||
ij_java_assignment_wrap = off
|
||||
ij_java_binary_operation_sign_on_next_line = false
|
||||
ij_java_binary_operation_wrap = off
|
||||
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||
ij_java_blank_lines_after_class_header = 0
|
||||
ij_java_blank_lines_after_imports = 1
|
||||
ij_java_blank_lines_after_package = 1
|
||||
ij_java_blank_lines_around_class = 1
|
||||
ij_java_blank_lines_around_field = 0
|
||||
ij_java_blank_lines_around_field_in_interface = 0
|
||||
ij_java_blank_lines_around_initializer = 1
|
||||
ij_java_blank_lines_around_method = 1
|
||||
ij_java_blank_lines_around_method_in_interface = 1
|
||||
ij_java_blank_lines_before_class_end = 0
|
||||
ij_java_blank_lines_before_imports = 1
|
||||
ij_java_blank_lines_before_method_body = 0
|
||||
ij_java_blank_lines_before_package = 0
|
||||
ij_java_block_brace_style = end_of_line
|
||||
ij_java_block_comment_add_space = false
|
||||
ij_java_block_comment_at_first_column = true
|
||||
ij_java_builder_methods = none
|
||||
ij_java_call_parameters_new_line_after_left_paren = false
|
||||
ij_java_call_parameters_right_paren_on_new_line = false
|
||||
ij_java_call_parameters_wrap = off
|
||||
ij_java_case_statement_on_separate_line = true
|
||||
ij_java_catch_on_new_line = false
|
||||
ij_java_class_annotation_wrap = split_into_lines
|
||||
ij_java_class_brace_style = end_of_line
|
||||
ij_java_class_count_to_use_import_on_demand = 99
|
||||
ij_java_class_names_in_javadoc = 1
|
||||
ij_java_do_not_indent_top_level_class_members = false
|
||||
ij_java_do_not_wrap_after_single_annotation = false
|
||||
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
|
||||
ij_java_do_while_brace_force = never
|
||||
ij_java_doc_add_blank_line_after_description = true
|
||||
ij_java_doc_add_blank_line_after_param_comments = false
|
||||
ij_java_doc_add_blank_line_after_return = false
|
||||
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_do_not_wrap_if_one_line = false
|
||||
ij_java_doc_enable_formatting = true
|
||||
ij_java_doc_enable_leading_asterisks = true
|
||||
ij_java_doc_indent_on_continuation = false
|
||||
ij_java_doc_keep_empty_lines = true
|
||||
ij_java_doc_keep_empty_parameter_tag = true
|
||||
ij_java_doc_keep_empty_return_tag = true
|
||||
ij_java_doc_keep_empty_throws_tag = true
|
||||
ij_java_doc_keep_invalid_tags = true
|
||||
ij_java_doc_param_description_on_new_line = false
|
||||
ij_java_doc_preserve_line_breaks = false
|
||||
ij_java_doc_use_throws_not_exception_tag = true
|
||||
ij_java_else_on_new_line = false
|
||||
ij_java_enum_constants_wrap = off
|
||||
ij_java_extends_keyword_wrap = off
|
||||
ij_java_extends_list_wrap = off
|
||||
ij_java_field_annotation_wrap = split_into_lines
|
||||
ij_java_finally_on_new_line = false
|
||||
ij_java_for_brace_force = never
|
||||
ij_java_for_statement_new_line_after_left_paren = false
|
||||
ij_java_for_statement_right_paren_on_new_line = false
|
||||
ij_java_for_statement_wrap = off
|
||||
ij_java_generate_final_locals = false
|
||||
ij_java_generate_final_parameters = false
|
||||
ij_java_if_brace_force = never
|
||||
ij_java_imports_layout = android.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|,$*,|
|
||||
ij_java_indent_case_from_switch = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_insert_override_annotation = true
|
||||
ij_java_keep_blank_lines_before_right_brace = 2
|
||||
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||
ij_java_keep_blank_lines_in_code = 2
|
||||
ij_java_keep_blank_lines_in_declarations = 2
|
||||
ij_java_keep_builder_methods_indents = false
|
||||
ij_java_keep_control_statement_in_one_line = true
|
||||
ij_java_keep_first_column_comment = true
|
||||
ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
ij_java_label_indent_size = 0
|
||||
ij_java_lambda_brace_style = end_of_line
|
||||
ij_java_layout_static_imports_separately = true
|
||||
ij_java_line_comment_add_space = false
|
||||
ij_java_line_comment_add_space_on_reformat = false
|
||||
ij_java_line_comment_at_first_column = true
|
||||
ij_java_method_annotation_wrap = split_into_lines
|
||||
ij_java_method_brace_style = end_of_line
|
||||
ij_java_method_call_chain_wrap = off
|
||||
ij_java_method_parameters_new_line_after_left_paren = false
|
||||
ij_java_method_parameters_right_paren_on_new_line = false
|
||||
ij_java_method_parameters_wrap = off
|
||||
ij_java_modifier_list_wrap = false
|
||||
ij_java_multi_catch_types_wrap = normal
|
||||
ij_java_names_count_to_use_import_on_demand = 99
|
||||
ij_java_new_line_after_lparen_in_annotation = false
|
||||
ij_java_new_line_after_lparen_in_record_header = false
|
||||
ij_java_parameter_annotation_wrap = off
|
||||
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_java_place_assignment_sign_on_next_line = false
|
||||
ij_java_prefer_longer_names = true
|
||||
ij_java_prefer_parameters_wrap = false
|
||||
ij_java_record_components_wrap = normal
|
||||
ij_java_repeat_synchronized = true
|
||||
ij_java_replace_instanceof_and_cast = false
|
||||
ij_java_replace_null_check = true
|
||||
ij_java_replace_sum_lambda_with_method_ref = true
|
||||
ij_java_resource_list_new_line_after_left_paren = false
|
||||
ij_java_resource_list_right_paren_on_new_line = false
|
||||
ij_java_resource_list_wrap = off
|
||||
ij_java_rparen_on_new_line_in_annotation = false
|
||||
ij_java_rparen_on_new_line_in_record_header = false
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
ij_java_space_after_colon = true
|
||||
ij_java_space_after_comma = true
|
||||
ij_java_space_after_comma_in_type_arguments = true
|
||||
ij_java_space_after_for_semicolon = true
|
||||
ij_java_space_after_quest = true
|
||||
ij_java_space_after_type_cast = true
|
||||
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||
ij_java_space_before_annotation_parameter_list = false
|
||||
ij_java_space_before_array_initializer_left_brace = false
|
||||
ij_java_space_before_catch_keyword = false
|
||||
ij_java_space_before_catch_left_brace = false
|
||||
ij_java_space_before_catch_parentheses = false
|
||||
ij_java_space_before_class_left_brace = false
|
||||
ij_java_space_before_colon = true
|
||||
ij_java_space_before_colon_in_foreach = true
|
||||
ij_java_space_before_comma = false
|
||||
ij_java_space_before_do_left_brace = false
|
||||
ij_java_space_before_else_keyword = false
|
||||
ij_java_space_before_else_left_brace = false
|
||||
ij_java_space_before_finally_keyword = false
|
||||
ij_java_space_before_finally_left_brace = false
|
||||
ij_java_space_before_for_left_brace = false
|
||||
ij_java_space_before_for_parentheses = false
|
||||
ij_java_space_before_for_semicolon = false
|
||||
ij_java_space_before_if_left_brace = false
|
||||
ij_java_space_before_if_parentheses = false
|
||||
ij_java_space_before_method_call_parentheses = false
|
||||
ij_java_space_before_method_left_brace = false
|
||||
ij_java_space_before_method_parentheses = false
|
||||
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||
ij_java_space_before_quest = true
|
||||
ij_java_space_before_switch_left_brace = false
|
||||
ij_java_space_before_switch_parentheses = false
|
||||
ij_java_space_before_synchronized_left_brace = false
|
||||
ij_java_space_before_synchronized_parentheses = false
|
||||
ij_java_space_before_try_left_brace = false
|
||||
ij_java_space_before_try_parentheses = false
|
||||
ij_java_space_before_type_parameter_list = false
|
||||
ij_java_space_before_while_keyword = false
|
||||
ij_java_space_before_while_left_brace = false
|
||||
ij_java_space_before_while_parentheses = false
|
||||
ij_java_space_inside_one_line_enum_braces = false
|
||||
ij_java_space_within_empty_array_initializer_braces = false
|
||||
ij_java_space_within_empty_method_call_parentheses = false
|
||||
ij_java_space_within_empty_method_parentheses = false
|
||||
ij_java_spaces_around_additive_operators = false
|
||||
ij_java_spaces_around_annotation_eq = true
|
||||
ij_java_spaces_around_assignment_operators = false
|
||||
ij_java_spaces_around_bitwise_operators = false
|
||||
ij_java_spaces_around_equality_operators = false
|
||||
ij_java_spaces_around_lambda_arrow = false
|
||||
ij_java_spaces_around_logical_operators = true
|
||||
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||
ij_java_spaces_around_multiplicative_operators = false
|
||||
ij_java_spaces_around_relational_operators = false
|
||||
ij_java_spaces_around_shift_operators = false
|
||||
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||
ij_java_spaces_around_unary_operator = false
|
||||
ij_java_spaces_within_angle_brackets = false
|
||||
ij_java_spaces_within_annotation_parentheses = false
|
||||
ij_java_spaces_within_array_initializer_braces = false
|
||||
ij_java_spaces_within_braces = false
|
||||
ij_java_spaces_within_brackets = false
|
||||
ij_java_spaces_within_cast_parentheses = false
|
||||
ij_java_spaces_within_catch_parentheses = false
|
||||
ij_java_spaces_within_for_parentheses = false
|
||||
ij_java_spaces_within_if_parentheses = false
|
||||
ij_java_spaces_within_method_call_parentheses = false
|
||||
ij_java_spaces_within_method_parentheses = false
|
||||
ij_java_spaces_within_parentheses = false
|
||||
ij_java_spaces_within_record_header = false
|
||||
ij_java_spaces_within_switch_parentheses = false
|
||||
ij_java_spaces_within_synchronized_parentheses = false
|
||||
ij_java_spaces_within_try_parentheses = false
|
||||
ij_java_spaces_within_while_parentheses = false
|
||||
ij_java_special_else_if_treatment = true
|
||||
ij_java_subclass_name_suffix = Impl
|
||||
ij_java_ternary_operation_signs_on_next_line = false
|
||||
ij_java_ternary_operation_wrap = off
|
||||
ij_java_test_name_suffix = Test
|
||||
ij_java_throws_keyword_wrap = off
|
||||
ij_java_throws_list_wrap = off
|
||||
ij_java_use_external_annotations = false
|
||||
ij_java_use_fq_class_names = false
|
||||
ij_java_use_relative_indents = false
|
||||
ij_java_use_single_class_imports = true
|
||||
ij_java_variable_annotation_wrap = off
|
||||
ij_java_visibility = public
|
||||
ij_java_while_brace_force = never
|
||||
ij_java_while_on_new_line = false
|
||||
ij_java_wrap_comments = false
|
||||
ij_java_wrap_first_method_in_call_chain = false
|
||||
ij_java_wrap_long_lines = false
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_continuation_indent_size = 4
|
||||
ij_xml_align_attributes = false
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_add_space = false
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = false
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = true
|
||||
ij_xml_text_wrap = normal
|
||||
ij_xml_use_custom_settings = true
|
||||
@@ -9,10 +9,10 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 60
|
||||
versionName "2.0.0"
|
||||
versionCode 84
|
||||
versionName "2.3.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "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"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -76,7 +76,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.8'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
@@ -3,6 +3,9 @@ package org.joinmastodon.android;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
@@ -13,6 +16,10 @@ public class GlobalUserPreferences{
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPreReplyPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
@@ -36,9 +43,42 @@ public class GlobalUserPreferences{
|
||||
.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 static void resetPreReplySheets(){
|
||||
getPreReplyPrefs().edit().clear().apply();
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
AUTO,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
public enum PreReplySheetType{
|
||||
OLD_POST,
|
||||
NON_MUTUAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.BadParcelableException;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -36,46 +37,15 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity{
|
||||
private static final String TAG="MainActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState==null){
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new SplashFragment());
|
||||
}else{
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.getBooleanExtra("fromNotification", false)){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!intent.hasExtra("notification"))
|
||||
args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
||||
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{
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
restartHomeFragment();
|
||||
}
|
||||
|
||||
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
|
||||
@@ -132,11 +102,11 @@ public class MainActivity extends FragmentStackActivity{
|
||||
session=AccountSessionManager.get(accountID);
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
|
||||
}
|
||||
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
||||
new GetSearchResults(q, null, true)
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
|
||||
new GetSearchResults(q, type, true, null, 0, 0)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
@@ -200,4 +170,47 @@ public class MainActivity extends FragmentStackActivity{
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
public void restartHomeFragment(){
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new SplashFragment());
|
||||
}else{
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.getBooleanExtra("fromNotification", false)){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!intent.hasExtra("notification"))
|
||||
args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
||||
// 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{
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
||||
.map(type->{
|
||||
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);
|
||||
return channel;
|
||||
})
|
||||
@@ -147,6 +149,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
.setShowWhen(true)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setAutoCancel(true)
|
||||
.setLights(context.getColor(R.color.primary_700), 500, 1000)
|
||||
.setColor(context.getColor(R.color.primary_700));
|
||||
if(avatar!=null){
|
||||
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
|
||||
|
||||
@@ -9,20 +9,31 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -42,6 +53,7 @@ public class CacheController{
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private List<FollowList> lists;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@@ -300,6 +312,99 @@ public class CacheController{
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public void reloadLists(Callback<List<FollowList>> callback){
|
||||
new GetLists()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
result.sort(Comparator.comparing(l->l.title));
|
||||
lists=result;
|
||||
if(callback!=null)
|
||||
callback.onSuccess(result);
|
||||
writeListsToFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(callback!=null)
|
||||
callback.onError(error);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<FollowList> loadListsFromFile(){
|
||||
File file=getListsFile();
|
||||
if(!file.exists())
|
||||
return null;
|
||||
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "failed to read lists from cache file", x);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeListsToFile(){
|
||||
databaseThread.postRunnable(()->{
|
||||
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||
MastodonAPIController.gson.toJson(lists, out);
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "failed to write lists to cache file", x);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public void getLists(Callback<List<FollowList>> callback){
|
||||
if(lists!=null){
|
||||
if(callback!=null)
|
||||
callback.onSuccess(lists);
|
||||
return;
|
||||
}
|
||||
databaseThread.postRunnable(()->{
|
||||
List<FollowList> lists=loadListsFromFile();
|
||||
if(lists!=null){
|
||||
this.lists=lists;
|
||||
if(callback!=null)
|
||||
uiHandler.post(()->callback.onSuccess(lists));
|
||||
return;
|
||||
}
|
||||
reloadLists(callback);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public File getListsFile(){
|
||||
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||
}
|
||||
|
||||
public void addList(FollowList list){
|
||||
if(lists==null)
|
||||
return;
|
||||
lists.add(list);
|
||||
lists.sort(Comparator.comparing(l->l.title));
|
||||
writeListsToFile();
|
||||
}
|
||||
|
||||
public void deleteList(String id){
|
||||
if(lists==null)
|
||||
return;
|
||||
lists.removeIf(l->l.id.equals(id));
|
||||
writeListsToFile();
|
||||
}
|
||||
|
||||
public void updateList(FollowList list){
|
||||
if(lists==null)
|
||||
return;
|
||||
for(int i=0;i<lists.size();i++){
|
||||
if(lists.get(i).id.equals(list.id)){
|
||||
lists.set(i, list);
|
||||
lists.sort(Comparator.comparing(l->l.title));
|
||||
writeListsToFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DatabaseHelper extends SQLiteOpenHelper{
|
||||
|
||||
public DatabaseHelper(){
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -44,7 +45,11 @@ public class MastodonAPIController{
|
||||
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
|
||||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
private AccountSession session;
|
||||
|
||||
@@ -86,17 +91,20 @@ public class MastodonAPIController{
|
||||
synchronized(req){
|
||||
req.okhttpCall=call;
|
||||
}
|
||||
if(req.timeout>0){
|
||||
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
||||
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
|
||||
|
||||
call.enqueue(new Callback(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
if(call.isCanceled())
|
||||
if(req.canceled)
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
||||
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
@@ -105,10 +113,10 @@ public class MastodonAPIController{
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
if(call.isCanceled())
|
||||
if(req.canceled)
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
||||
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
@@ -119,7 +127,7 @@ public class MastodonAPIController{
|
||||
try{
|
||||
if(BuildConfig.DEBUG){
|
||||
JsonElement respJson=JsonParser.parseReader(reader);
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
Log.d(TAG, logTag(session)+"response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else if(req.respClass!=null)
|
||||
@@ -136,7 +144,7 @@ public class MastodonAPIController{
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
@@ -145,19 +153,19 @@ public class MastodonAPIController{
|
||||
req.validateAndPostprocessResponse(respObj, response);
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
||||
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
|
||||
|
||||
req.onSuccess(respObj);
|
||||
}else{
|
||||
try{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||
Log.w(TAG, logTag(session)+response+" received error: "+error);
|
||||
if(error.has("details")){
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||
@@ -192,7 +200,7 @@ public class MastodonAPIController{
|
||||
});
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage(), 0, x);
|
||||
}
|
||||
}, 0);
|
||||
@@ -205,4 +213,8 @@ public class MastodonAPIController{
|
||||
public static OkHttpClient getHttpClient(){
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private static String logTag(AccountSession session){
|
||||
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
Token token;
|
||||
boolean canceled;
|
||||
Map<String, String> headers;
|
||||
long timeout;
|
||||
private ProgressDialog progressDialog;
|
||||
protected boolean removeUnsupportedItems;
|
||||
|
||||
@@ -127,6 +128,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
protected void setTimeout(long timeout){
|
||||
this.timeout=timeout;
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v1";
|
||||
}
|
||||
@@ -149,6 +154,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
if(requestBody instanceof RequestBody rb)
|
||||
return rb;
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
|
||||
private String accountID;
|
||||
private PrivateKey privateKey;
|
||||
private PublicKey publicKey;
|
||||
private PublicKey serverKey;
|
||||
private byte[] authKey;
|
||||
|
||||
public PushSubscriptionManager(String accountID){
|
||||
@@ -162,10 +161,6 @@ public class PushSubscriptionManager{
|
||||
@Override
|
||||
public void onSuccess(PushSubscription result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
result.serverKey=result.serverKey.replace('/','_');
|
||||
result.serverKey=result.serverKey.replace('+','-');
|
||||
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
|
||||
|
||||
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
|
||||
if(session==null)
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
|
||||
public CheckInviteLink(String path){
|
||||
super(HttpMethod.GET, path, Response.class);
|
||||
addHeader("Accept", "application/json");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "";
|
||||
}
|
||||
|
||||
public static class Response extends BaseModel{
|
||||
@RequiredField
|
||||
public String inviteCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public GetAccountLists(String id){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason){
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
|
||||
super(HttpMethod.POST, "/accounts", Token.class);
|
||||
setRequestBody(new Body(username, email, password, locale, reason));
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
|
||||
}
|
||||
|
||||
private static class Body{
|
||||
public String username, email, password, locale, reason;
|
||||
public String username, email, password, locale, reason, timeZone, inviteCode;
|
||||
public boolean agreement=true;
|
||||
|
||||
public Body(String username, String email, String password, String locale, String reason){
|
||||
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
|
||||
this.username=username;
|
||||
this.email=email;
|
||||
this.password=password;
|
||||
this.locale=locale;
|
||||
this.reason=reason;
|
||||
this.timeZone=timeZone;
|
||||
this.inviteCode=inviteCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
|
||||
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
|
||||
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
|
||||
addQueryParameter("q", q);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(offset>0)
|
||||
addQueryParameter("offset", offset+"");
|
||||
if(resolve)
|
||||
addQueryParameter("resolve", "true");
|
||||
if(following)
|
||||
addQueryParameter("following", "true");
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
private Uri avatar, cover;
|
||||
private File avatarFile, coverFile;
|
||||
private List<AccountField> fields;
|
||||
private Boolean discoverable, indexable;
|
||||
|
||||
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
@@ -41,6 +42,12 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
this.fields=fields;
|
||||
}
|
||||
|
||||
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
|
||||
this.discoverable=discoverable;
|
||||
this.indexable=indexable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||
@@ -58,15 +65,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
}else if(coverFile!=null){
|
||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||
}
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
}else{
|
||||
int i=0;
|
||||
for(AccountField field:fields){
|
||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||
i++;
|
||||
if(fields!=null){
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
}else{
|
||||
int i=0;
|
||||
for(AccountField field:fields){
|
||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(discoverable!=null)
|
||||
bldr.addFormDataPart("discoverable", discoverable.toString());
|
||||
if(indexable!=null)
|
||||
bldr.addFormDataPart("indexable", indexable.toString());
|
||||
|
||||
return bldr.build();
|
||||
}
|
||||
|
||||
@@ -6,18 +6,19 @@ import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
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);
|
||||
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{
|
||||
public Boolean locked, discoverable;
|
||||
public Boolean locked, discoverable, indexable;
|
||||
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.discoverable=discoverable;
|
||||
this.indexable=indexable;
|
||||
this.source=source;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
|
||||
public GetCatalogDefaultInstances(){
|
||||
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||
setTimeout(500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
return Uri.parse("https://api.joinmastodon.org/default-servers");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import org.joinmastodon.android.model.FilterContext;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class FilterRequest{
|
||||
public String title;
|
||||
public EnumSet<FilterContext> context;
|
||||
|
||||
@@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
|
||||
import okhttp3.FormBody;
|
||||
|
||||
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
|
||||
public AddAccountsToList(String listID, Collection<String> accountIDs){
|
||||
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
|
||||
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for(String id:accountIDs){
|
||||
builder.add("account_ids[]", id);
|
||||
}
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class CreateList extends MastodonAPIRequest<FollowList>{
|
||||
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.POST, "/lists", FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String title;
|
||||
public FollowList.RepliesPolicy repliesPolicy;
|
||||
public boolean exclusive;
|
||||
|
||||
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
this.title=title;
|
||||
this.repliesPolicy=repliesPolicy;
|
||||
this.exclusive=exclusive;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteList extends ResultlessMastodonAPIRequest{
|
||||
public DeleteList(String id){
|
||||
super(HttpMethod.DELETE, "/lists/"+id);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public GetLists(){
|
||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
|
||||
import okhttp3.FormBody;
|
||||
|
||||
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
|
||||
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
|
||||
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
|
||||
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for(String id:accountIDs){
|
||||
builder.add("account_ids[]", id);
|
||||
}
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class UpdateList extends MastodonAPIRequest<FollowList>{
|
||||
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String title;
|
||||
public FollowList.RepliesPolicy repliesPolicy;
|
||||
public boolean exclusive;
|
||||
|
||||
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
this.title=title;
|
||||
this.repliesPolicy=repliesPolicy;
|
||||
this.exclusive=exclusive;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,19 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.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);
|
||||
addQueryParameter("q", query);
|
||||
if(type!=null)
|
||||
addQueryParameter("type", type.name().toLowerCase());
|
||||
if(resolve)
|
||||
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){
|
||||
|
||||
@@ -17,6 +17,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
|
||||
public static class Request{
|
||||
public String status;
|
||||
public List<MediaAttribute> mediaAttributes;
|
||||
public List<String> mediaIds;
|
||||
public Poll poll;
|
||||
public String inReplyToId;
|
||||
@@ -32,5 +33,17 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public boolean multiple;
|
||||
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.mentions=Collections.emptyList();
|
||||
s.tags=Collections.emptyList();
|
||||
if(s.poll!=null){
|
||||
s.poll.id="fakeID"+i;
|
||||
s.poll.emojis=Collections.emptyList();
|
||||
s.poll.ownVotes=Collections.emptyList();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class TranslateStatus extends MastodonAPIRequest<Translation>{
|
||||
public TranslateStatus(String id, String lang){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/translate", Translation.class);
|
||||
setRequestBody(Map.of("lang", lang));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class GetFollowedTags extends HeaderPaginationRequest<Hashtag>{
|
||||
public GetFollowedTags(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetListTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
@@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("remote", "true");
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(!TextUtils.isEmpty(minID))
|
||||
addQueryParameter("min_id", minID);
|
||||
if(!TextUtils.isEmpty(sinceID))
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@@ -32,6 +33,7 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -66,6 +68,7 @@ public class AccountSession{
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
private transient List<FollowList> lists;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -195,7 +198,7 @@ public class AccountSession{
|
||||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, self.discoverable, self.source.indexable)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
@@ -261,4 +264,16 @@ public class AccountSession{
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public void updateAccountInfo(){
|
||||
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||
}
|
||||
|
||||
public boolean isNotificationsMentionsOnly(){
|
||||
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
|
||||
}
|
||||
|
||||
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +175,17 @@ public class AccountSessionManager{
|
||||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
session.getCacheController().getListsFile().delete();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
MastodonApp.context.deleteSharedPreferences(id);
|
||||
}else{
|
||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
||||
String dataDir=MastodonApp.context.getApplicationInfo().dataDir;
|
||||
if(dataDir!=null){
|
||||
File prefsDir=new File(dataDir, "shared_prefs");
|
||||
new File(prefsDir, id+".xml").delete();
|
||||
}
|
||||
}
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
@@ -279,7 +284,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSessionLocalInfo(AccountSession session){
|
||||
/*package*/ void updateSessionLocalInfo(AccountSession session){
|
||||
new GetOwnAccount()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class AccountAddedToListEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public final Account account;
|
||||
|
||||
public AccountAddedToListEvent(String accountID, String listID, Account account){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
this.account=account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class AccountRemovedFromListEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public final String targetAccountID;
|
||||
|
||||
public AccountRemovedFromListEvent(String accountID, String listID, String targetAccountID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
this.targetAccountID=targetAccountID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class FinishListCreationFragmentEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
|
||||
public FinishListCreationFragmentEvent(String accountID, String listID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class ListCreatedEvent{
|
||||
public final String accountID;
|
||||
public final FollowList list;
|
||||
|
||||
public ListCreatedEvent(String accountID, FollowList list){
|
||||
this.accountID=accountID;
|
||||
this.list=list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class ListDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
|
||||
public ListDeletedEvent(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 ListUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final FollowList list;
|
||||
|
||||
public ListUpdatedEvent(String accountID, FollowList list){
|
||||
this.accountID=accountID;
|
||||
this.list=list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
|
||||
private Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.add_user_to_list_title);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> allLists){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
loadAccountLists(allLists);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAccountLists(final List<FollowList> allLists){
|
||||
currentRequest=new GetAccountLists(account.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
|
||||
onDataLoaded(allLists.stream()
|
||||
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
|
||||
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
|
||||
.collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
TextView topText=new TextView(getActivity());
|
||||
topText.setTextAppearance(R.style.m3_body_medium);
|
||||
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
|
||||
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void onItemClick(CheckableListItem<FollowList> item){
|
||||
boolean add=!item.checked;
|
||||
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
item.checked=add;
|
||||
rebindItem(item);
|
||||
if(add){
|
||||
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
|
||||
}else{
|
||||
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
|
||||
protected FollowList followList;
|
||||
protected AvatarPileListItem<Void> membersItem;
|
||||
protected CheckableListItem<Void> exclusiveItem;
|
||||
protected FloatingHintEditTextLayout titleEditLayout;
|
||||
protected EditText titleEdit;
|
||||
protected Spinner showRepliesSpinner;
|
||||
private APIRequest<?> getMembersRequest;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
|
||||
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
|
||||
List<ListItem<Void>> items=new ArrayList<>();
|
||||
if(followList!=null){
|
||||
items.add(membersItem);
|
||||
}
|
||||
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
|
||||
items.add(exclusiveItem);
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(getMembersRequest!=null)
|
||||
getMembersRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
LinearLayout topView=new LinearLayout(getActivity());
|
||||
topView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.list_name);
|
||||
titleEditLayout.updateHint();
|
||||
if(followList!=null)
|
||||
titleEdit.setText(followList.title);
|
||||
topView.addView(titleEditLayout);
|
||||
|
||||
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
|
||||
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
|
||||
showRepliesLayout.setHint(R.string.list_show_replies_to);
|
||||
topView.addView(showRepliesLayout);
|
||||
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
|
||||
getString(R.string.list_replies_no_one),
|
||||
getString(R.string.list_replies_members),
|
||||
getString(R.string.list_replies_anyone)
|
||||
));
|
||||
showRepliesSpinner.setAdapter(spinnerAdapter);
|
||||
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
|
||||
case FOLLOWED -> 2;
|
||||
case LIST -> 1;
|
||||
case NONE -> 0;
|
||||
});
|
||||
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
|
||||
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected void doDeleteList(){
|
||||
new DeleteList(followList.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
AccountSessionManager.get(accountID).getCacheController().deleteList(followList.id);
|
||||
E.post(new ListDeletedEvent(accountID, followList.id));
|
||||
Nav.finish(BaseEditListFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onMembersClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}
|
||||
|
||||
protected void loadMembers(){
|
||||
getMembersRequest=new GetListAccounts(followList.id, null, 3)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
getMembersRequest=null;
|
||||
membersItem.avatars=new ArrayList<>();
|
||||
for(int i=0;i<Math.min(3, result.size());i++){
|
||||
Account acc=result.get(i);
|
||||
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
|
||||
}
|
||||
rebindItem(membersItem);
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getMembersRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
|
||||
return switch(showRepliesSpinner.getSelectedItemPosition()){
|
||||
case 0 -> FollowList.RepliesPolicy.NONE;
|
||||
case 1 -> FollowList.RepliesPolicy.LIST;
|
||||
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,24 @@ import android.view.WindowInsets;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
|
||||
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
@@ -33,16 +39,20 @@ import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -101,6 +111,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(T s:items){
|
||||
displayItems.addAll(buildDisplayItems(s));
|
||||
}
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,6 +133,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
if(notify)
|
||||
adapter.notifyItemRangeInserted(0, offset);
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
protected String getMaxID(){
|
||||
@@ -169,7 +181,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
||||
final Status status=_status.getContentStatus();
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||
private MediaAttachmentViewController transitioningHolder;
|
||||
|
||||
@Override
|
||||
@@ -235,6 +247,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
currentPhotoViewer=null;
|
||||
gridHolder.itemView.setHasTransientState(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -246,6 +259,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return gridHolder.getViewController(index);
|
||||
}
|
||||
});
|
||||
gridHolder.itemView.setHasTransientState(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -352,7 +366,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||
int prevSize=pollItems.size();
|
||||
pollItems.clear();
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems);
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems);
|
||||
if(prevSize!=pollItems.size()){
|
||||
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
|
||||
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
|
||||
@@ -450,6 +464,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
protected void loadRelationships(Set<String> ids){
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
// TODO somehow manage these and cancel outstanding requests on refresh
|
||||
@@ -560,6 +577,74 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
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;
|
||||
new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Translation result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
status.translation=result;
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
updateTranslation(itemID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
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();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTranslation(itemID);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
|
||||
item.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
@@ -568,6 +653,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void maybeShowPreReplySheet(Status status, Runnable proceed){
|
||||
Relationship rel=getRelationship(status.account.id);
|
||||
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
|
||||
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
|
||||
new NonMutualPreReplySheet(getActivity(), notAgain->{
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
|
||||
proceed.run();
|
||||
}, status.account, accountID).show();
|
||||
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
|
||||
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
|
||||
new OldPostPreReplySheet(getActivity(), notAgain->{
|
||||
if(notAgain)
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
|
||||
proceed.run();
|
||||
}, status).show();
|
||||
}else{
|
||||
proceed.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
@@ -640,7 +745,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
// Do not draw dividers between hashtag and/or account rows
|
||||
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
|
||||
return false;
|
||||
return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
|
||||
return !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
@@ -19,17 +22,14 @@ import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SoundEffectConstants;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
@@ -49,7 +49,6 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
@@ -57,7 +56,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
@@ -94,12 +93,14 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
@@ -109,7 +110,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift
|
||||
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
|
||||
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-z0-9_]+)(@[a-z0-9_\\.\\-]*)?|#([^\\s.]+)|:([a-z0-9_]+))", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
|
||||
|
||||
@SuppressLint("NewApi") // this class actually exists on 6.0
|
||||
@@ -252,7 +253,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
selfAvatar=view.findViewById(R.id.avatar);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
|
||||
selfUsername.setText('@'+self.username+'@'+instanceDomain);
|
||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||
if(self.avatar!=null)
|
||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
@@ -339,7 +341,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
}
|
||||
});
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
@@ -451,7 +453,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
char prevChar=spanStart>0 ? editable.charAt(spanStart-1) : ' ';
|
||||
if(!matcher.find() || !Character.isWhitespace(prevChar)){ // invalid mention, remove
|
||||
editable.removeSpan(span);
|
||||
continue;
|
||||
}else if(matcher.end()+spanStart<spanEnd){ // mention with something at the end, move the end offset
|
||||
editable.setSpan(span, spanStart, spanStart+matcher.end(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
}
|
||||
@@ -547,7 +548,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.publish){
|
||||
if(GlobalUserPreferences.altTextReminders)
|
||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
@@ -691,6 +692,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
req.visibility=statusVisibility;
|
||||
if(!mediaViewController.isEmpty()){
|
||||
req.mediaIds=mediaViewController.getAttachmentIDs();
|
||||
if(editingStatus != null){
|
||||
req.mediaAttributes=mediaViewController.getAttachmentAttributes();
|
||||
}
|
||||
}
|
||||
if(replyTo!=null){
|
||||
req.inReplyToId=replyTo.id;
|
||||
@@ -836,7 +840,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
boolean usePhotoPicker=UiUtils.isPhotoPickerAvailable();
|
||||
if(usePhotoPicker){
|
||||
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{
|
||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
@@ -961,6 +966,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void updateVisibilityIcon(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(statusVisibility==null){ // TODO find out why this happens
|
||||
statusVisibility=StatusPrivacy.PUBLIC;
|
||||
}
|
||||
@@ -1010,8 +1017,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
||||
}
|
||||
|
||||
private String sanitizeMediaDescription(String description){
|
||||
if(description == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
// The Gboard android keyboard attaches this text whenever the user
|
||||
// pastes something from the keyboard's suggestion bar.
|
||||
// Due to different end user locales, the exact text may vary, but at
|
||||
// least in version 13.4.08, all of the translations contained the
|
||||
// string "Gboard".
|
||||
if (description.contains("Gboard")){
|
||||
return null;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||
description = sanitizeMediaDescription(description);
|
||||
return mediaViewController.addMediaAttachment(uri, description);
|
||||
}
|
||||
|
||||
@@ -1052,6 +1077,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Editable e=mainEditText.getText();
|
||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||
if(start==-1 || end==-1)
|
||||
return;
|
||||
e.replace(start, end, text+" ");
|
||||
finishAutocomplete();
|
||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||
@@ -1104,4 +1131,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){
|
||||
postLang=language;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateEnterTransition(View prev, View container){
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
if(getArguments().getBoolean("fromThreadFragment")){
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
|
||||
);
|
||||
}else{
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
|
||||
);
|
||||
}
|
||||
anim.setDuration(300);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
return anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateExitTransition(View prev, View container){
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
|
||||
);
|
||||
anim.setDuration(200);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -131,20 +128,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.help){
|
||||
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
|
||||
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
|
||||
for(BulletSpan span:spans){
|
||||
BulletSpan betterSpan;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
|
||||
else
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
|
||||
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
|
||||
msg.removeSpan(span);
|
||||
}
|
||||
new M3AlertDialogBuilder(themeWrapper)
|
||||
.setTitle(R.string.what_is_alt_text)
|
||||
.setMessage(msg)
|
||||
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
@@ -181,7 +167,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
fakeAttachment.meta.width=width;
|
||||
fakeAttachment.meta.height=height;
|
||||
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
image.setAlpha(visible ? 1f : 0f);
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
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_add_24px);
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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_close_24;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -44,7 +45,7 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, id);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,24 +1,55 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
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.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends StatusListFragment{
|
||||
private String hashtag;
|
||||
private Hashtag hashtag;
|
||||
private String hashtagName;
|
||||
private ImageButton fab;
|
||||
private TextView headerTitle, headerSubtitle;
|
||||
private ProgressBarButton followButton;
|
||||
private ProgressBar followProgress;
|
||||
private MenuItem followMenuItem;
|
||||
private boolean followRequestRunning;
|
||||
private boolean toolbarContentVisible;
|
||||
private String maxID;
|
||||
|
||||
public HashtagTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -27,16 +58,25 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
hashtag=getArguments().getString("hashtag");
|
||||
setTitle('#'+hashtag);
|
||||
if(getArguments().containsKey("hashtag")){
|
||||
hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag"));
|
||||
hashtagName=hashtag.name;
|
||||
}else{
|
||||
hashtagName=getArguments().getString("hashtagName");
|
||||
}
|
||||
setTitle('#'+hashtagName);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)
|
||||
currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : maxID, null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -50,17 +90,39 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
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);
|
||||
|
||||
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;
|
||||
if(followMenuItem!=null)
|
||||
followMenuItem.setVisible(toolbarContentVisible);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("prefilledText", '#'+hashtag+' ');
|
||||
args.putString("prefilledText", '#'+hashtagName+' ');
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -68,4 +130,150 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
|
||||
}
|
||||
|
||||
@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);
|
||||
});
|
||||
updateHeader();
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(header));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMainAdapterOffset(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
followMenuItem=menu.add(getString(hashtag!=null && hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
|
||||
followMenuItem.setVisible(toolbarContentVisible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(hashtag!=null){
|
||||
setFollowed(!hashtag.following);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f);
|
||||
if(followMenuItem!=null)
|
||||
followMenuItem.setVisible(toolbarContentVisible);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
@@ -150,6 +150,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
});
|
||||
}
|
||||
}
|
||||
tabBar.selectTab(currentTab);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -5,40 +5,50 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
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.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -52,44 +62,143 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment{
|
||||
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{
|
||||
private ImageButton fab;
|
||||
private ImageView toolbarLogo;
|
||||
private Button toolbarShowNewPostsBtn;
|
||||
private LinearLayout listsDropdown;
|
||||
private FixedAspectRatioImageView listsDropdownArrow;
|
||||
private TextView listsDropdownText;
|
||||
private Button newPostsBtn;
|
||||
private View newPostsBtnWrap;
|
||||
private boolean newPostsBtnShown;
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
private ToolbarDropdownMenuController dropdownController;
|
||||
private HomeTimelineMenuController dropdownMainMenuController;
|
||||
private List<FollowList> lists=List.of();
|
||||
private ListMode listMode=ListMode.FOLLOWING;
|
||||
private FollowList currentList;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
private DiscoverInfoBannerHelper localTimelineBannerHelper;
|
||||
|
||||
private String maxID;
|
||||
private String lastSavedMarkerID;
|
||||
|
||||
public HomeTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
setListLayoutId(R.layout.fragment_timeline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
dropdownController=new ToolbarDropdownMenuController(this);
|
||||
dropdownMainMenuController=new HomeTimelineMenuController(dropdownController, new HomeTimelineMenuController.Callback(){
|
||||
@Override
|
||||
public void onFollowingSelected(){
|
||||
if(listMode==ListMode.FOLLOWING)
|
||||
return;
|
||||
listMode=ListMode.FOLLOWING;
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocalSelected(){
|
||||
if(listMode==ListMode.LOCAL)
|
||||
return;
|
||||
listMode=ListMode.LOCAL;
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FollowList> getLists(){
|
||||
return lists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListSelected(FollowList list){
|
||||
if(listMode==ListMode.LIST && currentList==list)
|
||||
return;
|
||||
listMode=ListMode.LIST;
|
||||
currentList=list;
|
||||
reload();
|
||||
}
|
||||
});
|
||||
setHasOptionsMenu(true);
|
||||
loadData();
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
lists=result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
});
|
||||
switch(listMode){
|
||||
case FOLLOWING -> {
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null || listMode!=ListMode.FOLLOWING)
|
||||
return;
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(listMode!=ListMode.FOLLOWING)
|
||||
return;
|
||||
super.onError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
case LOCAL -> {
|
||||
currentRequest=new GetPublicTimeline(true, false, offset>0 ? maxID : null, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
case LIST -> {
|
||||
currentRequest=new GetListTimeline(currentList.id, offset>0 ? maxID : null, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,6 +206,19 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
newPostsBtn=view.findViewById(R.id.new_posts_btn);
|
||||
newPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
|
||||
newPostsBtnWrap=view.findViewById(R.id.new_posts_btn_wrap);
|
||||
|
||||
if(newPostsBtnShown){
|
||||
newPostsBtnWrap.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
newPostsBtnWrap.setVisibility(View.GONE);
|
||||
newPostsBtnWrap.setScaleX(0.9f);
|
||||
newPostsBtnWrap.setScaleY(0.9f);
|
||||
newPostsBtnWrap.setAlpha(0f);
|
||||
newPostsBtnWrap.setTranslationY(V.dp(-56));
|
||||
}
|
||||
updateToolbarLogo();
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
@@ -116,13 +238,26 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.home, menu);
|
||||
menu.findItem(R.id.edit_list).setVisible(listMode==ListMode.LIST);
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.UpdateState.NO_UPDATE;
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater!=null)
|
||||
state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.settings){
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
}else if(id==R.id.edit_list){
|
||||
args.putParcelable("list", Parcels.wrap(currentList));
|
||||
Nav.go(getActivity(), EditListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -147,7 +282,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!data.isEmpty()){
|
||||
if(!data.isEmpty() && listMode==ListMode.FOLLOWING){
|
||||
String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID;
|
||||
if(!topPostID.equals(lastSavedMarkerID)){
|
||||
lastSavedMarkerID=topPostID;
|
||||
@@ -183,8 +318,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
// between the existing and newly loaded parts of the timeline.
|
||||
String sinceID=data.size()>1 ? data.get(1).id : "1";
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
|
||||
.setCallback(new Callback<>(){
|
||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||
loadAdditionalPosts(null, null, 20, sinceID, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
@@ -199,11 +334,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(needCache)
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +349,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -225,10 +361,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
V.setVisibilityAnimated(item.text, View.GONE);
|
||||
GapStatusDisplayItem gap=item.getItem();
|
||||
dataLoading=true;
|
||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
|
||||
.setCallback(new Callback<>(){
|
||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
if(getActivity()==null)
|
||||
@@ -242,7 +379,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
Status gapStatus=getStatusByID(gap.parentID);
|
||||
if(gapStatus!=null){
|
||||
gapStatus.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
}
|
||||
}else{
|
||||
Set<String> idsBelowGap=new HashSet<>();
|
||||
@@ -254,7 +392,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}else if(s.id.equals(gap.parentID)){
|
||||
belowGap=true;
|
||||
s.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
}else{
|
||||
gapPostIndex++;
|
||||
}
|
||||
@@ -270,6 +409,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}else{
|
||||
result=result.subList(0, endIndex);
|
||||
}
|
||||
if(needCache)
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
@@ -279,7 +420,6 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
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);
|
||||
@@ -287,7 +427,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
||||
}
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,9 +445,17 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
adapter.notifyItemChanged(gapPos);
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAdditionalPosts(String maxID, String minID, int limit, String sinceID, Callback<List<Status>> callback){
|
||||
MastodonAPIRequest<List<Status>> req=switch(listMode){
|
||||
case FOLLOWING -> new GetHomeTimeline(maxID, minID, limit, sinceID);
|
||||
case LOCAL -> new GetPublicTimeline(true, false, maxID, minID, limit, sinceID);
|
||||
case LIST -> new GetListTimeline(currentList.id, maxID, minID, limit, sinceID);
|
||||
};
|
||||
currentRequest=req;
|
||||
req.setCallback(callback).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -320,42 +469,41 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private void updateToolbarLogo(){
|
||||
toolbarLogo=new ImageView(getActivity());
|
||||
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
toolbarLogo.setImageResource(R.drawable.logo);
|
||||
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
|
||||
toolbarShowNewPostsBtn=new Button(getActivity());
|
||||
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
||||
toolbarShowNewPostsBtn.setTextColor(0xffffffff);
|
||||
toolbarShowNewPostsBtn.setStateListAnimator(null);
|
||||
toolbarShowNewPostsBtn.setBackgroundResource(R.drawable.bg_button_new_posts);
|
||||
toolbarShowNewPostsBtn.setText(R.string.see_new_posts);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_up_16_filled, 0, 0, 0);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablePadding(V.dp(8));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
|
||||
toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
|
||||
|
||||
if(newPostsBtnShown){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
toolbarLogo.setAlpha(0f);
|
||||
}else{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
toolbarShowNewPostsBtn.setAlpha(0f);
|
||||
toolbarShowNewPostsBtn.setScaleX(.8f);
|
||||
toolbarShowNewPostsBtn.setScaleY(.8f);
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
listsDropdown=new LinearLayout(getActivity());
|
||||
listsDropdown.setOnClickListener(this::onListsDropdownClick);
|
||||
listsDropdown.setBackgroundResource(R.drawable.bg_button_m3_text);
|
||||
listsDropdown.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName("android.widget.Spinner");
|
||||
}
|
||||
});
|
||||
listsDropdownArrow=new FixedAspectRatioImageView(getActivity());
|
||||
listsDropdownArrow.setUseHeight(true);
|
||||
listsDropdownArrow.setImageResource(R.drawable.ic_arrow_drop_down_24px);
|
||||
listsDropdownArrow.setScaleType(ImageView.ScaleType.CENTER);
|
||||
listsDropdownArrow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
listsDropdown.addView(listsDropdownArrow, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
listsDropdownText=new TextView(getActivity());
|
||||
listsDropdownText.setTextAppearance(R.style.action_bar_title);
|
||||
listsDropdownText.setSingleLine();
|
||||
listsDropdownText.setEllipsize(TextUtils.TruncateAt.END);
|
||||
listsDropdownText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
|
||||
listsDropdownText.setPaddingRelative(V.dp(4), 0, V.dp(16), 0);
|
||||
listsDropdownText.setText(getCurrentListTitle());
|
||||
listsDropdownArrow.setImageTintList(listsDropdownText.getTextColors());
|
||||
listsDropdown.setBackgroundTintList(listsDropdownText.getTextColors());
|
||||
listsDropdown.addView(listsDropdownText, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
FrameLayout logoWrap=new FrameLayout(getActivity());
|
||||
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
|
||||
FrameLayout.LayoutParams ddlp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.START);
|
||||
ddlp.topMargin=ddlp.bottomMargin=V.dp(8);
|
||||
logoWrap.addView(listsDropdown, ddlp);
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
toolbar.setContentInsetsRelative(V.dp(16), 0);
|
||||
}
|
||||
|
||||
private void showNewPostsButton(){
|
||||
@@ -365,20 +513,19 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
newPostsBtnWrap.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, 1f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, 0f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3));
|
||||
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_decelerate));
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
@@ -393,20 +540,19 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, .9f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, .9f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, V.dp(-56))
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3));
|
||||
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_accelerate));
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
newPostsBtnWrap.setVisibility(View.GONE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
@@ -421,6 +567,20 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private void onListsDropdownClick(View v){
|
||||
listsDropdownArrow.animate().rotation(-180f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
dropdownController.show(dropdownMainMenuController);
|
||||
AccountSessionManager.get(accountID).getCacheController().reloadLists(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(java.util.List<FollowList> result){
|
||||
lists=result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
@@ -443,4 +603,67 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Toolbar getToolbar(){
|
||||
return super.getToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropdownWillDismiss(){
|
||||
listsDropdownArrow.animate().rotation(0f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropdownDismissed(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
refreshing=true;
|
||||
showProgress();
|
||||
loadData();
|
||||
listsDropdownText.setText(getCurrentListTitle());
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDataLoaded(List<Status> d, boolean more){
|
||||
if(refreshing){
|
||||
if(listMode==ListMode.LOCAL){
|
||||
localTimelineBannerHelper.maybeAddBanner(list, mergeAdapter);
|
||||
localTimelineBannerHelper.onBannerBecameVisible();
|
||||
}else{
|
||||
localTimelineBannerHelper.removeBanner(mergeAdapter);
|
||||
}
|
||||
}
|
||||
super.onDataLoaded(d, more);
|
||||
}
|
||||
|
||||
private String getCurrentListTitle(){
|
||||
return switch(listMode){
|
||||
case FOLLOWING -> getString(R.string.timeline_following);
|
||||
case LOCAL -> getString(R.string.local_timeline);
|
||||
case LIST -> currentList.title;
|
||||
};
|
||||
}
|
||||
|
||||
private enum ListMode{
|
||||
FOLLOWING,
|
||||
LOCAL,
|
||||
LIST
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
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.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.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 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: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_add_24px);
|
||||
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(data.get(i).account.id)){
|
||||
data.remove(i);
|
||||
list.getAdapter().notifyItemRemoved(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ListTimelineFragment extends StatusListFragment{
|
||||
private FollowList followList;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
setTitle(followList.title);
|
||||
setHasOptionsMenu(true);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetListTimeline(followList.id, offset>0 ? getMaxID() : null, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.standalone_list_timeline, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
if(id==R.id.members){
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}else if(id==R.id.edit_list){
|
||||
Nav.go(getActivity(), EditListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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_tag_24px, 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_add_24px);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
@@ -35,8 +36,15 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
if(wantsElevationOnScrollEffect()){
|
||||
FragmentRootLinearLayout rootView;
|
||||
if(view instanceof FragmentRootLinearLayout frl)
|
||||
rootView=frl;
|
||||
else
|
||||
rootView=view.findViewById(R.id.appkit_loader_root);
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener(rootView, getViewsForElevationEffect()));
|
||||
}
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
if(refreshLayout!=null){
|
||||
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
|
||||
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
|
||||
|
||||
@@ -44,7 +44,7 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
private boolean onlyMentions=true;
|
||||
private boolean onlyMentions;
|
||||
private String maxID;
|
||||
private View tabBar;
|
||||
private View mentionsTab, allTab;
|
||||
@@ -58,9 +58,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
super.onCreate(savedInstanceState);
|
||||
setLayout(R.layout.fragment_notifications);
|
||||
E.register(this);
|
||||
if(savedInstanceState!=null){
|
||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", true);
|
||||
}
|
||||
onlyMentions=AccountSessionManager.get(accountID).isNotificationsMentionsOnly();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@@ -132,13 +130,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
if(!dataLoading){
|
||||
if(onlyMentions){
|
||||
refresh();
|
||||
}else{
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,12 +215,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
return views;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("onlyMentions", onlyMentions);
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
for(Notification n:data){
|
||||
if(n.id.equals(id))
|
||||
@@ -291,8 +279,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
allTab.setSelected(!onlyMentions);
|
||||
maxID=null;
|
||||
showProgress();
|
||||
loadData(0, 20);
|
||||
refreshing=true;
|
||||
reloadingFromCache=true;
|
||||
loadData(0, 20);
|
||||
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -312,7 +302,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -325,12 +314,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
|
||||
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;
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,10 +338,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMarkAllReadButton(){
|
||||
markAllReadItem.setEnabled(!data.isEmpty() && !realUnreadMarker.equals(data.get(0).id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendItems(List<Notification> items){
|
||||
super.onAppendItems(items);
|
||||
@@ -364,4 +350,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
private boolean isInEditMode;
|
||||
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private boolean editDirty;
|
||||
|
||||
public void setFields(List<AccountField> fields){
|
||||
this.fields=fields;
|
||||
@@ -83,12 +84,17 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
fields=editableFields;
|
||||
adapter.notifyDataSetChanged();
|
||||
dragHelper.attachToRecyclerView(list);
|
||||
editDirty=false;
|
||||
}
|
||||
|
||||
public List<AccountField> getFields(){
|
||||
return fields;
|
||||
}
|
||||
|
||||
public boolean isEditDirty(){
|
||||
return editDirty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
@@ -209,6 +215,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
private class EditableAboutViewHolder extends BaseViewHolder{
|
||||
private final EditText title;
|
||||
private final EditText value;
|
||||
private boolean ignoreTextChange;
|
||||
|
||||
public EditableAboutViewHolder(){
|
||||
super(R.layout.onboarding_profile_field);
|
||||
@@ -218,16 +225,26 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
dragHelper.startDrag(this);
|
||||
return true;
|
||||
});
|
||||
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
|
||||
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
|
||||
title.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
item.name=e.toString();
|
||||
if(!ignoreTextChange)
|
||||
editDirty=true;
|
||||
}));
|
||||
value.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
item.value=e.toString();
|
||||
if(!ignoreTextChange)
|
||||
editDirty=true;
|
||||
}));
|
||||
findViewById(R.id.delete).setOnClickListener(this::onRemoveRowClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
super.onBind(item);
|
||||
ignoreTextChange=true;
|
||||
title.setText(item.name);
|
||||
value.setText(item.value);
|
||||
ignoreTextChange=false;
|
||||
}
|
||||
|
||||
private void onRemoveRowClick(View v){
|
||||
|
||||
@@ -93,7 +93,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
|
||||
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
|
||||
case STATUS -> {
|
||||
Status status=res.status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
|
||||
@@ -40,7 +40,6 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
@@ -63,10 +62,13 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.text.ImageSpanThatDoesNotBreakShitForNoGoodReason;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
import org.joinmastodon.android.ui.views.CustomDrawingOrderLinearLayout;
|
||||
@@ -107,7 +109,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ImageView avatar;
|
||||
private CoverImageView cover;
|
||||
private View avatarBorder;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
|
||||
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
|
||||
private ProgressBarButton actionButton;
|
||||
private ViewPager2 pager;
|
||||
private NestedRecyclerScrollView scrollView;
|
||||
@@ -134,7 +136,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private boolean isOwnProfile;
|
||||
private ArrayList<AccountField> fields=new ArrayList<>();
|
||||
|
||||
private boolean isInEditMode;
|
||||
private boolean isInEditMode, editDirty;
|
||||
private Uri editNewAvatar, editNewCover;
|
||||
private String profileAccountID;
|
||||
private boolean refreshing;
|
||||
@@ -147,6 +149,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private boolean tabBarIsAtTop;
|
||||
private Animator tabBarColorAnim;
|
||||
private MenuItem editSaveMenuItem;
|
||||
private boolean savingEdits;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -184,6 +187,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
avatarBorder=content.findViewById(R.id.avatar_border);
|
||||
name=content.findViewById(R.id.name);
|
||||
username=content.findViewById(R.id.username);
|
||||
usernameDomain=content.findViewById(R.id.username_domain);
|
||||
bio=content.findViewById(R.id.bio);
|
||||
followersCount=content.findViewById(R.id.followers_count);
|
||||
followersLabel=content.findViewById(R.id.followers_label);
|
||||
@@ -243,15 +247,23 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabbar.setTabTextSize(V.dp(14));
|
||||
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
||||
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, (tab, position)->tab.setText(switch(position){
|
||||
case 0 -> R.string.profile_featured;
|
||||
case 1 -> R.string.profile_timeline;
|
||||
case 2 -> R.string.profile_about;
|
||||
default -> throw new IllegalStateException();
|
||||
}));
|
||||
tabbar.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.profile_featured;
|
||||
case 1 -> R.string.profile_timeline;
|
||||
case 2 -> R.string.profile_about;
|
||||
default -> throw new IllegalStateException();
|
||||
});
|
||||
public void onTabSelected(TabLayout.Tab tab){}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab){}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){
|
||||
if(getFragmentForPage(tab.getPosition()) instanceof ScrollableToTop stt)
|
||||
stt.scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -268,6 +280,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
refreshLayout.setOnRefreshListener(this);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
|
||||
if(savedInstanceState!=null){
|
||||
featuredFragment=(ProfileFeaturedFragment) getChildFragmentManager().getFragment(savedInstanceState, "featured");
|
||||
timelineFragment=(AccountTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "timeline");
|
||||
aboutFragment=(ProfileAboutFragment) getChildFragmentManager().getFragment(savedInstanceState, "about");
|
||||
}
|
||||
|
||||
if(loaded){
|
||||
bindHeaderView();
|
||||
dataLoaded();
|
||||
@@ -285,9 +303,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
}
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
|
||||
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
UiUtils.maybeShowTextCopiedToast(getActivity());
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -304,6 +320,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
|
||||
refreshLayout.setColorSchemeColors(colorPrimary);
|
||||
|
||||
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
|
||||
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
@@ -349,15 +370,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public void dataLoaded(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
args.putBoolean("__is_tab", true);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
if(featuredFragment==null){
|
||||
featuredFragment=new ProfileFeaturedFragment();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
args.putBoolean("__is_tab", true);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
featuredFragment.setArguments(args);
|
||||
}
|
||||
if(timelineFragment==null){
|
||||
timelineFragment=AccountTimelineFragment.newInstance(accountID, account, true);
|
||||
}
|
||||
if(aboutFragment==null){
|
||||
aboutFragment=new ProfileAboutFragment();
|
||||
aboutFragment.setFields(fields);
|
||||
}
|
||||
@@ -423,6 +448,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
if(featuredFragment==null)
|
||||
return;
|
||||
if(featuredFragment.isAdded())
|
||||
getChildFragmentManager().putFragment(outState, "featured", featuredFragment);
|
||||
if(timelineFragment.isAdded())
|
||||
getChildFragmentManager().putFragment(outState, "timeline", timelineFragment);
|
||||
if(aboutFragment.isAdded())
|
||||
getChildFragmentManager().putFragment(outState, "about", aboutFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
@@ -466,23 +504,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
if(isSelf){
|
||||
ssb.append('@');
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb=new SpannableStringBuilder(account.username);
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
lock.setTint(username.getCurrentTextColor());
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpanThatDoesNotBreakShitForNoGoodReason(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
// noinspection SetTextI18n
|
||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||
username.setText(account.username);
|
||||
}
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
String domain=account.getDomain();
|
||||
if(TextUtils.isEmpty(domain))
|
||||
domain=AccountSessionManager.get(accountID).domain;
|
||||
usernameDomain.setText(domain);
|
||||
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
@@ -517,7 +554,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
fields.add(joined);
|
||||
|
||||
for(AccountField field:account.fields){
|
||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
|
||||
ssb=new SpannableStringBuilder(field.name);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
@@ -573,6 +610,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
else
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -596,10 +634,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(getActivity(), account.url);
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
|
||||
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account, relationship.domainBlocking, ()->{
|
||||
relationship.domainBlocking=!relationship.domainBlocking;
|
||||
updateRelationship();
|
||||
});
|
||||
}, this::updateRelationship);
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
@@ -626,6 +664,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
saveAndExitEditMode();
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -813,6 +856,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
aboutFragment.enterEditMode(account.source.fields);
|
||||
refreshLayout.setEnabled(false);
|
||||
editDirty=false;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
}
|
||||
|
||||
private void exitEditMode(){
|
||||
@@ -855,16 +900,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
refreshLayout.setEnabled(true);
|
||||
|
||||
bindHeaderView();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
|
||||
private void saveAndExitEditMode(){
|
||||
if(!isInEditMode)
|
||||
throw new IllegalStateException();
|
||||
setActionProgressVisible(true);
|
||||
savingEdits=true;
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, aboutFragment.getFields())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
savingEdits=false;
|
||||
account=result;
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
exitEditMode();
|
||||
@@ -873,6 +921,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
savingEdits=false;
|
||||
error.showToast(getActivity());
|
||||
setActionProgressVisible(false);
|
||||
}
|
||||
@@ -896,11 +945,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isInEditMode){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
if(savingEdits)
|
||||
return true;
|
||||
if(editDirty || aboutFragment.isEditDirty()){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}else{
|
||||
exitEditMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -925,7 +980,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return;
|
||||
int radius=V.dp(25);
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
|
||||
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
|
||||
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,7 +992,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(drawable==null || drawable instanceof ColorDrawable)
|
||||
return;
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
|
||||
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
||||
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,9 +1016,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(requestCode==AVATAR_RESULT){
|
||||
editNewAvatar=data.getData();
|
||||
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
|
||||
editDirty=true;
|
||||
}else if(requestCode==COVER_RESULT){
|
||||
editNewCover=data.getData();
|
||||
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
|
||||
editDirty=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -996,9 +1053,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
FrameLayout view=tabViews[viewType];
|
||||
((ViewGroup)view.getParent()).removeView(view);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
FrameLayout view=new FrameLayout(parent.getContext());
|
||||
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
return new SimpleViewHolder(view);
|
||||
}
|
||||
@@ -1006,8 +1061,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
|
||||
Fragment fragment=getFragmentForPage(position);
|
||||
FrameLayout fragmentView=tabViews[position];
|
||||
fragmentView.setVisibility(View.VISIBLE);
|
||||
if(fragmentView.getParent() instanceof ViewGroup parent)
|
||||
parent.removeView(fragmentView);
|
||||
((FrameLayout)holder.itemView).addView(fragmentView);
|
||||
if(!fragment.isAdded()){
|
||||
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
|
||||
getChildFragmentManager().beginTransaction().add(fragmentView.getId(), fragment).commit();
|
||||
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -37,6 +52,13 @@ public class SplashFragment extends AppKitFragment{
|
||||
private View artContainer, blueFill, greenFill;
|
||||
private InterpolatingMotionEffect motionEffect;
|
||||
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
|
||||
private ProgressBarButton defaultServerButton;
|
||||
private ProgressBar defaultServerProgress;
|
||||
private String chosenDefaultServer=DEFAULT_SERVER;
|
||||
private boolean loadingDefaultServer, loadedDefaultServer;
|
||||
private Uri currentInviteLink;
|
||||
private ProgressDialog instanceLoadingProgress;
|
||||
private String inviteCode;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -50,9 +72,14 @@ public class SplashFragment extends AppKitFragment{
|
||||
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
|
||||
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
|
||||
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
|
||||
Button joinDefault=contentView.findViewById(R.id.btn_join_default_server);
|
||||
joinDefault.setText(getString(R.string.join_default_server, DEFAULT_SERVER));
|
||||
joinDefault.setOnClickListener(this::onJoinDefaultServerClick);
|
||||
defaultServerButton=contentView.findViewById(R.id.btn_join_default_server);
|
||||
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
|
||||
defaultServerButton.setOnClickListener(this::onJoinDefaultServerClick);
|
||||
defaultServerProgress=contentView.findViewById(R.id.action_progress);
|
||||
if(loadingDefaultServer){
|
||||
defaultServerButton.setTextVisible(false);
|
||||
defaultServerProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
contentView.findViewById(R.id.btn_learn_more).setOnClickListener(this::onLearnMoreClick);
|
||||
|
||||
artClouds=contentView.findViewById(R.id.art_clouds);
|
||||
@@ -84,6 +111,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
});
|
||||
}
|
||||
});
|
||||
if(!loadedDefaultServer && !loadingDefaultServer)
|
||||
loadAndChooseDefaultServer();
|
||||
|
||||
return contentView;
|
||||
}
|
||||
@@ -92,18 +121,76 @@ public class SplashFragment extends AppKitFragment{
|
||||
Bundle extras=new Bundle();
|
||||
boolean isSignup=v.getId()==R.id.btn_get_started;
|
||||
extras.putBoolean("signup", isSignup);
|
||||
extras.putString("defaultServer", chosenDefaultServer);
|
||||
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
||||
}
|
||||
|
||||
private void onJoinDefaultServerClick(View v){
|
||||
if(loadingDefaultServer)
|
||||
return;
|
||||
instanceLoadingProgress=new ProgressDialog(getActivity());
|
||||
instanceLoadingProgress.setCancelable(false);
|
||||
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
|
||||
instanceLoadingProgress.show();
|
||||
if(currentInviteLink!=null){
|
||||
new CheckInviteLink(currentInviteLink.getPath())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(CheckInviteLink.Response result){
|
||||
inviteCode=result.inviteCode;
|
||||
proceedWithServerDomain(currentInviteLink.getHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
if(error instanceof MastodonErrorResponse mer){
|
||||
switch(mer.httpStatus){
|
||||
case 401 -> new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.expired_invite_link)
|
||||
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
case 404 -> new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.invalid_invite_link)
|
||||
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
default -> error.showToast(getActivity());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(currentInviteLink.getHost());
|
||||
return;
|
||||
}
|
||||
proceedWithServerDomain(chosenDefaultServer);
|
||||
}
|
||||
|
||||
private void proceedWithServerDomain(String domain){
|
||||
new GetInstance()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.instance_signup_closed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(result));
|
||||
if(inviteCode!=null)
|
||||
args.putString("inviteCode", inviteCode);
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -111,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading_instance, true)
|
||||
.execNoAuth(DEFAULT_SERVER);
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private void onLearnMoreClick(View v){
|
||||
@@ -168,4 +256,66 @@ public class SplashFragment extends AppKitFragment{
|
||||
super.onHidden();
|
||||
motionEffect.deactivate();
|
||||
}
|
||||
|
||||
private void loadAndChooseDefaultServer(){
|
||||
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
|
||||
if(clipData!=null && clipData.getItemCount()>0){
|
||||
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
|
||||
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
|
||||
currentInviteLink=Uri.parse(clipText.toString());
|
||||
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
|
||||
}
|
||||
}else{
|
||||
loadingDefaultServer=true;
|
||||
defaultServerButton.setTextVisible(false);
|
||||
defaultServerProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
new GetCatalogDefaultInstances()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<CatalogDefaultInstance> result){
|
||||
if(result.isEmpty()){
|
||||
setChosenDefaultServer(DEFAULT_SERVER);
|
||||
return;
|
||||
}
|
||||
float sum=0f;
|
||||
for(CatalogDefaultInstance inst:result){
|
||||
sum+=inst.weight;
|
||||
}
|
||||
if(sum<=0)
|
||||
sum=1f;
|
||||
for(CatalogDefaultInstance inst:result){
|
||||
inst.weight/=sum;
|
||||
}
|
||||
float rand=ThreadLocalRandom.current().nextFloat();
|
||||
float prev=0f;
|
||||
for(CatalogDefaultInstance inst:result){
|
||||
if(rand>=prev && rand<prev+inst.weight){
|
||||
setChosenDefaultServer(inst.domain);
|
||||
return;
|
||||
}
|
||||
prev+=inst.weight;
|
||||
}
|
||||
// Just in case something didn't add up
|
||||
setChosenDefaultServer(result.get(result.size()-1).domain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
setChosenDefaultServer(DEFAULT_SERVER);
|
||||
}
|
||||
})
|
||||
.execNoAuth("");
|
||||
}
|
||||
|
||||
private void setChosenDefaultServer(String domain){
|
||||
chosenDefaultServer=domain;
|
||||
loadingDefaultServer=false;
|
||||
loadedDefaultServer=true;
|
||||
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
|
||||
defaultServerButton.setTextVisible(true);
|
||||
defaultServerProgress.setVisibility(View.GONE);
|
||||
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,27 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -25,10 +31,12 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -36,10 +44,16 @@ import me.grishka.appkit.utils.V;
|
||||
public class ThreadFragment extends StatusListFragment{
|
||||
private Status mainStatus;
|
||||
private ImageView endMark;
|
||||
private FrameLayout replyContainer;
|
||||
private LinearLayout replyButton;
|
||||
private ImageView replyButtonAva;
|
||||
private TextView replyButtonText;
|
||||
private int lastBottomInset;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setLayout(R.layout.fragment_thread);
|
||||
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
|
||||
if(inReplyToAccount!=null)
|
||||
@@ -61,8 +75,14 @@ public class ThreadFragment extends StatusListFragment{
|
||||
text.textSelectable=true;
|
||||
else if(item instanceof FooterStatusDisplayItem footer)
|
||||
footer.hideCounts=true;
|
||||
else if(item instanceof SpoilerStatusDisplayItem spoiler){
|
||||
for(StatusDisplayItem subItem:spoiler.contentItems){
|
||||
if(subItem instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
|
||||
items.add(items.size()-1, new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -119,6 +139,20 @@ public class ThreadFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
replyContainer=view.findViewById(R.id.reply_button_wrapper);
|
||||
replyButton=replyContainer.findViewById(R.id.reply_button);
|
||||
replyButtonText=replyButton.findViewById(R.id.reply_btn_text);
|
||||
replyButtonAva=replyButton.findViewById(R.id.avatar);
|
||||
replyButton.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
replyButton.setClipToOutline(true);
|
||||
replyButtonText.setText(getString(R.string.reply_to_user, mainStatus.account.displayName));
|
||||
replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
|
||||
replyButtonAva.setClipToOutline(true);
|
||||
replyButton.setOnClickListener(v->openReply());
|
||||
Account self=AccountSessionManager.get(accountID).self;
|
||||
if(!TextUtils.isEmpty(self.avatar)){
|
||||
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
|
||||
}
|
||||
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
|
||||
showContent();
|
||||
if(!loaded)
|
||||
@@ -168,4 +202,24 @@ public class ThreadFragment extends StatusListFragment{
|
||||
}
|
||||
super.onErrorRetryClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
lastBottomInset=insets.getSystemWindowInsetBottom();
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
|
||||
}
|
||||
|
||||
private void openReply(){
|
||||
maybeShowPreReplySheet(mainStatus, ()->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
|
||||
args.putBoolean("fromThreadFragment", true);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
}
|
||||
|
||||
public int getSnackbarOffset(){
|
||||
return replyContainer.getHeight()-lastBottomInset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||
@@ -13,13 +15,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private String currentQuery;
|
||||
public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
protected String currentQuery;
|
||||
private boolean resultDelivered;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
|
||||
@@ -28,12 +31,11 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
super.onCreate(savedInstanceState);
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder());
|
||||
searchViewHelper.setListeners(this::onQueryChanged, null);
|
||||
searchViewHelper.addDivider(contentView);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -47,17 +49,25 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
|
||||
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false, null, 0, 0)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
AccountSearchFragment.this.onSuccess(result.accounts);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected void onSuccess(List<Account> result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_hint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class AddListMembersFragment extends AccountSearchFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
AddListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_among_people_you_follow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Button;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@SuppressLint("ValidFragment") // This shouldn't be part of any saved states anyway
|
||||
public class AddNewListMembersFragment extends AccountSearchFragment{
|
||||
private Listener listener;
|
||||
private String maxID;
|
||||
|
||||
public AddNewListMembersFragment(Listener listener){
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(TextUtils.isEmpty(currentQuery)){
|
||||
currentRequest=new GetAccountFollowing(AccountSessionManager.get(accountID).self.id, offset>0 ? maxID : null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
setEmptyText("");
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
|
||||
maxID=result.getNextPageMaxID();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}else{
|
||||
refreshing=true;
|
||||
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
AddNewListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_among_people_you_follow);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||
holder.setOnLongClickListener(vh->false);
|
||||
Button button=holder.getButton();
|
||||
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinWidth(0);
|
||||
button.setOnClickListener(v->{
|
||||
holder.setActionProgressVisible(true);
|
||||
holder.itemView.setHasTransientState(true);
|
||||
Runnable onDone=()->{
|
||||
holder.setActionProgressVisible(false);
|
||||
holder.itemView.setHasTransientState(false);
|
||||
onBindViewHolder(holder);
|
||||
};
|
||||
AccountViewModel account=holder.getItem();
|
||||
if(listener.isAccountInList(account)){
|
||||
listener.removeAccountAccountFromList(account, onDone);
|
||||
}else{
|
||||
listener.addAccountToList(account, onDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
Button button=holder.getButton();
|
||||
int textRes, styleRes;
|
||||
if(listener.isAccountInList(holder.getItem())){
|
||||
textRes=R.string.remove;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else{
|
||||
textRes=R.string.add;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
button.setText(textRes);
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
// no-op
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
boolean isAccountInList(AccountViewModel account);
|
||||
void addAccountToList(AccountViewModel account, Runnable onDone);
|
||||
void removeAccountAccountFromList(AccountViewModel account, Runnable onDone);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
protected int itemLayoutRes=R.layout.item_account_list;
|
||||
|
||||
public BaseAccountListFragment(){
|
||||
super(40);
|
||||
@@ -73,6 +74,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
req.setCallback(new Callback<>(){
|
||||
@@ -122,20 +125,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
if(hasSubtitle()){
|
||||
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
|
||||
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
|
||||
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
|
||||
toolbar.setTitleTextColor(color);
|
||||
toolbar.setSubtitleTextColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean hasSubtitle(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
@@ -150,6 +142,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
}
|
||||
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||
protected void onBindViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
@@ -159,7 +152,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
|
||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
|
||||
onConfigureViewHolder(holder);
|
||||
return holder;
|
||||
}
|
||||
@@ -167,6 +160,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
BaseAccountListFragment.this.onBindViewHolder(holder);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
|
||||