Compare commits
583 Commits
feature/fi
...
feature/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cb75d45f | ||
|
|
1675c8ab79 | ||
|
|
986b9731a1 | ||
|
|
dc66b27c8a | ||
|
|
3924c65e8c | ||
|
|
df7da2d7f6 | ||
|
|
697f95f3e3 | ||
|
|
f0d7da7194 | ||
|
|
b641e83470 | ||
|
|
f22f9c2dcc | ||
|
|
9ef2c278f6 | ||
|
|
4a7cb4f129 | ||
|
|
3a47e8eea3 | ||
|
|
4defd9c420 | ||
|
|
3bbff120f0 | ||
|
|
bf07e9f079 | ||
|
|
72fdff96c8 | ||
|
|
4e21b60087 | ||
|
|
7c786e55a5 | ||
|
|
51294bbb14 | ||
|
|
5dcda4f37a | ||
|
|
33d725acb4 | ||
|
|
fb78ac1243 | ||
|
|
ccfa614dc7 | ||
|
|
bc7f614573 | ||
|
|
7700288dbe | ||
|
|
e2c62aa76b | ||
|
|
35bf858a83 | ||
|
|
870bfaf08c | ||
|
|
c4238fb19b | ||
|
|
ba7aeb358b | ||
|
|
6f3fd4d454 | ||
|
|
97547f334f | ||
|
|
1ab953d819 | ||
|
|
161c19b628 | ||
|
|
307d483a56 | ||
|
|
9612248695 | ||
|
|
1f63401e5b | ||
|
|
d35ec18a88 | ||
|
|
b93b1847c3 | ||
|
|
cd46ed565f | ||
|
|
4a0e4edef8 | ||
|
|
2ea7333daa | ||
|
|
fa7a66809d | ||
|
|
71884ab760 | ||
|
|
f31205c670 | ||
|
|
0091ae87ce | ||
|
|
c196dc563f | ||
|
|
2baf51616c | ||
|
|
b75e2ce26f | ||
|
|
15976a991b | ||
|
|
16ee7371e7 | ||
|
|
02f0c794c7 | ||
|
|
ad13b1e927 | ||
|
|
a354ea80ab | ||
|
|
9f65b8112a | ||
|
|
6ac5d957fe | ||
|
|
4258c55b88 | ||
|
|
969f29e2e9 | ||
|
|
68921d0f0b | ||
|
|
c4ac4ee173 | ||
|
|
659b4e2fcd | ||
|
|
24e5bda8d3 | ||
|
|
02b1ad8d7a | ||
|
|
47eeb01b75 | ||
|
|
4288814138 | ||
|
|
ac4458e106 | ||
|
|
3d24b2de10 | ||
|
|
ed994b23e9 | ||
|
|
8c4678aba5 | ||
|
|
3d5fb2dfea | ||
|
|
ef6238b593 | ||
|
|
bc9bec3d66 | ||
|
|
d16e199dd1 | ||
|
|
a9c2df2e83 | ||
|
|
4673a4b9f7 | ||
|
|
d4a5286895 | ||
|
|
1b4579346b | ||
|
|
0665b8dd3b | ||
|
|
4f367d3e88 | ||
|
|
853124e2ce | ||
|
|
5dcd6e5a0d | ||
|
|
6f25c8be0f | ||
|
|
1db4b1319e | ||
|
|
76a97fcb47 | ||
|
|
4baaa39f35 | ||
|
|
52f025ae5a | ||
|
|
14b805e883 | ||
|
|
433a7b15fe | ||
|
|
6c8cbbc34a | ||
|
|
d4fbb298c1 | ||
|
|
9d78bb508a | ||
|
|
2aeb5f03d6 | ||
|
|
6522403c37 | ||
|
|
f090ca7f75 | ||
|
|
ac8b950893 | ||
|
|
2f02a238df | ||
|
|
22f9e941aa | ||
|
|
0d5fa97800 | ||
|
|
b102deaee1 | ||
|
|
3a73dbf304 | ||
|
|
faabc068ca | ||
|
|
443a69b10d | ||
|
|
6881651ce7 | ||
|
|
04aadf48f2 | ||
|
|
fc7ff07f40 | ||
|
|
968b2ee460 | ||
|
|
890340de94 | ||
|
|
c0589da549 | ||
|
|
4ca1a7b29e | ||
|
|
5432f2590c | ||
|
|
60ccf5cf0a | ||
|
|
bc717f5b10 | ||
|
|
3aead05ad4 | ||
|
|
9ef43cc6d3 | ||
|
|
24df7d49d5 | ||
|
|
486eef21dd | ||
|
|
44a4d02815 | ||
|
|
336a8194bd | ||
|
|
ac7c1c8497 | ||
|
|
d72f66b9bf | ||
|
|
1d445b8b27 | ||
|
|
14175a9140 | ||
|
|
7859f4cd05 | ||
|
|
37622ba9ce | ||
|
|
7a6af89375 | ||
|
|
056bfaacfe | ||
|
|
6684311ec5 | ||
|
|
11943571ad | ||
|
|
f696fcd412 | ||
|
|
2919e109ca | ||
|
|
995f478708 | ||
|
|
04fdea0296 | ||
|
|
b112c3a0b2 | ||
|
|
47149dd394 | ||
|
|
d1e0c1ebad | ||
|
|
2ef9d87250 | ||
|
|
fb8764bcd7 | ||
|
|
d7f73e02c5 | ||
|
|
e897b3af57 | ||
|
|
e04fd8a004 | ||
|
|
ada70ae1b5 | ||
|
|
5fdec0900e | ||
|
|
56a93288c4 | ||
|
|
02e3421f98 | ||
|
|
df7b53e10f | ||
|
|
fdbf331432 | ||
|
|
aed86ac6f0 | ||
|
|
3a13d4d6c0 | ||
|
|
f5336564d0 | ||
|
|
1ce49c68fe | ||
|
|
d37e880993 | ||
|
|
6fdb81a01f | ||
|
|
f9d6827572 | ||
|
|
10bf72b9ff | ||
|
|
800f929a15 | ||
|
|
bfcff1e19f | ||
|
|
f373e7df3e | ||
|
|
3985de5b14 | ||
|
|
4d75621384 | ||
|
|
e175a721d4 | ||
|
|
d9784ebc31 | ||
|
|
f241092277 | ||
|
|
0702703d78 | ||
|
|
2c4504bad3 | ||
|
|
07ca5a8b77 | ||
|
|
798a43906f | ||
|
|
41cb0f2e09 | ||
|
|
e12c0fb81f | ||
|
|
ac39f119e2 | ||
|
|
016faf3df0 | ||
|
|
b2d6879282 | ||
|
|
6926a212f4 | ||
|
|
addb6e06bf | ||
|
|
8ea752fbf7 | ||
|
|
89afc05d5c | ||
|
|
936f39161b | ||
|
|
ee20ee0722 | ||
|
|
02f9f8c8ea | ||
|
|
de3a252884 | ||
|
|
5e7a00de3e | ||
|
|
2858aeb55e | ||
|
|
357104efa9 | ||
|
|
bb8027c7ef | ||
|
|
f9dd787009 | ||
|
|
9478258caa | ||
|
|
9432bf9f38 | ||
|
|
c935a00763 | ||
|
|
404c4a3fdd | ||
|
|
4fa8f6deb5 | ||
|
|
f55b05012d | ||
|
|
ae81fee449 | ||
|
|
ec8dda4501 | ||
|
|
269b0b36b0 | ||
|
|
c8ed7c364b | ||
|
|
356833e248 | ||
|
|
1aa80270b4 | ||
|
|
1fccecdbf6 | ||
|
|
8be396e801 | ||
|
|
6755ff836e | ||
|
|
a3fc9710f7 | ||
|
|
e005731ba6 | ||
|
|
18ae3f4f61 | ||
|
|
10dfe0327e | ||
|
|
1d1e921137 | ||
|
|
0985a4c968 | ||
|
|
8df589c103 | ||
|
|
3ac9c09338 | ||
|
|
71b6b2f451 | ||
|
|
d85940ded8 | ||
|
|
e9e491c0b0 | ||
|
|
c73562fb75 | ||
|
|
3feacb59c8 | ||
|
|
a033d711c1 | ||
|
|
ba31afda27 | ||
|
|
3029c37755 | ||
|
|
728d55ffa0 | ||
|
|
265e9a9f56 | ||
|
|
7fbe205902 | ||
|
|
9b84333868 | ||
|
|
9525893bf7 | ||
|
|
daf15dcc62 | ||
|
|
b3e2b69b70 | ||
|
|
32081b71f5 | ||
|
|
7849c34d1f | ||
|
|
24977ec613 | ||
|
|
786bbab0d5 | ||
|
|
1facb07c28 | ||
|
|
bba5aba22d | ||
|
|
d7b85d6eba | ||
|
|
6832bfb95c | ||
|
|
4c379b67a3 | ||
|
|
3a2ae1ce71 | ||
|
|
c80afaf9c0 | ||
|
|
31d22bac47 | ||
|
|
b5f6687925 | ||
|
|
b3f25af923 | ||
|
|
78c141e946 | ||
|
|
83d36ce736 | ||
|
|
b928357ff1 | ||
|
|
c074bc57bc | ||
|
|
0e80c88b7d | ||
|
|
5ffa5b01fc | ||
|
|
61d9929485 | ||
|
|
231f19d113 | ||
|
|
bb41f62db5 | ||
|
|
47edc3180b | ||
|
|
9939d99c4b | ||
|
|
8053e8bb05 | ||
|
|
b7e9380bc4 | ||
|
|
83600087e1 | ||
|
|
b3dd5a2279 | ||
|
|
f9a8e10a85 | ||
|
|
d982331e21 | ||
|
|
68dfab9a44 | ||
|
|
c4f736a3fb | ||
|
|
b5abc86428 | ||
|
|
6b356c4dfd | ||
|
|
9e6723be41 | ||
|
|
fe84dc4823 | ||
|
|
08023a104c | ||
|
|
fd97cc6e87 | ||
|
|
78e5f0f011 | ||
|
|
73944675fa | ||
|
|
c6ded3d505 | ||
|
|
45b97de615 | ||
|
|
ec11a5e8e3 | ||
|
|
d71d17e5af | ||
|
|
e49e485ba3 | ||
|
|
2d4fc2166a | ||
|
|
84ba4ea999 | ||
|
|
c38eb545b1 | ||
|
|
b2d4a5aab0 | ||
|
|
201995849c | ||
|
|
1fc2f81dab | ||
|
|
69ddc95c2c | ||
|
|
a6ac68499c | ||
|
|
c10d7cfee4 | ||
|
|
f933bdbc53 | ||
|
|
274bca84d9 | ||
|
|
1e286dbc7a | ||
|
|
724d872491 | ||
|
|
64d5f9190a | ||
|
|
f1ab6833d3 | ||
|
|
140395f3cd | ||
|
|
f922e028a7 | ||
|
|
9aac0c007e | ||
|
|
e682cae7e7 | ||
|
|
f14977ba24 | ||
|
|
e8290e2f78 | ||
|
|
197110cfaf | ||
|
|
f320ac066c | ||
|
|
78acb5e7ea | ||
|
|
a14e864731 | ||
|
|
a87da87aad | ||
|
|
8befbb5a62 | ||
|
|
28893bc50b | ||
|
|
b73ef28f12 | ||
|
|
a71dc7f481 | ||
|
|
de4e7c1822 | ||
|
|
62090475f9 | ||
|
|
3c8715a7c4 | ||
|
|
4e188503a2 | ||
|
|
6fb7e97f13 | ||
|
|
80c9c591fc | ||
|
|
992fb5fefe | ||
|
|
f7c76f7503 | ||
|
|
e94364ecf6 | ||
|
|
1fab3d3743 | ||
|
|
35477055a9 | ||
|
|
e585f1df29 | ||
|
|
6311d18751 | ||
|
|
d29eef51ca | ||
|
|
c9692ef27b | ||
|
|
c36d3e9011 | ||
|
|
9409bbb9b2 | ||
|
|
3497747015 | ||
|
|
6343da1410 | ||
|
|
78a0c60600 | ||
|
|
ea152234a9 | ||
|
|
03612fcf64 | ||
|
|
a6a67512f4 | ||
|
|
dc9e977d40 | ||
|
|
5657df8db2 | ||
|
|
6abfe6ddd7 | ||
|
|
ab7489a049 | ||
|
|
a6fd6ae135 | ||
|
|
b30d4a025f | ||
|
|
5b747bfc74 | ||
|
|
a410d19114 | ||
|
|
a8589cc5b0 | ||
|
|
b057c9f7a8 | ||
|
|
96e4a4933c | ||
|
|
630064500d | ||
|
|
9543294996 | ||
|
|
56e9cc3406 | ||
|
|
be569cbe72 | ||
|
|
99f0817bdb | ||
|
|
220cd35d82 | ||
|
|
07f4ef1697 | ||
|
|
f20732ddc2 | ||
|
|
b1e0dc5843 | ||
|
|
285eb25706 | ||
|
|
ec556511e6 | ||
|
|
85c3d9f65f | ||
|
|
a7ebadf269 | ||
|
|
94c09d46c2 | ||
|
|
889fbc688d | ||
|
|
85d971242e | ||
|
|
23f82197c6 | ||
|
|
b4b16e2f37 | ||
|
|
2d838a8a23 | ||
|
|
7dab63cfe8 | ||
|
|
6548751bb0 | ||
|
|
65c158391f | ||
|
|
556fc4d31e | ||
|
|
c673f08aec | ||
|
|
818024d8dd | ||
|
|
cb3b893f72 | ||
|
|
f482b4bfe9 | ||
|
|
333c47339d | ||
|
|
a9283bfec8 | ||
|
|
f6f08d176c | ||
|
|
4c9f29e949 | ||
|
|
4ff5136652 | ||
|
|
56835b4f2d | ||
|
|
c43f734101 | ||
|
|
d4e1850d8c | ||
|
|
0b08072dfc | ||
|
|
66cdd63496 | ||
|
|
8b502b605c | ||
|
|
936a86acd7 | ||
|
|
64fc052c55 | ||
|
|
9897b8bfcd | ||
|
|
bcb50a9dd1 | ||
|
|
0b0775a86e | ||
|
|
73253f5f64 | ||
|
|
591b1cd9bf | ||
|
|
47a5d18ee0 | ||
|
|
b3f43804b0 | ||
|
|
139ec08d9d | ||
|
|
bc8846c351 | ||
|
|
85066f8ae6 | ||
|
|
1aec319e1c | ||
|
|
9848a94853 | ||
|
|
18ed3b44b4 | ||
|
|
a4043cc0e5 | ||
|
|
2b94d0c147 | ||
|
|
7e6395192b | ||
|
|
31db1cae7c | ||
|
|
f2c80a92c4 | ||
|
|
2cd107b400 | ||
|
|
eef4f42203 | ||
|
|
7eba69f574 | ||
|
|
cd226125cd | ||
|
|
1f543b4aa5 | ||
|
|
bd752824d9 | ||
|
|
a67dcbbf66 | ||
|
|
e0c77d3399 | ||
|
|
caf3ab5ce1 | ||
|
|
e549636645 | ||
|
|
3e2c3f40c7 | ||
|
|
40e20ead44 | ||
|
|
dfeba71abe | ||
|
|
2c0ec28803 | ||
|
|
2e1795dc6f | ||
|
|
74b06a4997 | ||
|
|
346610dd04 | ||
|
|
37cdc7116d | ||
|
|
d403f4ef01 | ||
|
|
cd1be782fa | ||
|
|
7f7eed1dec | ||
|
|
4843d574ca | ||
|
|
67059f3d71 | ||
|
|
0f0291074e | ||
|
|
4e143abfb9 | ||
|
|
558c5fba56 | ||
|
|
e50df3ea6d | ||
|
|
4383b11947 | ||
|
|
22209efc37 | ||
|
|
c3b5bb409b | ||
|
|
15f4d3326b | ||
|
|
a9ab9cb249 | ||
|
|
ac2e7cde41 | ||
|
|
3b7e11ba96 | ||
|
|
889fff5103 | ||
|
|
84ae1aad56 | ||
|
|
e9a2fae600 | ||
|
|
d32f03a067 | ||
|
|
7020018bb7 | ||
|
|
0cd0b9d580 | ||
|
|
3fda9e9eab | ||
|
|
e65404a466 | ||
|
|
3d47d1b4db | ||
|
|
d1749ab610 | ||
|
|
806c264686 | ||
|
|
34a9cb5a74 | ||
|
|
64fad2e871 | ||
|
|
961c69b525 | ||
|
|
c70f393559 | ||
|
|
9abdc174f4 | ||
|
|
2e5bfa1d9c | ||
|
|
9c89c26097 | ||
|
|
e3b6a5d389 | ||
|
|
0fb54efde5 | ||
|
|
a4a3f32dba | ||
|
|
03a1e29e0c | ||
|
|
eda9ff272b | ||
|
|
b3728e06ac | ||
|
|
33cbd85e19 | ||
|
|
8cb1f3f387 | ||
|
|
3f0c6fcec5 | ||
|
|
697262bb47 | ||
|
|
797cf893da | ||
|
|
a3564b70e1 | ||
|
|
43004307b8 | ||
|
|
5102fa2699 | ||
|
|
acd1e4ced3 | ||
|
|
6717070f93 | ||
|
|
65907742a6 | ||
|
|
6ef55f8b31 | ||
|
|
313448c42a | ||
|
|
4883f24fc7 | ||
|
|
be5ed6e224 | ||
|
|
3b32fe3663 | ||
|
|
e15b752bad | ||
|
|
5928cf675a | ||
|
|
7655a5a3ae | ||
|
|
387499ae49 | ||
|
|
8ab140c55d | ||
|
|
914abb95dd | ||
|
|
b5e0fca9c0 | ||
|
|
bac6f58115 | ||
|
|
5360c0f0f7 | ||
|
|
243d803b51 | ||
|
|
b343fe3835 | ||
|
|
3c42c1120f | ||
|
|
ad840dcef6 | ||
|
|
e2a20f9599 | ||
|
|
900823e0dd | ||
|
|
918bda54d6 | ||
|
|
cc1d1180e8 | ||
|
|
006c0d00f2 | ||
|
|
741a55110a | ||
|
|
f7dcb754ed | ||
|
|
f5e50bf668 | ||
|
|
4ad895ab71 | ||
|
|
0fccd0ab37 | ||
|
|
f73072d95e | ||
|
|
95cb9b5079 | ||
|
|
c6684d3c9b | ||
|
|
5c5989d8c0 | ||
|
|
60e92d30b0 | ||
|
|
8bf8e3f86b | ||
|
|
4fc6a8a2a5 | ||
|
|
891ee2d06b | ||
|
|
b450bc7ae8 | ||
|
|
4ca1e0d5db | ||
|
|
859213dd9e | ||
|
|
ad2857791d | ||
|
|
497827f2e2 | ||
|
|
967e333022 | ||
|
|
8df1406006 | ||
|
|
4af42fafdc | ||
|
|
a9e6a452c1 | ||
|
|
a4a4632397 | ||
|
|
421f39e414 | ||
|
|
f8121e2dc4 | ||
|
|
b1784fc51c | ||
|
|
96db0d7de7 | ||
|
|
3837ed9cb1 | ||
|
|
2be789a43c | ||
|
|
fd8d96169a | ||
|
|
397d768f3a | ||
|
|
1562dc32c1 | ||
|
|
f76eba894a | ||
|
|
42ae74f1a7 | ||
|
|
9d09a904ab | ||
|
|
5366c92b4d | ||
|
|
c047c53aac | ||
|
|
489a49ca40 | ||
|
|
ed44a4dac4 | ||
|
|
dc5c2dd907 | ||
|
|
cd86d04c9f | ||
|
|
d0c64fcdf5 | ||
|
|
dfff3b8bcf | ||
|
|
38f377ca09 | ||
|
|
cc28bba884 | ||
|
|
4e13f868fd | ||
|
|
251ffbba8d | ||
|
|
beb3081918 | ||
|
|
1b3c9106b5 | ||
|
|
385b91761b | ||
|
|
01ae9ba7f5 | ||
|
|
df1df28e23 | ||
|
|
23b2603a5f | ||
|
|
1e6dadd7ab | ||
|
|
26ab5a7f55 | ||
|
|
d7b76ed70a | ||
|
|
bc7946dc23 | ||
|
|
3a34599c82 | ||
|
|
0a6dd8a754 | ||
|
|
a61ece13af | ||
|
|
89e58fa947 | ||
|
|
4c47bb0768 | ||
|
|
82005bf3bd | ||
|
|
43600756c0 | ||
|
|
3c3e0633ad | ||
|
|
f819ad6917 | ||
|
|
03d89ae93c | ||
|
|
2e84faa505 | ||
|
|
e7e8d13d9e | ||
|
|
a683c2cb11 | ||
|
|
addf7de316 | ||
|
|
44d4eada51 | ||
|
|
40bfdea5b1 | ||
|
|
55138c1e86 | ||
|
|
e7ad396fc6 | ||
|
|
0aef680572 | ||
|
|
6dc37d6bde | ||
|
|
60ea7cedf6 | ||
|
|
c986b10e14 | ||
|
|
d52174bd9e | ||
|
|
c65d138911 | ||
|
|
ad9bb8ad58 | ||
|
|
b1cb4d4257 | ||
|
|
100bd4b062 | ||
|
|
7da09d9b37 | ||
|
|
f46eb07228 | ||
|
|
7627b5eb25 | ||
|
|
c710448c6b | ||
|
|
1ad270b1d6 | ||
|
|
099e253b2b | ||
|
|
66de4a5b91 | ||
|
|
41437d91d5 | ||
|
|
d33d5a6efa | ||
|
|
4f9248d040 | ||
|
|
f40c0e41f3 | ||
|
|
deeb03ff2b | ||
|
|
5c2a09e243 | ||
|
|
2473c999db | ||
|
|
ea2cc265e3 | ||
|
|
a0cd2d42cf |
1
.github/FUNDING.yml
vendored
@@ -1,6 +1,7 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: LucasGGamerM
|
||||
custom: ["https://liberapay.com/LucasGGamerM/donate", liberapay.com]
|
||||
patreon: # mastodon
|
||||
open_collective: # Replace with a single Open Collective username e.g., user1
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
|
||||
22
.github/workflows/nightly-builds.yml
vendored
@@ -10,6 +10,28 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Appkit Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: grishka/appkit
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
|
||||
- name: Comment out signing config in appkits gradle file
|
||||
run: |
|
||||
sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||
|
||||
- name: Grant execute permission for gradlew for Appkit
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Compile appkit
|
||||
run: ./gradlew publishToMavenLocal
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
|
||||
9
FAQ.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## F.A.Q
|
||||
|
||||
Q: What are the main differences between Moshidon and Megalodon?
|
||||
|
||||
A: There are many, but the most outstanding differences are: the ability to have other server's local timeline inside the app. It can be acessed in the "Add community" option in the top right corner of the Edit timelines screen. Other outstanding features that Moshidon has are some quality of life improvements, such as notification actions and allowing for unlisted replies by default. Most other features are pretty minor, such as profile notes directly available in the person's profile. Other features are quite minor usability and visibility improvements. All of which can be found in the settings page.
|
||||
|
||||
Q: Will there ever be a versjon of Moshidon for iOS?
|
||||
|
||||
A: No. As android and iOS apps do not share code, it is incredibly hard to port.
|
||||
73
README.md
@@ -18,27 +18,31 @@
|
||||
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
||||
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate!
|
||||
|
||||
### You can also donate some Monero through this wallet address as well:
|
||||
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j
|
||||
|
||||
---
|
||||
|
||||
## F.A.Q
|
||||
|
||||
### Q: What are the main differences between Moshidon and Megalodon?
|
||||
|
||||
### A: There are many, but the most outstanding differences are: the ability to have other server's local timeline inside the app. It can be acessed in the "Add community" option in the top right corner of the Edit timelines screen. Other outstanding features that Moshidon has are some quality of life improvements, such as notification actions and allowing for unlisted replies by default. Most other features are pretty minor, such as profile notes directly available in the person's profile. Other features are quite minor usability and visibility improvements. All of which can be found in the settings page.
|
||||
|
||||
---
|
||||
|
||||
## Key features
|
||||
|
||||
### **The ability to add new custom local timelines!**
|
||||
### **The ability to add other server's local timeline to your timelines**
|
||||
|
||||
#### It can be accessed in the "Edit timelines" menu, where you can add a new "Community" to see other server's local posts!
|
||||
It can be accessed in the "Edit timelines" menu, where you can add a new "Community" to see other server's local posts!
|
||||
|
||||
### **Material you theme support on Android 12+ devices!**
|
||||
### **View remote profiles**
|
||||
|
||||
### **Show posts filtered with a warning!**
|
||||
You can now see all of a profile follows and followers, by directly loading them from the profile's home instance. In case of a failed lookup, the app will automatically fall back to the older method.
|
||||
|
||||
**Allows you to have filtered posts collapsed with a warning! As shown in the screenshots:**
|
||||
### **Translate posts easily**
|
||||
|
||||
Allows you to easily translate posts in another language with a translate button! Your instance must support translation, otherwise it will not work.
|
||||
|
||||
### **Show posts filtered with a warning**
|
||||
|
||||
Allows you to have filtered posts collapsed with a warning! As shown in the screenshots:
|
||||
|
||||
Before | After
|
||||
:-------------------------:|:-------------------------:
|
||||
@@ -47,7 +51,7 @@ Before | After
|
||||
|
||||
### **Color themes**
|
||||
|
||||
**Allows you to change theme within the app. Supports Purple, pink, green, blue, red, orange, yellow and Nord!**
|
||||
Allows you to change theme within the app. Supports Material You, purple, pink, green, blue, red, orange, yellow and Nord!
|
||||
|
||||
### **Unlisted posting**
|
||||
|
||||
@@ -71,6 +75,10 @@ That’s one of the reasons why choosing a small, **well-moderated instance is i
|
||||
|
||||
This is important to **ensure the content you’re sharing is as accessible as possible** to people who can’t see the images and rely on software to read back the provided content descriptions. Thankfully, it’s quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
|
||||
|
||||
### **Reminder to add alt text to attached media**
|
||||
|
||||
By default, Moshidon will show a warning to add alt text if your post has any attachments without any alt text. This is for better accessibility, and it can easily be bypassed and disabled in settings.
|
||||
|
||||
### **Pinning posts**
|
||||
|
||||
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in people’s profiles shows all the posts they pinned.**
|
||||
@@ -95,12 +103,22 @@ Moshidon is also available in [IzzyOnDroid repo](https://apt.izzysoft.de/fdroid/
|
||||
|
||||
## Release variants
|
||||
|
||||
All downloads can be found on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
|
||||
### Stable variant
|
||||
|
||||
All stable version downloads can be found on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
|
||||
|
||||
**`moshidon.apk`**
|
||||
|
||||
Variant with an integrated updater. If you download Moshidon from here (and not from an app store), just download the regular `moshidon.apk`.
|
||||
|
||||
### Nightly variant
|
||||
|
||||
All nightly builds can be downloaded at [Nightly Releases](https://github.com/LucasGGamerM/moshidon-nightly/releases) page.
|
||||
|
||||
**`moshidon-nightly.apk`**
|
||||
|
||||
Unstable variant with an integrated updater. It's for development and testing purposes. If you find any bugs with it, please file a bug report at our [issues](https://github.com/LucasGGamerM/moshidon/issues) page.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -108,16 +126,18 @@ Variant with an integrated updater. If you download Moshidon from here (and not
|
||||
|
||||
### Features
|
||||
|
||||
* [Adding the ability to view other server's local timelines](https://github.com/LucasGGamerM/moshidon/tree/feature/local-timelines)
|
||||
* [Adding the ability to load followers and following from remote instance](https://github.com/LucasGGamerM/moshidon/tree/feature/remote-followers)
|
||||
* [Adding the ability to have filtered posts show with a warning](https://github.com/LucasGGamerM/moshidon/tree/feature/filters_again)
|
||||
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
|
||||
* Adding a useful private profile note box!*
|
||||
* Auto hiding the compose button on scroll!*
|
||||
* Adding the ability to remind yourself to add alt text to images!*
|
||||
* An indicator for if an image has alt text or not*
|
||||
* Adding the ability to have drafts!*
|
||||
* Also adding the ability to view announcements from your instance!*
|
||||
* Adding the ability to post for local timeline only (Only on instances that support it!)*
|
||||
* Adding a useful private profile note box
|
||||
* Auto hiding the compose button on scroll
|
||||
* Adding the ability to remind yourself to add alt text to images
|
||||
* An indicator for if an image has alt text or not
|
||||
* Adding the ability to have drafts
|
||||
* Also adding the ability to view announcements from your instance
|
||||
* Adding the ability to post for local timeline only (Only on instances that support it!)
|
||||
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
|
||||
* [Implement pinning posts and displaying pinned posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
|
||||
* [Implement deleting and re-drafting](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
|
||||
@@ -139,6 +159,7 @@ Variant with an integrated updater. If you download Moshidon from here (and not
|
||||
|
||||
### Behavior
|
||||
|
||||
* Allow for confirmation before reblogging
|
||||
* Adding a bottom option for the publish button, allowing for easier use on larger screens!
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||
* [Always preserve content warnings when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||
@@ -170,6 +191,12 @@ This project is released under the [GPL-3 License](./LICENSE).
|
||||
|
||||
## Links
|
||||
|
||||
[Official matrix chatroom:](https://matrix.to/#/#moshidon:matrix.org) https://matrix.to/#/#moshidon:matrix.org
|
||||
[F.A.Q](FAQ.md)
|
||||
|
||||
[Official matrix chatroom:](https://matrix.to/#/#moshidon:floss.social) https://matrix.to/#/#moshidon:floss.social
|
||||
|
||||
[Moshidon roadmap](https://github.com/users/LucasGGamerM/projects/1)
|
||||
|
||||
<a rel="me" href="https://floss.social/@moshidon">@moshidon<wbr>@floss.social</a>
|
||||
|
||||
---
|
||||
|
||||
@@ -3,6 +3,7 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.0'
|
||||
|
||||
@@ -16,11 +16,11 @@ android {
|
||||
applicationId "org.joinmastodon.android.moshinda"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 99
|
||||
versionName "1.2.0+fork.99.moshinda"
|
||||
versionCode 100
|
||||
versionName "1.3.0+fork.100.moshinda"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
nightly{
|
||||
@@ -62,7 +62,6 @@ android {
|
||||
initWith release
|
||||
}
|
||||
nightly{
|
||||
initWith release
|
||||
if(System.getenv("CURRENT_DATE") != null){
|
||||
versionNameSuffix '-nightly+@' + System.getenv("CURRENT_DATE")
|
||||
} else {
|
||||
@@ -71,6 +70,7 @@ android {
|
||||
versionNameSuffix '-nightly+@' + properties.getProperty('CURRENT_DATE')
|
||||
}
|
||||
applicationIdSuffix '.nightly'
|
||||
|
||||
signingConfig signingConfigs.nightly
|
||||
manifestPlaceholders = [oAuthScheme:"moshidon-android-nightly-auth"]
|
||||
}
|
||||
@@ -91,7 +91,7 @@ android {
|
||||
setRoot "src/github"
|
||||
}
|
||||
debug {
|
||||
setRoot "src/github"
|
||||
setRoot "src/debug"
|
||||
}
|
||||
}
|
||||
namespace 'org.joinmastodon.android'
|
||||
@@ -114,7 +114,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.7'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.8'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public class ThreadFragmentTest {
|
||||
|
||||
private Status fakeStatus(String id, String inReplyTo) {
|
||||
Status status = Status.ofFake(id, null, null);
|
||||
status.inReplyToId = inReplyTo;
|
||||
return status;
|
||||
}
|
||||
|
||||
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
|
||||
return new ThreadFragment.NeighborAncestryInfo(s, d, a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapNeighborhoodAncestry() {
|
||||
StatusContext context = new StatusContext();
|
||||
context.ancestors = List.of(
|
||||
fakeStatus("oldest ancestor", null),
|
||||
fakeStatus("younger ancestor", "oldest ancestor")
|
||||
);
|
||||
Status mainStatus = fakeStatus("main status", "younger ancestor");
|
||||
context.descendants = List.of(
|
||||
fakeStatus("first reply", "main status"),
|
||||
fakeStatus("reply to first reply", "first reply"),
|
||||
fakeStatus("third level reply", "reply to first reply"),
|
||||
fakeStatus("another reply", "main status")
|
||||
);
|
||||
|
||||
List<ThreadFragment.NeighborAncestryInfo> neighbors =
|
||||
ThreadFragment.mapNeighborhoodAncestry(mainStatus, context);
|
||||
|
||||
assertEquals(List.of(
|
||||
fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null),
|
||||
fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)),
|
||||
fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)),
|
||||
fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus),
|
||||
fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)),
|
||||
fakeInfo(context.descendants.get(2), null, context.descendants.get(1)),
|
||||
fakeInfo(context.descendants.get(3), null, null)
|
||||
), neighbors);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maybeApplyMainStatus() {
|
||||
ThreadFragment fragment = new ThreadFragment();
|
||||
fragment.contextInitiallyRendered = true;
|
||||
fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH);
|
||||
|
||||
Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH);
|
||||
update1.editedAt = Instant.ofEpochSecond(1);
|
||||
fragment.updatedStatus = update1;
|
||||
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.maybeApplyMainStatus();
|
||||
assertEquals("fired update event", update1, event1.status);
|
||||
assertEquals("updated main status", update1, fragment.mainStatus);
|
||||
|
||||
Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH);
|
||||
update2.favouritesCount = 123;
|
||||
fragment.updatedStatus = update2;
|
||||
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.maybeApplyMainStatus();
|
||||
assertEquals("only fired counter update event", update2.id, event2.id);
|
||||
assertEquals("updated counter is correct", 123, event2.favorites);
|
||||
assertEquals("updated main status", update2, fragment.mainStatus);
|
||||
|
||||
Status update3 = Status.ofFake("123456", "whatever", Instant.EPOCH);
|
||||
fragment.contextInitiallyRendered = false;
|
||||
fragment.updatedStatus = update3;
|
||||
assertNull("no update when context hasn't been rendered", fragment.maybeApplyMainStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortStatusContext() {
|
||||
StatusContext context = new StatusContext();
|
||||
context.ancestors = List.of(
|
||||
fakeStatus("younger ancestor", "oldest ancestor"),
|
||||
fakeStatus("oldest ancestor", null)
|
||||
);
|
||||
context.descendants = List.of(
|
||||
fakeStatus("reply to first reply", "first reply"),
|
||||
fakeStatus("third level reply", "reply to first reply"),
|
||||
fakeStatus("first reply", "main status"),
|
||||
fakeStatus("another reply", "main status")
|
||||
);
|
||||
|
||||
ThreadFragment.sortStatusContext(
|
||||
fakeStatus("main status", "younger ancestor"),
|
||||
context
|
||||
);
|
||||
List<Status> expectedAncestors = List.of(
|
||||
fakeStatus("oldest ancestor", null),
|
||||
fakeStatus("younger ancestor", "oldest ancestor")
|
||||
);
|
||||
List<Status> expectedDescendants = List.of(
|
||||
fakeStatus("first reply", "main status"),
|
||||
fakeStatus("reply to first reply", "first reply"),
|
||||
fakeStatus("third level reply", "reply to first reply"),
|
||||
fakeStatus("another reply", "main status")
|
||||
);
|
||||
|
||||
// TODO: ??? i have no idea how this code works. it certainly doesn't return what i'd expect
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class UiUtilsTest {
|
||||
@BeforeClass
|
||||
public static void createDummySession() {
|
||||
Instance dummyInstance = new Instance();
|
||||
dummyInstance.uri = "test.tld";
|
||||
Account dummyAccount = new Account();
|
||||
dummyAccount.id = "123456";
|
||||
AccountSessionManager.getInstance().addAccount(dummyInstance, null, dummyAccount, null, null);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanUp() {
|
||||
AccountSessionManager.getInstance().removeAccount("test.tld_123456");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseFediverseHandle() {
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||
UiUtils.parseFediverseHandle("megalodon@floss.social")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||
UiUtils.parseFediverseHandle("@megalodon@floss.social")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.empty())),
|
||||
UiUtils.parseFediverseHandle("@megalodon")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||
UiUtils.parseFediverseHandle("mailto:megalodon@floss.social")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.empty(),
|
||||
UiUtils.parseFediverseHandle("megalodon")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.empty(),
|
||||
UiUtils.parseFediverseHandle("this is not a fedi handle")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.empty(),
|
||||
UiUtils.parseFediverseHandle("not@a-domain")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acctMatches() {
|
||||
assertTrue("local account, domain not specified", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone",
|
||||
"someone",
|
||||
null
|
||||
));
|
||||
|
||||
assertTrue("domain not specified", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone@somewhere.social",
|
||||
"someone",
|
||||
null
|
||||
));
|
||||
|
||||
assertTrue("local account, domain specified, different casing", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"SomeOne",
|
||||
"someone",
|
||||
"Test.TLD"
|
||||
));
|
||||
|
||||
assertFalse("username doesn't match", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone-else@somewhere.social",
|
||||
"someone",
|
||||
"somewhere.social"
|
||||
));
|
||||
|
||||
assertFalse("domain doesn't match", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone@somewhere.social",
|
||||
"someone",
|
||||
"somewhere.else"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import static org.joinmastodon.android.model.Filter.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.Filter.FilterContext.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class StatusFilterPredicateTest {
|
||||
|
||||
private static final Filter hideMeFilter = new Filter(), warnMeFilter = new Filter();
|
||||
private static final List<Filter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||
|
||||
private static final Status
|
||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
|
||||
|
||||
static {
|
||||
hideMeFilter.phrase = "hide me";
|
||||
hideMeFilter.filterAction = HIDE;
|
||||
hideMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||
|
||||
warnMeFilter.phrase = "warning";
|
||||
warnMeFilter.filterAction = WARN;
|
||||
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHide() {
|
||||
assertFalse("should not pass because matching filter applies to given context",
|
||||
new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideRegardlessOfContext() {
|
||||
assertTrue("filters without context should always pass",
|
||||
new StatusFilterPredicate(allFilters, null).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideInDifferentContext() {
|
||||
assertTrue("should pass because matching filter does not apply to given context",
|
||||
new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideWithWarningText() {
|
||||
assertTrue("should pass because matching filter is for warnings",
|
||||
new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarn() {
|
||||
assertFalse("should not pass because filter applies to given context",
|
||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnRegardlessOfContext() {
|
||||
assertTrue("filters without context should always pass",
|
||||
new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnInDifferentContext() {
|
||||
assertTrue("should pass because filter does not apply to given context",
|
||||
new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnWithHideText() {
|
||||
assertTrue("should pass because matching filter is for hiding",
|
||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Keep
|
||||
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
private static final long CHECK_PERIOD=6*3600*1000L;
|
||||
private static final String TAG="GithubSelfUpdater";
|
||||
|
||||
private UpdateState state=UpdateState.NO_UPDATE;
|
||||
private UpdateInfo info;
|
||||
private long downloadID;
|
||||
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
|
||||
MastodonApp.context.unregisterReceiver(this);
|
||||
setState(UpdateState.DOWNLOADED);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public GithubSelfUpdaterImpl(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
|
||||
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
|
||||
info=new UpdateInfo();
|
||||
info.version=prefs.getString("version", null);
|
||||
info.size=prefs.getLong("apkSize", 0);
|
||||
info.changelog=prefs.getString("changelog", null);
|
||||
downloadID=prefs.getLong("downloadID", 0);
|
||||
if(downloadID==0 || !getUpdateApkFile().exists()){
|
||||
state=UpdateState.UPDATE_AVAILABLE;
|
||||
}else{
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
|
||||
if(state==UpdateState.DOWNLOADING){
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
}
|
||||
}
|
||||
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
|
||||
// We are in a new version, running for the first time after update. Gotta clean things up.
|
||||
long id=getPrefs().getLong("downloadID", 0);
|
||||
if(id!=0){
|
||||
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
|
||||
}
|
||||
getUpdateApkFile().delete();
|
||||
getPrefs().edit()
|
||||
.remove("apkSize")
|
||||
.remove("version")
|
||||
.remove("apkURL")
|
||||
.remove("checkedByBuild")
|
||||
.remove("downloadID")
|
||||
.remove("changelog")
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeCheckForUpdates(){
|
||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
|
||||
if(timeSinceLastCheck>=CHECK_PERIOD){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkForUpdates() {
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
|
||||
private void actuallyCheckForUpdates(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://api.github.com/repos/LucasGGamerM/moshidon/releases")
|
||||
.build();
|
||||
Call call=MastodonAPIController.getHttpClient().newCall(req);
|
||||
try(Response resp=call.execute()){
|
||||
JsonArray arr=JsonParser.parseReader(resp.body().charStream()).getAsJsonArray();
|
||||
for (JsonElement jsonElement : arr) {
|
||||
JsonObject obj = jsonElement.getAsJsonObject();
|
||||
if (obj.get("prerelease").getAsBoolean() && !GlobalUserPreferences.enablePreReleases) continue;
|
||||
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
String changelog=obj.get("body").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||
return;
|
||||
}
|
||||
int newMajor=Integer.parseInt(matcher.group(1)),
|
||||
newMinor=Integer.parseInt(matcher.group(2)),
|
||||
newRevision=Integer.parseInt(matcher.group(3)),
|
||||
newForkNumber=Integer.parseInt(matcher.group(4));
|
||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||
return;
|
||||
}
|
||||
int curMajor=Integer.parseInt(matcher.group(1)),
|
||||
curMinor=Integer.parseInt(matcher.group(2)),
|
||||
curRevision=Integer.parseInt(matcher.group(3)),
|
||||
curForkNumber=Integer.parseInt(matcher.group(4));
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
JsonObject asset=el.getAsJsonObject();
|
||||
if("moshidon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
|
||||
long size=asset.get("size").getAsLong();
|
||||
String url=asset.get("browser_download_url").getAsString();
|
||||
|
||||
UpdateInfo info=new UpdateInfo();
|
||||
info.size=size;
|
||||
info.version=version;
|
||||
info.changelog=changelog;
|
||||
this.info=info;
|
||||
|
||||
getPrefs().edit()
|
||||
.putLong("apkSize", size)
|
||||
.putString("version", version)
|
||||
.putString("apkURL", url)
|
||||
.putString("changelog", changelog)
|
||||
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
|
||||
.remove("downloadID")
|
||||
.apply();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
|
||||
break;
|
||||
}
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "actuallyCheckForUpdates", x);
|
||||
}finally{
|
||||
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(UpdateState state){
|
||||
this.state=state;
|
||||
E.post(new SelfUpdateStateChangedEvent(state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateState getState(){
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateInfo getUpdateInfo(){
|
||||
return info;
|
||||
}
|
||||
|
||||
public File getUpdateApkFile(){
|
||||
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadUpdate(){
|
||||
if(state==UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
downloadID=dm.enqueue(
|
||||
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
||||
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
||||
);
|
||||
getPrefs().edit().putLong("downloadID", downloadID).apply();
|
||||
setState(UpdateState.DOWNLOADING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installUpdate(Activity activity){
|
||||
if(state!=UpdateState.DOWNLOADED)
|
||||
throw new IllegalStateException();
|
||||
Uri uri;
|
||||
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}else{
|
||||
uri=Uri.fromFile(getUpdateApkFile());
|
||||
}
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||
activity.startActivity(intent);
|
||||
|
||||
// TODO figure out how to restart the app when updating via this new API
|
||||
/*
|
||||
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
|
||||
try{
|
||||
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
|
||||
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
|
||||
@Override
|
||||
public void onCreated(int i){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBadgingChanged(int i){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveChanged(int i, boolean b){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(int id, float progress){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(int id, boolean success){
|
||||
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
});
|
||||
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
|
||||
PackageInstaller.Session session=installer.openSession(sid);
|
||||
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
|
||||
byte[] buffer=new byte[16384];
|
||||
int read;
|
||||
while((read=in.read(buffer))>0){
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
session.commit(intent.getIntentSender());
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "installUpdate", x);
|
||||
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDownloadProgress(){
|
||||
if(state!=UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
|
||||
if(cursor.moveToFirst()){
|
||||
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
|
||||
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
|
||||
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
|
||||
return total>0 ? (float)loaded/total : 0f;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDownload(){
|
||||
if(state!=UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
dm.remove(downloadID);
|
||||
downloadID=0;
|
||||
getPrefs().edit().remove("downloadID").apply();
|
||||
setState(UpdateState.UPDATE_AVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleIntentFromInstaller(Intent intent, Activity activity){
|
||||
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
activity.startActivity(confirmIntent);
|
||||
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
|
||||
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
|
||||
Intent restartIntent=new Intent(context, MainActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setPackage(context.getPackageName());
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
|
||||
context.startActivity(restartIntent);
|
||||
}else{
|
||||
// Bypass activity starting restrictions by starting it from a notification
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
|
||||
nm.createNotificationChannel(chan);
|
||||
Notification n=new Notification.Builder(context, "selfUpdateRestart")
|
||||
.setContentTitle(context.getString(R.string.update_installed))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
|
||||
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.build();
|
||||
nm.notify(1, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class SelfUpdateContentProvider extends ContentProvider{
|
||||
@Override
|
||||
public boolean onCreate(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri){
|
||||
if(isCorrectUri(uri))
|
||||
return "application/vnd.android.package-archive";
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||
if(isCorrectUri(uri)){
|
||||
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
private boolean isCorrectUri(Uri uri){
|
||||
return "/update.apk".equals(uri.getPath());
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 988 B After Width: | Height: | Size: 988 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
@@ -40,6 +41,22 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
|
||||
# https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
|
||||
# This list contains domains of toxic mastodon instances
|
||||
# Last-Modified: 1672044500
|
||||
|
||||
# gab - a neonazi social network
|
||||
gab.ai
|
||||
gab.com
|
||||
gab.protohype.net
|
||||
|
||||
# consequence-free speech
|
||||
social.unzensiert.to
|
||||
freeatlantis.com
|
||||
|
||||
# reactionary bigotry and hatespeech against magrinalized groups
|
||||
poa.st
|
||||
freespeechextremist.com
|
||||
rdrama.cc
|
||||
outpoa.st
|
||||
anime.website
|
||||
gameliberty.club
|
||||
social.byoblu.com
|
||||
yggdrasil.social
|
||||
smuglo.li
|
||||
dogeposting.social
|
||||
unsafe.space
|
||||
freezepeach.xyz
|
||||
|
||||
# + CSAM
|
||||
rojogato.com
|
||||
|
||||
# antivaxxer shitposting & fearmongering
|
||||
shadowsocial.org
|
||||
|
||||
# Kiwifarms
|
||||
kiwifarms.net
|
||||
kiwifarms.cc
|
||||
kiwifarms.is
|
||||
kiwifarms.pleroma.net
|
||||
|
||||
|
||||
# https://mastodon.art/@Curator/109649354849593592
|
||||
|
||||
poa.st antisemitic racist homophobic
|
||||
nicecrew.digital antisemitic
|
||||
beefyboys.win antisemitic racist homophobic harassment
|
||||
cawfee.club antisemitic racist homophobic
|
||||
comfyboy.club antisemitic racist homophobic
|
||||
freespeechextremist.com racist homophobic
|
||||
cum.salon racist misogynist
|
||||
bae.st racist
|
||||
natehiggers.online racist
|
||||
rapemeat.solutions misogynist
|
||||
rapist.town misogynist
|
||||
rapefeminists.network misogynist
|
||||
kiwifarms.cc harassment
|
||||
noagendasocial.com noagenda
|
||||
posting.lolicon.rocks underage
|
||||
urchan.org harassment homophobic racist
|
||||
ryona.agency harassment
|
||||
yggdrasil.social antisemitic homophobic racist
|
||||
genderheretics.xyz transphobic
|
||||
baraag.net underage
|
||||
lolison.top underage
|
||||
shota.house underage
|
||||
shota.social underage
|
||||
aethy.com underage
|
||||
taullo.social underage
|
||||
childpawn.shop underage
|
||||
posting.lolicon.rocks underage
|
||||
loli.best underage
|
||||
gothloli.club underage
|
||||
smuglo.li underage
|
||||
youjo.love underage
|
||||
pedo.school underage
|
||||
lolison.network underage
|
||||
freak.university underage
|
||||
mirr0r.city underage
|
||||
xhais.love underage
|
||||
refusal.biz underage
|
||||
refusal.llc underage
|
||||
mirr0r.city underage
|
||||
nnia.space underage
|
||||
ignorelist.com malicious
|
||||
repl.co malicious
|
||||
|
||||
# custom
|
||||
|
||||
pawoo.net csam
|
||||
|
171
mastodon/src/main/assets/blocks.txt
Normal file
@@ -0,0 +1,171 @@
|
||||
13bells.com
|
||||
4aem.com
|
||||
aethy.com
|
||||
anime.website
|
||||
annihilation.social
|
||||
anon-kenkai.com
|
||||
asbestos.cafe
|
||||
bae.st
|
||||
bajax.us
|
||||
banepo.st
|
||||
baraag.net
|
||||
beefyboys.win
|
||||
beepboop.ga
|
||||
berserker.town
|
||||
bikeshed.party
|
||||
boks.moe
|
||||
brainsoap.net
|
||||
breastmilk.club
|
||||
brighteon.social
|
||||
cawfee.club
|
||||
clew.lol
|
||||
clubcyberia.co
|
||||
collapsitarian.io
|
||||
comfyboy.club
|
||||
contrapointsfan.club
|
||||
cum.camp
|
||||
cum.salon
|
||||
cybercriminal.eu
|
||||
darknight-coffee.org
|
||||
dembased.xyz
|
||||
desupost.soy
|
||||
detroitriotcity.com
|
||||
eatthebugs.social
|
||||
eientei.org
|
||||
elementality.org
|
||||
eveningzoo.club
|
||||
firedragonstudios.com
|
||||
firefaithfellowship.com
|
||||
fluf.club
|
||||
foxfam.club
|
||||
freak.university
|
||||
freeatlantis.com
|
||||
freecumextremist.com
|
||||
freedomstrike.org
|
||||
freesoftwareextremist.com
|
||||
freespeech.group
|
||||
freespeechextremist.com
|
||||
freetalklive.com
|
||||
froth.zone
|
||||
fulltermprivacy.com
|
||||
gameliberty.club
|
||||
gearlandia.haus
|
||||
genderheretics.xyz
|
||||
geofront.rocks
|
||||
gleasonator.com
|
||||
glee.li
|
||||
glindr.org
|
||||
goyim.app
|
||||
goyslop.cafe
|
||||
haeder.net
|
||||
handholding.io
|
||||
hidamari.apartments
|
||||
hitchhiker.social
|
||||
hunk.city
|
||||
iddqd.social
|
||||
intkos.link
|
||||
justicewarrior.social
|
||||
kawa-kun.com
|
||||
kitsunemimi.club
|
||||
kiwifarms.cc
|
||||
kompost.cz
|
||||
kurosawa.moe
|
||||
leafposter.club
|
||||
leftychan.net
|
||||
lewdieheaven.com
|
||||
liberdon.com
|
||||
ligma.pro
|
||||
lizards.live
|
||||
lolicon.rocks
|
||||
lolison.top
|
||||
lovingexpressions.net
|
||||
lucasvl.nl
|
||||
mahodou.moe
|
||||
makemysarcophagus.com
|
||||
maladaptive.art
|
||||
masochi.st
|
||||
mastinator.com
|
||||
merovingian.club
|
||||
midwaytrades.com
|
||||
mirr0r.city
|
||||
moa.st
|
||||
mouse.services
|
||||
mugicha.club
|
||||
narrativerry.xyz
|
||||
natehiggers.online
|
||||
neckbeard.xyz
|
||||
needs.vodka
|
||||
neenster.org
|
||||
nicecrew.digital
|
||||
nnia.space
|
||||
noagendasocial.com
|
||||
noagendasocial.nl
|
||||
noagendatube.com
|
||||
nobodyhasthe.biz
|
||||
nukem.biz
|
||||
obo.sh
|
||||
onionfarms.org
|
||||
outpoa.st
|
||||
pawlicker.com
|
||||
pawoo.net
|
||||
pedo.school
|
||||
piazza.today
|
||||
pibvt.net
|
||||
pieville.net
|
||||
pisskey.io
|
||||
plagu.ee
|
||||
pmth.us
|
||||
poa.st
|
||||
poast.org
|
||||
poast.tv
|
||||
poster.place
|
||||
prospeech.space
|
||||
quodverum.com
|
||||
rakket.app
|
||||
rapemeat.solutions
|
||||
rdrama.cc
|
||||
rebelbase.site
|
||||
retardedniggers.forsale
|
||||
rojogato.com
|
||||
ryona.agency
|
||||
schwartzwelt.xyz
|
||||
seal.cafe
|
||||
shigusegubu.club
|
||||
shitpost.cloud
|
||||
shitposter.club
|
||||
shota.house
|
||||
silliness.observer
|
||||
skinheads.eu
|
||||
skinheads.io
|
||||
skinheads.social
|
||||
skinheads.uk
|
||||
skippers-bin.com
|
||||
skyshanty.xyz
|
||||
slash.cl
|
||||
sleepy.cafe
|
||||
smuglo.li
|
||||
sneed.social
|
||||
sonichu.com
|
||||
spinster.xyz
|
||||
springbo.cc
|
||||
starnix.network
|
||||
stereophonic.space
|
||||
strelizia.net
|
||||
syspxl.xyz
|
||||
tastingtraffic.net
|
||||
teci.world
|
||||
theapex.social
|
||||
thepostearthdestination.com
|
||||
tkammer.de
|
||||
trumpislovetrumpis.life
|
||||
truthsocial.co.in
|
||||
urchan.org
|
||||
varishangout.net
|
||||
whinge.house
|
||||
whinge.town
|
||||
wideboys.org
|
||||
wolfgirl.bar
|
||||
xn--p1abe3d.xn--80asehdb
|
||||
yggdrasil.social
|
||||
youjo.love
|
||||
zztails.gay
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
public static void exit(Context context) {
|
||||
Intent intent = new Intent(context, ExitActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
@@ -19,6 +20,8 @@ import org.jsoup.internal.StringUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
@@ -30,22 +33,51 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
|
||||
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text);
|
||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
|
||||
boolean isOpenable = isFediUrl || fediHandle.isPresent();
|
||||
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
if(sessions.isEmpty()){
|
||||
if (sessions.isEmpty()){
|
||||
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}else if(sessions.size()==1 && !isMastodonURL){
|
||||
} else if (isOpenable || sessions.size() > 1) {
|
||||
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
|
||||
sheet.setOnClick((accountId, open) -> {
|
||||
if (open && text.isPresent()) {
|
||||
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
|
||||
if (clazz == null) {
|
||||
Toast.makeText(this, R.string.sk_open_in_app_failed, Toast.LENGTH_SHORT).show();
|
||||
// TODO: do something about the window getting leaked
|
||||
sheet.dismiss();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
args.putString("fromExternalShare", clazz.getSimpleName());
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.putExtras(args);
|
||||
finish();
|
||||
startActivity(intent);
|
||||
};
|
||||
|
||||
fediHandle
|
||||
.<MastodonAPIRequest<?>>map(handle ->
|
||||
UiUtils.lookupAccountHandle(this, accountId, handle, callback))
|
||||
.or(() ->
|
||||
UiUtils.lookupURL(this, accountId, text.get(), callback))
|
||||
.ifPresent(req ->
|
||||
req.wrapProgress(this, R.string.loading, true, d -> {
|
||||
UiUtils.transformDialogForLookup(this, accountId, isFediUrl ? text.get() : null, d);
|
||||
d.setOnDismissListener((ev) -> finish());
|
||||
}));
|
||||
} else {
|
||||
openComposeFragment(accountId);
|
||||
}
|
||||
});
|
||||
sheet.show();
|
||||
} else if (sessions.size() == 1) {
|
||||
openComposeFragment(sessions.get(0).getID());
|
||||
}else{
|
||||
new AccountSwitcherSheet(this, false, false, isMastodonURL, accountSession -> {
|
||||
if(accountSession!=null)
|
||||
openComposeFragment(accountSession.getID());
|
||||
else
|
||||
UiUtils.openURL(this, AccountSessionManager.getInstance().getLastActiveAccountID(), text);
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,11 +140,4 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
return null;
|
||||
return new ArrayList<>(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent outContent) {
|
||||
super.onProvideAssistContent(outContent);
|
||||
|
||||
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.os.Build;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
@@ -30,14 +31,14 @@ public class GlobalUserPreferences{
|
||||
public static boolean alwaysExpandContentWarnings;
|
||||
public static boolean disableMarquee;
|
||||
public static boolean disableSwipe;
|
||||
public static boolean disableDividers;
|
||||
public static boolean showDividers;
|
||||
public static boolean voteButtonForSingleChoice;
|
||||
public static boolean uniformNotificationIcon;
|
||||
public static boolean enableDeleteNotifications;
|
||||
public static boolean translateButtonOpenedOnly;
|
||||
public static boolean uniformNotificationIcon;
|
||||
public static boolean relocatePublishButton;
|
||||
public static boolean reduceMotion;
|
||||
public static boolean keepOnlyLatestNotification;
|
||||
public static boolean enableFabAutoHide;
|
||||
public static boolean disableAltTextReminder;
|
||||
public static boolean showAltIndicator;
|
||||
public static boolean showNoAltIndicator;
|
||||
@@ -48,23 +49,28 @@ public class GlobalUserPreferences{
|
||||
public static boolean spectatorMode;
|
||||
public static boolean autoHideFab;
|
||||
public static boolean defaultToUnlistedReplies;
|
||||
public static boolean disableDoubleTapToSwipe;
|
||||
public static boolean doubleTapToSwipe;
|
||||
public static boolean compactReblogReplyLine;
|
||||
public static boolean confirmBeforeReblog;
|
||||
public static boolean replyLineAboveHeader;
|
||||
public static boolean swapBookmarkWithBoostAction;
|
||||
public static boolean loadRemoteAccountFollowers;
|
||||
public static boolean mentionRebloggerAutomatically;
|
||||
public static boolean allowRemoteLoading;
|
||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||
public static String publishButtonText;
|
||||
public static ThemePreference theme;
|
||||
public static ColorPreference color;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
|
||||
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
||||
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
||||
public static Map<String, List<String>> recentLanguages;
|
||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||
public static Set<String> accountsWithLocalOnlySupport;
|
||||
public static Set<String> accountsInGlitchMode;
|
||||
public static Set<String> accountsWithContentTypesEnabled;
|
||||
public static Map<String, ContentType> accountsDefaultContentTypes;
|
||||
|
||||
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
||||
public static Map<String, Integer> recentEmojis;
|
||||
@@ -74,7 +80,6 @@ public class GlobalUserPreferences{
|
||||
*/
|
||||
public static String replyVisibility;
|
||||
|
||||
|
||||
public static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
@@ -85,6 +90,16 @@ public class GlobalUserPreferences{
|
||||
catch (JsonSyntaxException ignored) { return orElse; }
|
||||
}
|
||||
|
||||
public static void removeAccount(String accountId) {
|
||||
recentLanguages.remove(accountId);
|
||||
pinnedTimelines.remove(accountId);
|
||||
accountsInGlitchMode.remove(accountId);
|
||||
accountsWithLocalOnlySupport.remove(accountId);
|
||||
accountsWithContentTypesEnabled.remove(accountId);
|
||||
accountsDefaultContentTypes.remove(accountId);
|
||||
save();
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
@@ -99,13 +114,14 @@ public class GlobalUserPreferences{
|
||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
||||
disableDividers=prefs.getBoolean("disableDividers", true);
|
||||
showDividers =prefs.getBoolean("showDividers", false);
|
||||
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
|
||||
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
||||
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
||||
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
reduceMotion=prefs.getBoolean("reduceMotion", false);
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
enableFabAutoHide=prefs.getBoolean("enableFabAutoHide", true);
|
||||
disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false);
|
||||
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
|
||||
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
|
||||
@@ -117,7 +133,7 @@ public class GlobalUserPreferences{
|
||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
|
||||
disableDoubleTapToSwipe=prefs.getBoolean("disableDoubleTapToSwipe", false);
|
||||
doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true);
|
||||
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
||||
@@ -133,6 +149,10 @@ public class GlobalUserPreferences{
|
||||
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
replyVisibility=prefs.getString("replyVisibility", null);
|
||||
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||
|
||||
try {
|
||||
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
|
||||
@@ -159,13 +179,12 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||
.putBoolean("disableMarquee", disableMarquee)
|
||||
.putBoolean("disableSwipe", disableSwipe)
|
||||
.putBoolean("disableDividers", disableDividers)
|
||||
.putBoolean("showDividers", showDividers)
|
||||
.putBoolean("relocatePublishButton", relocatePublishButton)
|
||||
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
|
||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||
.putBoolean("reduceMotion", reduceMotion)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("enableFabAutoHide", enableFabAutoHide)
|
||||
.putBoolean("disableAltTextReminder", disableAltTextReminder)
|
||||
.putBoolean("showAltIndicator", showAltIndicator)
|
||||
.putBoolean("showNoAltIndicator", showNoAltIndicator)
|
||||
@@ -174,10 +193,11 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||
.putBoolean("spectatorMode", spectatorMode)
|
||||
.putBoolean("autoHideFab", autoHideFab)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putBoolean("bottomEncoding", bottomEncoding)
|
||||
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
|
||||
.putBoolean("disableDoubleTapToSwipe", disableDoubleTapToSwipe)
|
||||
.putBoolean("doubleTapToSwipe", doubleTapToSwipe)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||
@@ -192,6 +212,10 @@ public class GlobalUserPreferences{
|
||||
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
|
||||
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
|
||||
.putString("replyVisibility", replyVisibility)
|
||||
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
||||
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -212,5 +236,10 @@ public class GlobalUserPreferences{
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
}
|
||||
|
||||
public enum AutoRevealMode {
|
||||
NEVER,
|
||||
THREADS,
|
||||
DISCUSSIONS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import static org.joinmastodon.android.fragments.ComposeFragment.CAMERA_PERMISSION_CODE;
|
||||
import static org.joinmastodon.android.fragments.ComposeFragment.CAMERA_PIC_REQUEST_CODE;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PictureTakenEvent;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
@@ -22,13 +32,13 @@ import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity{
|
||||
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
@@ -38,10 +48,18 @@ public class MainActivity extends FragmentStackActivity{
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||
}else{
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
AccountSessionManager.getInstance()
|
||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
||||
showFragmentForExternalShare(intent.getExtras());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
@@ -55,6 +73,7 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
@@ -78,12 +97,12 @@ public class MainActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent){
|
||||
super.onNewIntent(intent);
|
||||
if(intent.getBooleanExtra("fromNotification", false)){
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras());
|
||||
else if (intent.getBooleanExtra("fromNotification", false)) {
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
AccountSession accountSession;
|
||||
try{
|
||||
accountSession=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
DomainManager.getInstance().setCurrentDomain(accountSession.domain);
|
||||
AccountSessionManager.getInstance().getAccount(accountID);
|
||||
}catch(IllegalStateException x){
|
||||
return;
|
||||
}
|
||||
@@ -107,23 +126,24 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
|
||||
private void showFragmentForNotification(Notification notification, String accountID){
|
||||
Fragment fragment;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("_can_go_back", true);
|
||||
try{
|
||||
notification.postprocess();
|
||||
}catch(ObjectValidationException x){
|
||||
Log.w("MainActivity", x);
|
||||
return;
|
||||
}
|
||||
if(notification.status!=null){
|
||||
fragment=new ThreadFragment();
|
||||
args.putParcelable("status", Parcels.wrap(notification.status));
|
||||
}else{
|
||||
fragment=new ProfileFragment();
|
||||
args.putParcelable("profileAccount", Parcels.wrap(notification.account));
|
||||
}
|
||||
UiUtils.showFragmentForNotification(this, notification, accountID, null);
|
||||
}
|
||||
|
||||
private void showFragmentForExternalShare(Bundle args) {
|
||||
String className = args.getString("fromExternalShare");
|
||||
Fragment fragment = switch (className) {
|
||||
case "ThreadFragment" -> new ThreadFragment();
|
||||
case "ProfileFragment" -> new ProfileFragment();
|
||||
default -> null;
|
||||
};
|
||||
if (fragment == null) return;
|
||||
args.putBoolean("_can_go_back", true);
|
||||
fragment.setArguments(args);
|
||||
showFragment(fragment);
|
||||
}
|
||||
@@ -157,25 +177,61 @@ public class MainActivity extends FragmentStackActivity{
|
||||
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
|
||||
);
|
||||
Bundle currentArgs = currentFragment.getArguments();
|
||||
if (this.fragmentContainers.size() == 1
|
||||
&& currentArgs != null
|
||||
&& currentArgs.getBoolean("_can_go_back", false)
|
||||
&& currentArgs.containsKey("account")) {
|
||||
if (fragmentContainers.size() != 1
|
||||
|| currentArgs == null
|
||||
|| !currentArgs.getBoolean("_can_go_back", false)) {
|
||||
super.onBackPressed();
|
||||
return;
|
||||
}
|
||||
if (currentArgs.getBoolean("_finish_on_back", false)) {
|
||||
finish();
|
||||
} else if (currentArgs.containsKey("account")) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString("account", currentArgs.getString("account"));
|
||||
args.putString("tab", "notifications");
|
||||
if (getIntent().getBooleanExtra("fromNotification", false)) {
|
||||
args.putString("tab", "notifications");
|
||||
}
|
||||
Fragment fragment=new HomeFragment();
|
||||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent outContent) {
|
||||
super.onProvideAssistContent(outContent);
|
||||
|
||||
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(requestCode==CAMERA_PIC_REQUEST_CODE && resultCode== Activity.RESULT_OK){
|
||||
Bitmap image = (Bitmap) data.getExtras().get("data");
|
||||
String path = MediaStore.Images.Media.insertImage(this.getContentResolver(), image, null, null);
|
||||
E.post(new PictureTakenEvent(Uri.parse(path)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == CAMERA_PERMISSION_CODE && (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
startActivityForResult(cameraIntent, CAMERA_PIC_REQUEST_CODE);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.permission_required, Toast.LENGTH_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
public Fragment getCurrentFragment() {
|
||||
for (int i = fragmentContainers.size() - 1; i >= 0; i--) {
|
||||
FrameLayout fl = fragmentContainers.get(i);
|
||||
if (fl.getVisibility() == View.VISIBLE) {
|
||||
return getFragmentManager().findFragmentById(fl.getId());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
super.onProvideAssistContent(assistContent);
|
||||
Fragment fragment = getCurrentFragment();
|
||||
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
|
||||
@Override
|
||||
public void onSuccess(Token token){
|
||||
new GetOwnAccount()
|
||||
// in case the instance (looking at pixelfed) wants to redirect to a
|
||||
// website, we need to pass a context so we can launch a browser
|
||||
.setContext(OAuthActivity.this)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account account){
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
|
||||
public class PanicResponderActivity extends Activity {
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Intent intent = getIntent();
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
AccountSessionManager.getInstance().getLoggedInAccounts().forEach(accountSession -> logOut(accountSession.getID()));
|
||||
ExitActivity.exit(this);
|
||||
}
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
private void logOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
||||
@@ -28,6 +29,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.NotificationAction;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
@@ -37,6 +39,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@@ -56,7 +59,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
private static final String ACTION_KEY_TEXT_REPLY = "ACTION_KEY_TEXT_REPLY";
|
||||
|
||||
private static final int SUMMARY_ID = 791;
|
||||
private static int notificationId;
|
||||
private static int notificationId = 0;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
@@ -123,8 +126,16 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
|
||||
if(intent.hasExtra("notification")){
|
||||
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
String statusID=notification.status.id;
|
||||
if (statusID != null) {
|
||||
String statusID = null;
|
||||
String targetAccountID = null;
|
||||
|
||||
if(notification.status != null){
|
||||
statusID = notification.status.id;
|
||||
}
|
||||
if(notification.account != null){
|
||||
targetAccountID = notification.account.id;
|
||||
}
|
||||
if (statusID != null || targetAccountID != null) {
|
||||
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
||||
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
||||
|
||||
@@ -134,6 +145,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
case BOOST -> new SetStatusReblogged(notification.status.id, true, preferences.postingDefaultVisibility).exec(accountID);
|
||||
case UNBOOST -> new SetStatusReblogged(notification.status.id, false, preferences.postingDefaultVisibility).exec(accountID);
|
||||
case REPLY -> handleReplyAction(context, accountID, intent, notification, notificationId, preferences);
|
||||
case FOLLOW_BACK -> new SetAccountFollowed(notification.account.id, true, true, false).exec(accountID);
|
||||
default -> Log.w(TAG, "onReceive: Failed to get NotificationAction");
|
||||
}
|
||||
}
|
||||
@@ -241,6 +253,9 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
if(notification.status.reblogged)
|
||||
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.sk_undo_reblog), NotificationAction.UNBOOST));
|
||||
}
|
||||
case FOLLOW -> {
|
||||
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.follow_back), NotificationAction.FOLLOW_BACK));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,27 +300,60 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
}
|
||||
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
|
||||
|
||||
// copied from ComposeFragment - TODO: generalize?
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
Status status = notification.status;
|
||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!status.account.id.equals(ownID))
|
||||
mentions.add('@'+status.account.acct);
|
||||
for(Mention mention:status.mentions){
|
||||
if(mention.id.equals(ownID))
|
||||
continue;
|
||||
String m='@'+mention.acct;
|
||||
if(!mentions.contains(m))
|
||||
mentions.add(m);
|
||||
}
|
||||
String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
|
||||
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
req.status = input.toString() + "\n\n" + "@" + notification.status.account.acct;
|
||||
req.language = notification.status.language;
|
||||
req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility);
|
||||
req.status = initialText + input.toString();
|
||||
req.language = preferences.postingDefaultLanguage;
|
||||
req.visibility = preferences.postingDefaultVisibility;
|
||||
req.inReplyToId = notification.status.id;
|
||||
if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){
|
||||
req.spoilerText = "re: " + notification.status.spoilerText;
|
||||
}
|
||||
|
||||
new CreateStatus(req, UUID.randomUUID().toString()).exec(accountID);
|
||||
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Status status) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
|
||||
new Notification.Builder(context, accountID+"_"+notification.type) :
|
||||
new Notification.Builder(context)
|
||||
.setPriority(Notification.PRIORITY_DEFAULT)
|
||||
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
|
||||
new Notification.Builder(context, accountID+"_"+notification.type) :
|
||||
new Notification.Builder(context)
|
||||
.setPriority(Notification.PRIORITY_DEFAULT)
|
||||
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
|
||||
notification.status = status;
|
||||
Intent contentIntent=new Intent(context, MainActivity.class);
|
||||
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
contentIntent.putExtra("fromNotification", true);
|
||||
contentIntent.putExtra("accountID", accountID);
|
||||
contentIntent.putExtra("notification", Parcels.wrap(notification));
|
||||
|
||||
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setContentText(context.getString(R.string.mo_notification_action_replied, notification.status.account.getDisplayUsername()))
|
||||
.build();
|
||||
notificationManager.notify(accountID, notificationId, repliedNotification);
|
||||
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setContentTitle(context.getString(R.string.sk_notification_action_replied, notification.status.account.displayName))
|
||||
.setContentText(status.getStrippedText())
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setContentIntent(PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build();
|
||||
notificationManager.notify(accountID, notificationId, repliedNotification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse errorResponse) {
|
||||
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
@@ -37,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=3;
|
||||
private static final int DB_VERSION=4;
|
||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
@@ -62,7 +61,7 @@ public class CacheController{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Status> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
@@ -74,10 +73,8 @@ public class CacheController{
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
|
||||
continue outer;
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
@@ -92,7 +89,7 @@ public class CacheController{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -115,7 +112,7 @@ public class CacheController{
|
||||
runOnDbThread((db)->{
|
||||
if(clear)
|
||||
db.delete("home_timeline", null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Status s:posts){
|
||||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
@@ -123,6 +120,7 @@ public class CacheController{
|
||||
if(s.hasGapAfter)
|
||||
flags|=POST_FLAG_GAP_AFTER;
|
||||
values.put("flags", flags);
|
||||
values.put("time", s.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
@@ -137,7 +135,7 @@ public class CacheController{
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Notification> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
@@ -148,10 +146,8 @@ public class CacheController{
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
@@ -164,17 +160,13 @@ public class CacheController{
|
||||
}
|
||||
}
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
@@ -201,7 +193,7 @@ public class CacheController{
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
if(clear)
|
||||
db.delete(table, null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Notification n:notifications){
|
||||
if(n.type==null){
|
||||
continue;
|
||||
@@ -209,6 +201,7 @@ public class CacheController{
|
||||
values.put("id", n.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||
values.put("type", n.type.ordinal());
|
||||
values.put("time", n.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
@@ -305,21 +298,24 @@ public class CacheController{
|
||||
CREATE TABLE `home_timeline` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_all` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_mentions` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
createRecentSearchesTable(db);
|
||||
createPostsNotificationsTable(db);
|
||||
@@ -327,12 +323,16 @@ public class CacheController{
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||
if(oldVersion==1){
|
||||
if(oldVersion<2){
|
||||
createRecentSearchesTable(db);
|
||||
}
|
||||
if(oldVersion==2){
|
||||
if(oldVersion<3){
|
||||
// MEGALODON-SPECIFIC
|
||||
createPostsNotificationsTable(db);
|
||||
}
|
||||
if(oldVersion<4){
|
||||
addTimeColumns(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||
@@ -350,9 +350,21 @@ public class CacheController{
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
}
|
||||
|
||||
private void addTimeColumns(SQLiteDatabase db){
|
||||
db.execSQL("DELETE FROM `home_timeline`");
|
||||
db.execSQL("DELETE FROM `notifications_all`");
|
||||
db.execSQL("DELETE FROM `notifications_mentions`");
|
||||
db.execSQL("DELETE FROM `notifications_posts`");
|
||||
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_posts` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -28,6 +29,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;
|
||||
@@ -51,7 +53,9 @@ public class MastodonAPIController{
|
||||
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
|
||||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
private AccountSession session;
|
||||
private static List<String> badDomains = new ArrayList<>();
|
||||
@@ -60,7 +64,7 @@ public class MastodonAPIController{
|
||||
thread.start();
|
||||
try {
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||
MastodonApp.context.getAssets().open("blocks.tsv")
|
||||
MastodonApp.context.getAssets().open("blocks.txt")
|
||||
));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
@@ -91,7 +95,7 @@ public class MastodonAPIController{
|
||||
Request.Builder builder=new Request.Builder()
|
||||
.url(req.getURL().toString())
|
||||
.method(req.getMethod(), req.getRequestBody())
|
||||
.header("User-Agent", "MastodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
.header("User-Agent", "MoshidonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
|
||||
String token=null;
|
||||
if(session!=null)
|
||||
@@ -158,6 +162,11 @@ public class MastodonAPIController{
|
||||
respObj=gson.fromJson(reader, req.respClass);
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if (req.context != null && response.body().contentType().subtype().equals("html")) {
|
||||
UiUtils.launchWebBrowser(req.context, response.request().url().toString());
|
||||
req.cancel();
|
||||
return;
|
||||
}
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@@ -20,9 +21,11 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -44,10 +47,11 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
TypeToken<T> respTypeToken;
|
||||
Call okhttpCall;
|
||||
Token token;
|
||||
boolean canceled;
|
||||
boolean canceled, isRemote;
|
||||
Map<String, String> headers;
|
||||
private ProgressDialog progressDialog;
|
||||
protected boolean removeUnsupportedItems;
|
||||
@Nullable Context context;
|
||||
|
||||
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
||||
this.path=path;
|
||||
@@ -101,6 +105,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> execRemote(String domain) {
|
||||
return execRemote(domain, null);
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> execRemote(String domain, @Nullable AccountSession remoteSession) {
|
||||
this.isRemote = true;
|
||||
return Optional.ofNullable(remoteSession)
|
||||
.or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream()
|
||||
.filter(acc -> acc.domain.equals(domain))
|
||||
.findAny())
|
||||
.map(AccountSession::getID)
|
||||
.map(this::exec)
|
||||
.orElse(this.execNoAuth(domain));
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||
return wrapProgress(activity, message, cancelable, null);
|
||||
}
|
||||
@@ -164,9 +183,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> setContext(Context context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
||||
if(respObj instanceof BaseModel){
|
||||
((BaseModel) respObj).isRemote = isRemote;
|
||||
((BaseModel) respObj).postprocess();
|
||||
}else if(respObj instanceof List){
|
||||
if(removeUnsupportedItems){
|
||||
@@ -175,6 +205,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
Object item=itr.next();
|
||||
if(item instanceof BaseModel){
|
||||
try{
|
||||
((BaseModel) item).isRemote = isRemote;
|
||||
((BaseModel) item).postprocess();
|
||||
}catch(ObjectValidationException x){
|
||||
Log.w(TAG, "Removing invalid object from list", x);
|
||||
@@ -182,15 +213,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
}
|
||||
}
|
||||
// no idea why we're post-processing twice, but well, as long
|
||||
// as upstream does it like this, i don't wanna break anything
|
||||
for(Object item:((List<?>) respObj)){
|
||||
if(item instanceof BaseModel){
|
||||
((BaseModel) item).isRemote = isRemote;
|
||||
((BaseModel) item).postprocess();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
for(Object item:((List<?>) respObj)){
|
||||
if(item instanceof BaseModel)
|
||||
if(item instanceof BaseModel) {
|
||||
((BaseModel) item).isRemote = isRemote;
|
||||
((BaseModel) item).postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class StatusInteractionController{
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
runningFavoriteRequests.remove(status.id);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class StatusInteractionController{
|
||||
public void onSuccess(Status reblog){
|
||||
Status result = reblog.getContentStatus();
|
||||
runningReblogRequests.remove(status.id);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetAccountByHandle extends MastodonAPIRequest<Account>{
|
||||
/**
|
||||
* note that this method usually only returns a result if the instance already knows about an
|
||||
* account - so it makes sense for looking up local users, search might be preferred otherwise
|
||||
*/
|
||||
public GetAccountByHandle(String acct){
|
||||
super(HttpMethod.GET, "/accounts/lookup", Account.class);
|
||||
addQueryParameter("acct", acct);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.DomainBlock;
|
||||
import org.joinmastodon.android.model.ExtendedDescription;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetDomainBlocks extends MastodonAPIRequest<List<DomainBlock>>{
|
||||
public GetDomainBlocks(){
|
||||
super(HttpMethod.GET, "/instance/domain_blocks", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ExtendedDescription;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
|
||||
public class GetExtendedDescription extends MastodonAPIRequest<ExtendedDescription>{
|
||||
public GetExtendedDescription(){
|
||||
super(HttpMethod.GET, "/instance/extended_description", ExtendedDescription.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.WeeklyActivity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetWeeklyActivity extends MastodonAPIRequest<List<WeeklyActivity>>{
|
||||
public GetWeeklyActivity(){
|
||||
super(HttpMethod.GET, "/instance/activity", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
|
||||
private String maxID;
|
||||
public PleromaMarkNotificationsRead(String maxID) {
|
||||
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
|
||||
this.maxID = maxID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() {
|
||||
MultipartBody.Builder builder=new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
builder.addFormDataPart("max_id", maxID);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
@@ -46,6 +47,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public String language;
|
||||
|
||||
public String quoteId;
|
||||
public ContentType contentType;
|
||||
|
||||
public static class Poll{
|
||||
public ArrayList<String> options=new ArrayList<>();
|
||||
|
||||
@@ -2,17 +2,22 @@ package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
|
||||
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
|
||||
public GetStatusSourceText(String id){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
|
||||
}
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public static class Response extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String text;
|
||||
@RequiredField
|
||||
public String spoilerText;
|
||||
public ContentType contentType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetBubbleTimeline(String maxID, int limit) {
|
||||
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
@@ -7,6 +9,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@@ -14,6 +17,7 @@ import org.joinmastodon.android.model.Token;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AccountSession{
|
||||
public Token token;
|
||||
@@ -87,4 +91,15 @@ public class AccountSession{
|
||||
pushSubscriptionManager=new PushSubscriptionManager(getID());
|
||||
return pushSubscriptionManager;
|
||||
}
|
||||
|
||||
public Optional<Instance> getInstance() {
|
||||
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
|
||||
}
|
||||
|
||||
public Uri getInstanceUri() {
|
||||
return new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -121,6 +122,12 @@ public class AccountSessionManager{
|
||||
sessions.put(session.getID(), session);
|
||||
lastActiveAccountID=session.getID();
|
||||
writeAccountsFile();
|
||||
|
||||
// write initial instance info to file immediately to avoid sessions without instance info
|
||||
InstanceInfoStorageWrapper wrapper = new InstanceInfoStorageWrapper();
|
||||
wrapper.instance = instance;
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
||||
|
||||
updateMoreInstanceInfo(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
@@ -129,14 +136,16 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public synchronized void writeAccountsFile(){
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~");
|
||||
File file = new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
try{
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
try(FileOutputStream out=new FileOutputStream(tmpFile)){
|
||||
SessionsStorageWrapper w=new SessionsStorageWrapper();
|
||||
w.accounts=new ArrayList<>(sessions.values());
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(w, writer);
|
||||
writer.flush();
|
||||
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing accounts file", x);
|
||||
@@ -162,6 +171,11 @@ public class AccountSessionManager{
|
||||
return sessions.get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(Account account) {
|
||||
return sessions.get(account.getDomainFromURL() + "_" + account.id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession getLastActiveAccount(){
|
||||
if(sessions.isEmpty() || lastActiveAccountID==null)
|
||||
@@ -189,6 +203,7 @@ public class AccountSessionManager{
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
GlobalUserPreferences.removeAccount(id);
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
if(sessions.isEmpty())
|
||||
@@ -259,31 +274,35 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public void maybeUpdateLocalInfo(){
|
||||
maybeUpdateLocalInfo(null);
|
||||
}
|
||||
|
||||
public void maybeUpdateLocalInfo(AccountSession activeSession){
|
||||
long now=System.currentTimeMillis();
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
for(AccountSession session:sessions.values()){
|
||||
domains.add(session.domain.toLowerCase());
|
||||
// if(now-session.infoLastUpdated>24L*3600_000L){
|
||||
updateSessionPreferences(session);
|
||||
updateSessionLocalInfo(session);
|
||||
// }
|
||||
// if(now-session.filtersLastUpdated>3600_000L){
|
||||
updateSessionWordFilters(session);
|
||||
// }
|
||||
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
|
||||
updateSessionPreferences(session);
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
|
||||
updateSessionWordFilters(session);
|
||||
}
|
||||
updateSessionMarkers(session);
|
||||
}
|
||||
if(loadedInstances){
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateCustomEmojis(Set<String> domains){
|
||||
private void maybeUpdateCustomEmojis(Set<String> domains, String activeDomain){
|
||||
long now=System.currentTimeMillis();
|
||||
for(String domain:domains){
|
||||
// Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||
updateInstanceInfo(domain);
|
||||
// }
|
||||
Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
|
||||
updateInstanceInfo(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +430,9 @@ public class AccountSessionManager{
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
|
||||
wrapper.instance = instance;
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
@@ -422,10 +443,13 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
|
||||
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
|
||||
File file = getInstanceInfoFile(domain);
|
||||
File tmpFile = new File(file.getPath() + "~");
|
||||
try(FileOutputStream out=new FileOutputStream(tmpFile)){
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(emojis, writer);
|
||||
writer.flush();
|
||||
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "Error writing instance info file for "+domain, x);
|
||||
}
|
||||
@@ -445,7 +469,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
if(!loadedInstances){
|
||||
loadedInstances=true;
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
maybeUpdateCustomEmojis(domains, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,10 +493,6 @@ public class AccountSessionManager{
|
||||
return instances.get(domain);
|
||||
}
|
||||
|
||||
public Instance getInstanceInfoForAccount(String account) {
|
||||
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
|
||||
}
|
||||
|
||||
public void updateAccountInfo(String id, Account account){
|
||||
AccountSession session=getAccount(id);
|
||||
session.self=account;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
|
||||
public class PictureTakenEvent {
|
||||
public Uri uri;
|
||||
|
||||
public PictureTakenEvent(Uri uri){
|
||||
this.uri = uri;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
@@ -23,6 +20,7 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
@@ -64,7 +62,12 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList());
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
result=result.stream().filter(status -> {
|
||||
// don't hide own posts in own profile
|
||||
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
|
||||
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
|
||||
}).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -85,7 +88,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user))
|
||||
return;
|
||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||
@@ -93,10 +97,11 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
return;
|
||||
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
||||
if(ev.status.mediaAttachments.isEmpty())
|
||||
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
|
||||
return;
|
||||
}
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
if (isOnTop()) scrollToTop();
|
||||
}
|
||||
|
||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||
@@ -123,4 +128,19 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
// could return different uris based on filter (e.g. media -> "/media"), but i want to
|
||||
// return the remote url to the user, and i don't know whether i'd need to append
|
||||
// '#media' (akkoma/pleroma) or '/media' (glitch/mastodon) since i don't know anything
|
||||
// about the remote instance. so, just returning the base url to the user instead
|
||||
return Uri.parse(user.url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -103,4 +104,9 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? base.path("/announcements").build() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
@@ -13,7 +14,6 @@ import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
@@ -21,14 +21,15 @@ import android.view.animation.TranslateAnimation;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
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.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
@@ -37,6 +38,7 @@ import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
@@ -49,7 +51,7 @@ 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.ui.views.MediaGridLayout;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -74,7 +76,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, DomainDisplay{
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||
protected DisplayItemsAdapter adapter;
|
||||
protected String accountID;
|
||||
@@ -85,22 +87,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected Rect tmpRect=new Rect();
|
||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||
|
||||
private final int THRESHOLD = 800;
|
||||
protected boolean currentlyScrolling;
|
||||
|
||||
public BaseStatusListFragment(){
|
||||
super(20);
|
||||
if (withComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
if (wantsComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
protected boolean withComposeButton() {
|
||||
protected boolean wantsComposeButton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
UiUtils.loadMaxWidth(getContext());
|
||||
if(GlobalUserPreferences.disableMarquee){
|
||||
setTitleMarqueeEnabled(false);
|
||||
setSubtitleMarqueeEnabled(false);
|
||||
@@ -109,8 +109,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return adapter=new DisplayItemsAdapter();
|
||||
@@ -139,7 +137,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
displayItems.clear();
|
||||
}
|
||||
|
||||
protected void prependItems(List<T> items, boolean notify){
|
||||
protected int prependItems(List<T> items, boolean notify){
|
||||
data.addAll(0, items);
|
||||
int offset=0;
|
||||
for(T s:items){
|
||||
@@ -152,6 +150,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
if(notify)
|
||||
adapter.notifyItemRangeInserted(0, offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected String getMaxID(){
|
||||
@@ -212,7 +211,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||
MediaAttachmentViewController holder=findPhotoViewHolder(index);
|
||||
if(holder!=null){
|
||||
if(holder!=null && list!=null){
|
||||
transitioningHolder=holder;
|
||||
View view=transitioningHolder.photo;
|
||||
int[] pos={0, 0};
|
||||
@@ -278,6 +277,45 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getFab() {
|
||||
if (getParentFragment() instanceof HasFab l) return l.getFab();
|
||||
else return fab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFab() {
|
||||
View fab = getFab();
|
||||
if (fab == null || fab.getVisibility() == View.VISIBLE) return;
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
}
|
||||
|
||||
public boolean isScrolling() {
|
||||
return currentlyScrolling;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideFab() {
|
||||
View fab = getFab();
|
||||
if (fab == null || fab.getVisibility() != View.VISIBLE) return;
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
scrollDiff = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -289,52 +327,34 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
if(currentPhotoViewer!=null)
|
||||
currentPhotoViewer.offsetView(-dx, -dy);
|
||||
|
||||
if (fab!=null && GlobalUserPreferences.enableFabAutoHide) {
|
||||
// This piece of code should make it so that the fab is always visible if the status list scroll view is at the item at the top
|
||||
if(list.getChildAt(0).getTop() == 0){
|
||||
scrollDiff= THRESHOLD +1;
|
||||
}else{
|
||||
if(dy > 0){
|
||||
scrollDiff=0;
|
||||
}
|
||||
}
|
||||
|
||||
View fab = getFab();
|
||||
if (fab!=null && GlobalUserPreferences.autoHideFab && dy != UiUtils.SCROLL_TO_TOP_DELTA) {
|
||||
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
// animate.setFillAfter(true);
|
||||
fab.startAnimation(animate);
|
||||
fab.setEnabled(false);
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
scrollDiff = 0;
|
||||
hideFab();
|
||||
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
||||
if (scrollDiff > THRESHOLD) {
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
// animate.setFillAfter(true);
|
||||
fab.startAnimation(animate);
|
||||
fab.setEnabled(true);
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) {
|
||||
showFab();
|
||||
scrollDiff = 0;
|
||||
} else {
|
||||
scrollDiff += Math.abs(dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
super.onScrollStateChanged(recyclerView, newState);
|
||||
currentlyScrolling = newState != RecyclerView.SCROLL_STATE_IDLE;
|
||||
}
|
||||
});
|
||||
list.addItemDecoration(new StatusListItemDecoration());
|
||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||
private Rect tmpRect=new Rect();
|
||||
@Override
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||
int lastIndex = -1, firstIndex = -1;
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
@@ -346,29 +366,54 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
View child=list.getChildAt(i);
|
||||
holder=list.getChildViewHolder(child);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> h){
|
||||
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
|
||||
if(otherID.equals(id)){
|
||||
if (firstIndex < 0) firstIndex = i;
|
||||
lastIndex = i;
|
||||
StatusDisplayItem item = h.getItem();
|
||||
hasDescendant = item.hasDescendantNeighbor;
|
||||
// no for direct descendants because main status (right above) is
|
||||
// being displayed with an extended footer - no connected layout
|
||||
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
|
||||
list.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
outRect.left=Math.min(outRect.left, tmpRect.left);
|
||||
outRect.top=Math.min(outRect.top, tmpRect.top);
|
||||
outRect.right=Math.max(outRect.right, tmpRect.right);
|
||||
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
|
||||
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
|
||||
isWarning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// shifting the selection box down
|
||||
// see also: FooterStatusDisplayItem#onBind (setMargins)
|
||||
if (isWarning || firstIndex < 0 || lastIndex < 0 ||
|
||||
!(list.getChildViewHolder(list.getChildAt(lastIndex))
|
||||
instanceof FooterStatusDisplayItem.Holder)) return;
|
||||
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
|
||||
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
|
||||
list.getChildViewHolder(list.getChildAt(prevIndex))
|
||||
instanceof WarningFilteredStatusDisplayItem.Holder;
|
||||
boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() &&
|
||||
list.getChildViewHolder(list.getChildAt(nextIndex))
|
||||
instanceof WarningFilteredStatusDisplayItem.Holder;
|
||||
if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4);
|
||||
if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4);
|
||||
}
|
||||
});
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
|
||||
updateToolbar();
|
||||
|
||||
if (withComposeButton()) {
|
||||
fab = view.findViewById(R.id.fab);
|
||||
if (wantsComposeButton() && !getArguments().getBoolean("__disable_fab", false)) {
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(this::onFabLongClick);
|
||||
} else if (fab != null) {
|
||||
fab.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,26 +502,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(holder.getItem().status.reloadWhenClicked){
|
||||
Status queryStatus = holder.getItem().status;
|
||||
UiUtils.lookupStatus(getContext(), queryStatus, accountID, null, status -> {
|
||||
submitPollVote(holder.getItemID(), status.poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
|
||||
});
|
||||
return;
|
||||
}
|
||||
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||
}
|
||||
}
|
||||
|
||||
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
|
||||
Poll poll=holder.getItem().poll;
|
||||
if(holder.getItem().status.reloadWhenClicked){
|
||||
Status queryStatus = holder.getItem().status;
|
||||
UiUtils.lookupStatus(getContext(), queryStatus, accountID, null, status -> {
|
||||
submitPollVote(holder.getItemID(), status.poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
|
||||
});
|
||||
return;
|
||||
}
|
||||
submitPollVote(holder.getItemID(), poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@@ -565,6 +596,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
|
||||
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
|
||||
holder.rebind();
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
|
||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||
@@ -576,6 +615,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
warning.getItem().status.filterRevealed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID(){
|
||||
return accountID;
|
||||
}
|
||||
@@ -646,8 +686,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return list.getChildAt(0) == null || list.getChildAt(0).getTop() == 0;
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
protected int getListWidthForMediaLayout(){
|
||||
@@ -696,13 +736,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
currentPhotoViewer.onPause();
|
||||
}
|
||||
|
||||
protected void onFabClick(View v){
|
||||
public void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
protected boolean onFabLongClick(View v) {
|
||||
public boolean onFabLongClick(View v) {
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID);
|
||||
}
|
||||
|
||||
@@ -714,6 +754,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDataLoaded(List<T> d, boolean more) {
|
||||
super.onDataLoaded(d, more);
|
||||
// more available, but the page isn't even full yet? seems wrong, let's load some more
|
||||
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
@@ -761,7 +812,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
private int currentMediaHiddenLayoutsWidth=0;
|
||||
|
||||
{
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.disableDividers ? R.attr.colorWindowBackground : R.attr.colorPollVoted));
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.showDividers ? R.attr.colorPollVoted : R.attr.colorWindowBackground));
|
||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||
dividerPaint.setStrokeWidth(V.dp(1));
|
||||
}
|
||||
@@ -775,6 +826,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
||||
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
||||
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
|
||||
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -35,4 +37,14 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/bookmarks").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static android.os.ext.SdkExtensions.getExtensionVersion;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
|
||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
|
||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
|
||||
@@ -9,6 +7,7 @@ import static org.joinmastodon.android.ui.utils.UiUtils.isPhotoPickerAvailable;
|
||||
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
|
||||
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
@@ -17,6 +16,7 @@ import android.app.TimePickerDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -29,7 +29,6 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.icu.text.BreakIterator;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.opengl.Visibility;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
@@ -70,7 +69,10 @@ import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.github.bottomSoftwareFoundation.bottom.Bottom;
|
||||
import com.squareup.otto.Subscribe;
|
||||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -87,13 +89,17 @@ import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PictureTakenEvent;
|
||||
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsBaseFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
@@ -112,6 +118,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
|
||||
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.TransferSpeedTracker;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ComposeEditText;
|
||||
@@ -151,7 +158,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
@@ -160,6 +167,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private static final String GLITCH_LOCAL_ONLY_SUFFIX = "👁";
|
||||
private static final Pattern GLITCH_LOCAL_ONLY_PATTERN = Pattern.compile("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*");
|
||||
private static final String TAG="ComposeFragment";
|
||||
public static final int CAMERA_PERMISSION_CODE = 626938;
|
||||
public static final int CAMERA_PIC_REQUEST_CODE = 6242069;
|
||||
|
||||
public static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@@ -182,8 +191,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private int charCount, charLimit, trimmedCharCount;
|
||||
|
||||
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
|
||||
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
|
||||
private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss, contentTypeBtn;
|
||||
private ImageView sensitiveIcon;
|
||||
private ComposeMediaLayout attachmentsView;
|
||||
private TextView replyText;
|
||||
@@ -237,6 +246,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private Runnable updateUploadEtaRunnable;
|
||||
|
||||
private String language, encoding;
|
||||
private ContentType contentType;
|
||||
private MastodonLanguage.LanguageResolver languageResolver;
|
||||
|
||||
private int navigationBarColorBefore;
|
||||
@@ -244,11 +254,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
setRetainInstance(true);
|
||||
navigationBarColorBefore = getActivity().getWindow().getNavigationBarColor();
|
||||
getActivity().getWindow().setNavigationBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLightest));
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
|
||||
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
self=session.self;
|
||||
instanceDomain=session.domain;
|
||||
@@ -266,9 +279,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Nav.finish(this);
|
||||
return;
|
||||
}
|
||||
if(customEmojis.isEmpty()){
|
||||
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
|
||||
}
|
||||
|
||||
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
|
||||
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
|
||||
@@ -285,6 +295,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.isUploadingOrProcessing())
|
||||
att.cancelUpload();
|
||||
@@ -353,6 +364,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
emojiBtn=view.findViewById(R.id.btn_emoji);
|
||||
spoilerBtn=view.findViewById(R.id.btn_spoiler);
|
||||
visibilityBtn=view.findViewById(R.id.btn_visibility);
|
||||
contentTypeBtn=view.findViewById(R.id.btn_content_type);
|
||||
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
|
||||
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
|
||||
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
|
||||
@@ -363,19 +375,29 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
view.findViewById(GlobalUserPreferences.replyLineAboveHeader ? R.id.reply_text_below : R.id.reply_text)
|
||||
.setVisibility(View.GONE);
|
||||
|
||||
if (isPhotoPickerAvailable()) {
|
||||
PopupMenu attachPopup = new PopupMenu(getContext(), mediaBtn);
|
||||
attachPopup.inflate(R.menu.attach);
|
||||
attachPopup.setOnMenuItemClickListener(i -> {
|
||||
PopupMenu attachPopup = new PopupMenu(getContext(), mediaBtn);
|
||||
attachPopup.inflate(R.menu.attach);
|
||||
if(isPhotoPickerAvailable())
|
||||
attachPopup.getMenu().findItem(R.id.media).setVisible(true);
|
||||
|
||||
attachPopup.setOnMenuItemClickListener(i -> {
|
||||
if (i.getItemId() == R.id.camera){
|
||||
if (getContext().checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
startActivityForResult(cameraIntent, CAMERA_PIC_REQUEST_CODE);
|
||||
} else {
|
||||
getActivity().requestPermissions(new String[] { Manifest.permission.CAMERA }, CAMERA_PERMISSION_CODE);
|
||||
}
|
||||
|
||||
} else {
|
||||
openFilePicker(i.getItemId() == R.id.media);
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getContext(), attachPopup);
|
||||
mediaBtn.setOnClickListener(v->attachPopup.show());
|
||||
mediaBtn.setOnTouchListener(attachPopup.getDragToOpenListener());
|
||||
} else {
|
||||
mediaBtn.setOnClickListener(v -> openFilePicker(false));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getContext(), attachPopup);
|
||||
mediaBtn.setOnClickListener(v->attachPopup.show());
|
||||
mediaBtn.setOnTouchListener(attachPopup.getDragToOpenListener());
|
||||
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
|
||||
pollBtn.setOnClickListener(v->togglePoll());
|
||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||
@@ -387,6 +409,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
|
||||
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
|
||||
|
||||
buildContentTypePopup(contentTypeBtn);
|
||||
contentTypeBtn.setOnClickListener(v->contentTypePopup.show());
|
||||
contentTypeBtn.setOnTouchListener(contentTypePopup.getDragToOpenListener());
|
||||
|
||||
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
|
||||
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
|
||||
|
||||
@@ -489,8 +515,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
if(editingStatus!=null && editingStatus.visibility!=null) {
|
||||
statusVisibility=editingStatus.visibility;
|
||||
if (savedInstanceState != null) {
|
||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
} else if (editingStatus != null && editingStatus.visibility != null) {
|
||||
statusVisibility = editingStatus.visibility;
|
||||
} else {
|
||||
loadDefaultStatusVisibility(savedInstanceState);
|
||||
}
|
||||
@@ -505,6 +533,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}).setChecked(true);
|
||||
visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly);
|
||||
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey("contentType")) {
|
||||
contentType = (ContentType) savedInstanceState.getSerializable("contentType");
|
||||
} else if (getArguments().containsKey("sourceContentType")) {
|
||||
try {
|
||||
String val = getArguments().getString("sourceContentType");
|
||||
contentType = val == null ? null : ContentType.valueOf(val);
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
|
||||
int contentTypeId = ContentType.getContentTypeRes(contentType);
|
||||
contentTypePopup.getMenu().findItem(contentTypeId).setChecked(true);
|
||||
contentTypeBtn.setSelected(contentTypeId != R.id.content_type_null && contentTypeId != R.id.content_type_plain);
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
@@ -541,10 +583,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
outState.putParcelableArrayList("attachments", serializedAttachments);
|
||||
}
|
||||
outState.putSerializable("visibility", statusVisibility);
|
||||
outState.putSerializable("contentType", contentType);
|
||||
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
|
||||
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == CAMERA_PERMISSION_CODE && (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
startActivityForResult(cameraIntent, CAMERA_PIC_REQUEST_CODE);
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.permission_required, Toast.LENGTH_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(){
|
||||
super.onResume();
|
||||
@@ -579,8 +635,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
if(s.length()==0)
|
||||
if(s.length()==0){
|
||||
updateCharCounter();
|
||||
return;
|
||||
}
|
||||
int start=lastChangeStart;
|
||||
int count=lastChangeCount;
|
||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||
@@ -863,12 +921,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
||||
buildLanguageSelector(languageButton);
|
||||
|
||||
if (editingStatus != null && scheduledStatus == null) {
|
||||
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
|
||||
// editing an already published post
|
||||
draftsBtn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
private void navigateToUnsentPosts() {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -948,6 +1012,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
});
|
||||
}
|
||||
|
||||
private int getContentTypeName(String id) {
|
||||
return switch (id) {
|
||||
case "text/plain" -> R.string.sk_content_type_plain;
|
||||
case "text/html" -> R.string.sk_content_type_html;
|
||||
case "text/markdown" -> R.string.sk_content_type_markdown;
|
||||
case "text/bbcode" -> R.string.sk_content_type_bbcode;
|
||||
case "text/x.misskeymarkdown" -> R.string.sk_content_type_mfm;
|
||||
default -> throw new IllegalArgumentException("Invalid content type");
|
||||
};
|
||||
}
|
||||
|
||||
private void addBottomLanguage(Menu menu) {
|
||||
if (menu.findItem(allLanguages.size()) == null) {
|
||||
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
|
||||
@@ -1017,8 +1092,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(att.state!=AttachmentUploadState.DONE)
|
||||
nonDoneAttachmentCount++;
|
||||
}
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
// sendError.setVisibility(View.GONE);
|
||||
publishButton.setEnabled((!isInstancePixelfed() || attachments.size() > 0) && (trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
sendError.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
@@ -1036,18 +1111,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void onPublishClick(View v){
|
||||
if (!attachments.isEmpty()
|
||||
&& statusVisibility != StatusPrivacy.DIRECT
|
||||
&& !attachments.stream().allMatch(attachment -> attachment.description != null && !attachment.description.isBlank())) {
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.mo_no_image_desc_title)
|
||||
.setMessage(R.string.mo_no_image_desc)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.publish, (dialog, i)-> publish())
|
||||
.show();
|
||||
} else {
|
||||
publish();
|
||||
}
|
||||
publish();
|
||||
}
|
||||
|
||||
private void publishErrorCallback(ErrorResponse error) {
|
||||
@@ -1105,9 +1169,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
req.status=text;
|
||||
req.localOnly=localOnly;
|
||||
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
|
||||
req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
|
||||
req.sensitive=sensitive;
|
||||
req.language=language;
|
||||
req.contentType=contentType;
|
||||
req.scheduledAt = scheduledAt;
|
||||
if(!attachments.isEmpty()){
|
||||
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
||||
@@ -1169,7 +1234,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
sendProgress.setVisibility(View.VISIBLE);
|
||||
sendError.setVisibility(View.GONE);
|
||||
|
||||
Callback<Status> resCallback=new Callback<>(){
|
||||
Callback<Status> resCallback = new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
maybeDeleteScheduledPost(() -> {
|
||||
@@ -1182,7 +1247,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||
}
|
||||
}else{
|
||||
E.post(new StatusUpdatedEvent(result));
|
||||
// pixelfed doesn't return the edited status :/
|
||||
Status editedStatus = result == null ? editingStatus : result;
|
||||
if (result == null) {
|
||||
editedStatus.text = req.status;
|
||||
editedStatus.spoilerText = req.spoilerText;
|
||||
editedStatus.sensitive = req.sensitive;
|
||||
editedStatus.language = req.language;
|
||||
// user will have to reload to see html
|
||||
editedStatus.content = req.status;
|
||||
}
|
||||
E.post(new StatusUpdatedEvent(editedStatus));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
||||
Nav.finish(ComposeFragment.this);
|
||||
@@ -1203,7 +1278,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if(editingStatus!=null && !redraftStatus){
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
@@ -1385,9 +1459,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(requestCode==CAMERA_PIC_REQUEST_CODE && resultCode==Activity.RESULT_OK){
|
||||
Bitmap image = (Bitmap) data.getExtras().get("data");
|
||||
String path = MediaStore.Images.Media.insertImage(getContext().getContentResolver(), image, null, null);
|
||||
addMediaAttachment(Uri.parse(path), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPictureTaken(PictureTakenEvent ev){
|
||||
addMediaAttachment(ev.uri, null);
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private boolean addMediaAttachment(Uri uri, String description){
|
||||
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS){
|
||||
showMediaAttachmentError(getResources().getQuantityString(R.plurals.cant_add_more_than_x_attachments, MAX_ATTACHMENTS, MAX_ATTACHMENTS));
|
||||
@@ -1511,7 +1595,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
||||
if(areThereAnyUploadingAttachments()){
|
||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||
}
|
||||
attachment.state=AttachmentUploadState.UPLOADING;
|
||||
attachment.progressBar.setVisibility(View.VISIBLE);
|
||||
@@ -1710,6 +1794,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.serverAttachment==null)
|
||||
return;
|
||||
editMediaDescription(att);
|
||||
}
|
||||
|
||||
private void editMediaDescription(DraftMediaAttachment att) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("attachment", att.serverAttachment.id);
|
||||
@@ -1758,11 +1846,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollChanged=true;
|
||||
updatePublishButtonState();
|
||||
}));
|
||||
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
|
||||
|
||||
int maxCharactersPerOption = 50;
|
||||
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
|
||||
maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption;
|
||||
else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
|
||||
maxCharactersPerOption = instance.pollLimits.maxOptionChars;
|
||||
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)});
|
||||
|
||||
pollOptionsView.addView(option.view);
|
||||
pollOptions.add(option);
|
||||
if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
|
||||
|
||||
int maxPollOptions = 4;
|
||||
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
|
||||
maxPollOptions = instance.configuration.polls.maxOptions;
|
||||
else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
|
||||
maxPollOptions = instance.pollLimits.maxOptions;
|
||||
|
||||
if(pollOptions.size()==maxPollOptions)
|
||||
addPollOptionBtn.setVisibility(View.GONE);
|
||||
return option;
|
||||
}
|
||||
@@ -1917,9 +2018,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
visibilityPopup=new PopupMenu(getActivity(), v);
|
||||
visibilityPopup.inflate(R.menu.compose_visibility);
|
||||
Menu m=visibilityPopup.getMenu();
|
||||
if (isInstancePixelfed()) {
|
||||
m.findItem(R.id.vis_private).setVisible(false);
|
||||
}
|
||||
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
||||
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||
if (instance.pleroma != null) {
|
||||
if (isInstanceAkkoma()) {
|
||||
m.findItem(R.id.vis_local).setVisible(true);
|
||||
} else if (localOnly || prefsSaysSupported) {
|
||||
localOnlyItem.setVisible(true);
|
||||
@@ -1963,16 +2067,38 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void buildContentTypePopup(View btn) {
|
||||
contentTypePopup=new PopupMenu(getActivity(), btn);
|
||||
contentTypePopup.inflate(R.menu.compose_content_type);
|
||||
Menu m = contentTypePopup.getMenu();
|
||||
ContentType.adaptMenuToInstance(m, instance);
|
||||
if (contentType != null) m.findItem(R.id.content_type_null).setVisible(false);
|
||||
|
||||
contentTypePopup.setOnMenuItemClickListener(i->{
|
||||
int id=i.getItemId();
|
||||
if (id == R.id.content_type_null) contentType = null;
|
||||
else if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
|
||||
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
|
||||
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
|
||||
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
|
||||
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
|
||||
else return false;
|
||||
btn.setSelected(id != R.id.content_type_null && id != R.id.content_type_plain);
|
||||
i.setChecked(true);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
|
||||
btn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
|
||||
if(replyTo != null) {
|
||||
statusVisibility = (replyTo.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : replyTo.visibility);
|
||||
}
|
||||
|
||||
// A saved privacy setting from a previous compose session wins over the reply visibility
|
||||
if(savedInstanceState !=null){
|
||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
Preferences prefs = asm.getAccount(accountID).preferences;
|
||||
if (prefs != null) {
|
||||
@@ -2121,14 +2247,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
});
|
||||
}
|
||||
private void editMediaDescription(DraftMediaAttachment att) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("attachment", att.serverAttachment.id);
|
||||
args.putParcelable("uri", att.uri);
|
||||
args.putString("existingDescription", att.description);
|
||||
Nav.goForResult(getActivity(), ComposeImageDescriptionFragment.class, args, IMAGE_DESCRIPTION_RESULT, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle(){
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
@@ -14,13 +16,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class CustomLocalTimelineFragment extends StatusListFragment {
|
||||
public class CustomLocalTimelineFragment extends StatusListFragment implements ProvidesAssistContent.ProvidesWebUri {
|
||||
// private String name;
|
||||
private String domain;
|
||||
|
||||
private String maxID;
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
protected boolean wantsComposeButton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ public class CustomLocalTimelineFragment extends StatusListFragment {
|
||||
result.stream().forEach(status -> {
|
||||
status.account.acct += "@"+domain;
|
||||
status.mentions.forEach(mention -> mention.id = null);
|
||||
status.reloadWhenClicked = true;
|
||||
status.isRemote = true;
|
||||
});
|
||||
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
@@ -71,4 +73,14 @@ public class CustomLocalTimelineFragment extends StatusListFragment {
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(domain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,12 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CustomLocalTimeline;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
@@ -196,7 +199,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
@@ -222,7 +225,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
|
||||
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@@ -236,11 +239,6 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return list.getChildAt(0).getTop() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -35,4 +37,16 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.encodedPath(isInstanceAkkoma()
|
||||
? '/' + getSession().self.username + "#favorites"
|
||||
: "/favourites").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
@@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
|
||||
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
@@ -149,8 +151,13 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return list.getChildAt(0).getTop() == 0;
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma() ? "/friend-requests" : "/follow_requests").build();
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop {
|
||||
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String nextMaxID;
|
||||
private String accountId;
|
||||
private String accountID;
|
||||
|
||||
public FollowedHashtagsFragment() {
|
||||
super(20);
|
||||
@@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
accountID=args.getString("account");
|
||||
setTitle(R.string.sk_hashtags_you_follow);
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,14 +72,20 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
return new HashtagsAdapter();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return list.getChildAt(0).getTop() == 0;
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? null : base.path("/followed_tags").build();
|
||||
}
|
||||
|
||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||
@@ -114,7 +122,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface HasAccountID {
|
||||
String getAccountID();
|
||||
|
||||
default AccountSession getSession() {
|
||||
return AccountSessionManager.getInstance().getAccount(getAccountID());
|
||||
}
|
||||
|
||||
default boolean isInstanceAkkoma() {
|
||||
return getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
}
|
||||
|
||||
default boolean isInstancePixelfed() {
|
||||
return getInstance().map(Instance::isPixelfed).orElse(false);
|
||||
}
|
||||
|
||||
default Optional<Instance> getInstance() {
|
||||
return getSession().getInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
public interface HasFab {
|
||||
View getFab();
|
||||
void showFab();
|
||||
void hideFab();
|
||||
boolean isScrolling();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
@@ -8,7 +9,6 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
@@ -40,7 +40,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
private MenuItem followButton;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
protected boolean wantsComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -146,12 +146,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onFabLongClick(View v) {
|
||||
public boolean onFabLongClick(View v) {
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFabClick(View v){
|
||||
public void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("prefilledText", '#'+hashtag+' ');
|
||||
@@ -162,4 +162,14 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
@@ -20,6 +21,8 @@ import android.widget.LinearLayout;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
@@ -40,16 +43,13 @@ import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
import java.util.Optional;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -63,11 +63,9 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID {
|
||||
private FragmentRootLinearLayout content;
|
||||
|
||||
private HomeTabFragment homeTabFragment;
|
||||
|
||||
private NotificationsFragment notificationsFragment;
|
||||
private DiscoverFragment searchFragment;
|
||||
private ProfileFragment profileFragment;
|
||||
@@ -79,6 +77,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
private int currentTab=R.id.tab_home;
|
||||
|
||||
private String accountID;
|
||||
private boolean isPleroma;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -86,18 +85,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
E.register(this);
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.mo_app_name);
|
||||
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
|
||||
.map(Instance::isAkkoma)
|
||||
.orElse(false);
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
// TODO: clean up
|
||||
if(savedInstanceState==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
|
||||
homeTabFragment=new HomeTabFragment();
|
||||
homeTabFragment.setArguments(args);
|
||||
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("disableDiscover", isPleroma);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
searchFragment.setArguments(args);
|
||||
@@ -149,7 +151,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
|
||||
.commit();
|
||||
|
||||
|
||||
String defaultTab=getArguments().getString("tab");
|
||||
if("notifications".equals(defaultTab)){
|
||||
tabBar.selectTab(R.id.tab_notifications);
|
||||
@@ -170,18 +171,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
@Override
|
||||
public void onViewStateRestored(Bundle savedInstanceState){
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
|
||||
if(savedInstanceState==null) return;
|
||||
|
||||
|
||||
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
|
||||
|
||||
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||
currentTab=savedInstanceState.getInt("selectedTab");
|
||||
tabBar.selectTab(currentTab);
|
||||
Fragment current=fragmentForTab(currentTab);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.hide(homeTabFragment)
|
||||
.hide(searchFragment)
|
||||
@@ -189,15 +186,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
.hide(profileFragment)
|
||||
.show(current)
|
||||
.commit();
|
||||
|
||||
maybeTriggerLoading(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden){
|
||||
super.onHiddenChanged(hidden);
|
||||
if (!hidden && fragmentForTab(currentTab) instanceof DomainDisplay display)
|
||||
DomainManager.getInstance().setCurrentDomain(display.getDomain());
|
||||
fragmentForTab(currentTab).onHiddenChanged(hidden);
|
||||
}
|
||||
|
||||
@@ -221,9 +215,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
|
||||
|
||||
homeTabFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
|
||||
searchFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
profileFragment.onApplyWindowInsets(topOnlyInsets);
|
||||
@@ -242,33 +234,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
public void setCurrentTab(@IdRes int tab){
|
||||
if(tab==currentTab)
|
||||
return;
|
||||
tabBar.selectTab(tab);
|
||||
onTabSelected(tab);
|
||||
}
|
||||
|
||||
private void onTabSelected(@IdRes int tab){
|
||||
Fragment newFragment=fragmentForTab(tab);
|
||||
if(tab==currentTab){
|
||||
if(tab == R.id.tab_search){
|
||||
if(newFragment instanceof ScrollableToTop scrollable)
|
||||
scrollable.scrollToTop();
|
||||
searchFragment.selectSearch();
|
||||
return;
|
||||
}
|
||||
if(newFragment instanceof ScrollableToTop scrollable)
|
||||
if (tab == R.id.tab_search)
|
||||
searchFragment.onSelect();
|
||||
else if(newFragment instanceof ScrollableToTop scrollable)
|
||||
scrollable.scrollToTop();
|
||||
return;
|
||||
}
|
||||
if(tab==currentTab && tab == R.id.tab_search){
|
||||
if(newFragment instanceof ScrollableToTop scrollable)
|
||||
scrollable.scrollToTop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newFragment instanceof DomainDisplay display) {
|
||||
DomainManager.getInstance().setCurrentDomain(display.getDomain());
|
||||
}
|
||||
|
||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||
maybeTriggerLoading(newFragment);
|
||||
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
|
||||
currentTab=tab;
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
|
||||
}
|
||||
|
||||
private void maybeTriggerLoading(Fragment newFragment){
|
||||
@@ -295,10 +282,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new AccountSwitcherSheet(getActivity(), true, true, false, accountSession -> {
|
||||
getActivity().finish();
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
}).show();
|
||||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
}
|
||||
if(tab==R.id.tab_search){
|
||||
@@ -334,7 +318,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("selectedTab", currentTab);
|
||||
|
||||
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
|
||||
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
|
||||
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||
@@ -343,10 +326,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
public void updateNotificationBadge() {
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
if (instance == null) return;
|
||||
Optional<Instance> instance = session.getInstance();
|
||||
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
|
||||
|
||||
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null)
|
||||
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Notification> notifications) {
|
||||
@@ -354,9 +337,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
try {
|
||||
long newestId = Long.parseLong(notifications.get(0).id);
|
||||
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
|
||||
System.out.println("NEWEST: " + newestId);
|
||||
System.out.println("LAST SEEN: " + lastSeenId);
|
||||
|
||||
setNotificationBadge(newestId > lastSeenId);
|
||||
} catch (Exception ignored) {
|
||||
setNotificationBadge(false);
|
||||
@@ -370,6 +350,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
public void setNotificationBadge(boolean badge) {
|
||||
notificationTabIcon.setImageResource(badge
|
||||
? R.drawable.ic_fluent_alert_28_selector_badged
|
||||
@@ -385,4 +366,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
|
||||
setNotificationBadge(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -24,6 +25,7 @@ import android.view.ViewParent;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
@@ -46,7 +48,6 @@ import org.joinmastodon.android.events.HashtagUpdatedEvent;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsFragment;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
@@ -56,6 +57,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -73,7 +75,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, DomainDisplay {
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
|
||||
private static final int ANNOUNCEMENTS_RESULT = 654;
|
||||
|
||||
private String accountID;
|
||||
@@ -101,13 +103,14 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
private PopupMenu overflowPopup;
|
||||
private View overflowActionView = null;
|
||||
private boolean announcementsBadged, settingsBadged;
|
||||
private ImageButton fab;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
accountID = getArguments().getString("account");
|
||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
|
||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
|
||||
assert timelineDefinitions != null;
|
||||
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
|
||||
count = timelineDefinitions.size();
|
||||
@@ -129,6 +132,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
FrameLayout view = new FrameLayout(getContext());
|
||||
inflater.inflate(R.layout.compose_fab, view);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(this::onFabLongClick);
|
||||
pager = new ViewPager2(getContext());
|
||||
toolbarFrame = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.home_toolbar, getToolbar(), false);
|
||||
|
||||
@@ -136,6 +143,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
Bundle args = new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
args.putBoolean("__disable_fab", true);
|
||||
args.putBoolean("onlyPosts", true);
|
||||
|
||||
for (int i = 0; i < timelineDefinitions.size(); i++) {
|
||||
@@ -203,10 +211,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
if (fragments[position] instanceof BaseRecyclerFragment<?> page){
|
||||
if(!page.loaded && !page.isDataLoading()) page.loadData();
|
||||
}
|
||||
|
||||
//update recent app list url
|
||||
if (fragments[position] instanceof DomainDisplay page)
|
||||
DomainManager.getInstance().setCurrentDomain(page.getDomain());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -291,12 +295,18 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDomain() {
|
||||
if (fragments[pager.getCurrentItem()] instanceof DomainDisplay page) {
|
||||
return page.getDomain();
|
||||
private void onFabClick(View v){
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
|
||||
l.onFabClick(v);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onFabLongClick(View v) {
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
|
||||
return l.onFabLongClick(v);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return DomainDisplay.super.getDomain();
|
||||
}
|
||||
|
||||
private void addListsToOverflowMenu() {
|
||||
@@ -446,9 +456,26 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
updateSwitcherIcon(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFab() {
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.showFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideFab() {
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolling() {
|
||||
return (fragments[pager.getCurrentItem()] instanceof HasFab fabulous)
|
||||
&& fabulous.isScrolling();
|
||||
}
|
||||
|
||||
private void updateSwitcherIcon(int i) {
|
||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||
showFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -483,8 +510,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
if (((ScrollableToTop) fragments[pager.getCurrentItem()]).isScrolledToTop() &&
|
||||
!GlobalUserPreferences.disableDoubleTapToSwipe && !newPostsBtnShown) {
|
||||
if (((IsOnTop) fragments[pager.getCurrentItem()]).isOnTop() &&
|
||||
GlobalUserPreferences.doubleTapToSwipe && !newPostsBtnShown) {
|
||||
int nextPage = (pager.getCurrentItem() + 1) % count;
|
||||
navigateTo(nextPage);
|
||||
return;
|
||||
@@ -492,11 +519,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
((ScrollableToTop) fragments[pager.getCurrentItem()]).scrollToTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return ((ScrollableToTop) fragments[pager.getCurrentItem()]).isScrolledToTop();
|
||||
}
|
||||
|
||||
public void hideNewPostsButton(){
|
||||
if(!newPostsBtnShown)
|
||||
return;
|
||||
@@ -687,6 +709,15 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
return hashtagsItems.values();
|
||||
}
|
||||
|
||||
public ImageButton getFab() {
|
||||
return fab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
|
||||
}
|
||||
|
||||
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -8,8 +9,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -38,7 +37,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
private String lastSavedMarkerID;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
protected boolean wantsComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -156,7 +155,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
@@ -235,7 +234,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
@@ -288,4 +287,14 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.HOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.instance.GetDomainBlocks;
|
||||
import org.joinmastodon.android.fragments.onboarding.GoogleMadeMeAddThisFragment;
|
||||
import org.joinmastodon.android.model.DomainBlock;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Severity;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class InstanceBlockListFragment extends LoaderFragment {
|
||||
private UsableRecyclerView list;
|
||||
private MergeRecyclerAdapter adapter;
|
||||
private View buttonBar;
|
||||
private Instance instance;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
|
||||
private List<DomainBlock> domainBlocks;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
setTitle(R.string.mo_instance_info_moderated_servers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
|
||||
|
||||
list=view.findViewById(R.id.list);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new ItemsAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 0, 0, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData() {
|
||||
currentRequest= new GetDomainBlocks().setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<DomainBlock> result) {
|
||||
domainBlocks=result;
|
||||
dataLoaded();
|
||||
}
|
||||
}).execNoAuth(instance.uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
doLoadData();
|
||||
}
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(domainBlocks.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return domainBlocks.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<DomainBlock>{
|
||||
private final TextView instanceUri, reason;
|
||||
private final ImageView severity;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_server_block, list);
|
||||
instanceUri=findViewById(R.id.instance);
|
||||
reason=findViewById(R.id.reason);
|
||||
severity=findViewById(R.id.severity);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(DomainBlock item){
|
||||
instanceUri.setText(item.domain);
|
||||
reason.setText(item.comment);
|
||||
switch (item.severity) {
|
||||
case SILENCE -> {
|
||||
severity.setImageDrawable(getContext().getDrawable(R.drawable.ic_fluent_speaker_mute_28_regular));
|
||||
severity.setContentDescription(getContext().getString(R.string.mo_severity_silence));
|
||||
}
|
||||
case SUSPEND -> {
|
||||
severity.setImageDrawable(getContext().getDrawable(R.drawable.ic_fluent_shield_prohibited_28_regular));
|
||||
severity.setContentDescription(getContext().getString(R.string.mo_severity_suspend));
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
severity.setTooltipText(severity.getContentDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.instance.GetExtendedDescription;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.ExtendedDescription;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class InstanceInfoFragment extends LoaderFragment {
|
||||
|
||||
private Instance instance;
|
||||
private String extendedDescription;
|
||||
private CoverImageView cover;
|
||||
private TextView uri, description, readMore;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
private final CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable();
|
||||
private LinearLayout textWrap;
|
||||
|
||||
private ScrollView scrollView, textScrollView;
|
||||
private float titleTransY;
|
||||
|
||||
private String accountID;
|
||||
private Account account;
|
||||
private String targetDomain;
|
||||
private final ArrayList<AccountField> fields=new ArrayList<>();
|
||||
|
||||
private boolean refreshing;
|
||||
private boolean isExpanded = false;
|
||||
|
||||
public UsableRecyclerView list;
|
||||
private List<AccountField> metadataListData=Collections.emptyList();
|
||||
private MetadataAdapter adapter;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
|
||||
private int statusBarHeight;
|
||||
|
||||
private float textMaxHeight, textCollapsedHeight;
|
||||
private LinearLayout.LayoutParams collapseParams, wrapParams;
|
||||
|
||||
public InstanceInfoFragment(){
|
||||
super(R.layout.loader_fragment_overlay_toolbar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
account= AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
targetDomain=getArguments().getString("instanceDomain");
|
||||
loadData();
|
||||
loadExtendedDescription();
|
||||
DomainManager.getInstance().setCurrentDomain(targetDomain + "/about");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View content=inflater.inflate(R.layout.fragment_instance_info, container, false);
|
||||
|
||||
refreshLayout=content.findViewById(R.id.refresh_layout);
|
||||
scrollView=content.findViewById(R.id.scroll_view);
|
||||
cover=content.findViewById(R.id.cover);
|
||||
uri =content.findViewById(R.id.uri);
|
||||
description=content.findViewById(R.id.description);
|
||||
list=content.findViewById(R.id.metadata);
|
||||
textScrollView=content.findViewById(R.id.text_scroll_view);
|
||||
textWrap=content.findViewById(R.id.text_wrap);
|
||||
readMore=content.findViewById(R.id.read_more);
|
||||
textMaxHeight=getActivity().getResources().getDimension(R.dimen.description_max_height);
|
||||
textCollapsedHeight=getActivity().getResources().getDimension(R.dimen.description_collapsed_height);
|
||||
collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight);
|
||||
wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
cover.setForeground(coverGradient);
|
||||
cover.setOnClickListener(this::onCoverClick);
|
||||
readMore.setOnClickListener(this::onReadMoreClick);
|
||||
refreshLayout.setOnRefreshListener(this);
|
||||
|
||||
if(loaded){
|
||||
bindViews();
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.setDrawSelectorOnTop(true);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
|
||||
list.setAdapter(adapter=new MetadataAdapter());
|
||||
list.setClipToPadding(false);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
currentRequest=new GetInstance()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
if (getActivity() == null) return;
|
||||
instance = result;
|
||||
bindViews();
|
||||
dataLoaded();
|
||||
invalidateOptionsMenu();
|
||||
if(refreshing) {
|
||||
refreshing = false;
|
||||
refreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(targetDomain);
|
||||
}
|
||||
|
||||
private void loadExtendedDescription() {
|
||||
new GetExtendedDescription()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(ExtendedDescription result){
|
||||
if (getActivity() == null || result == null || TextUtils.isEmpty(result.content)) return;
|
||||
extendedDescription = result.content;
|
||||
updateDescription();
|
||||
collapseDescription();
|
||||
}
|
||||
})
|
||||
.execNoAuth(targetDomain);
|
||||
}
|
||||
|
||||
private void updateDescription() {
|
||||
if (instance == null || description == null)
|
||||
return;
|
||||
|
||||
description.setText(HtmlParser.parse(TextUtils.isEmpty(extendedDescription) ?
|
||||
TextUtils.isEmpty(instance.description) ? instance.shortDescription : instance.description
|
||||
: extendedDescription,
|
||||
account.emojis, Collections.emptyList(), Collections.emptyList(), accountID));
|
||||
|
||||
description.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(textWrap.getWidth(), View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
if(refreshing)
|
||||
return;
|
||||
refreshing=true;
|
||||
doLoadData();
|
||||
loadExtendedDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dataLoaded(){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
setFields(fields);
|
||||
super.dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
updateToolbar();
|
||||
scrollView.setOnScrollChangeListener(this::onScrollChanged);
|
||||
titleTransY=getToolbar().getLayoutParams().height;
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
toolbarSubtitleView.setTranslationY(titleTransY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
statusBarHeight = insets.getSystemWindowInsetTop();
|
||||
if(contentView!=null){
|
||||
((ViewGroup.MarginLayoutParams) getToolbar().getLayoutParams()).topMargin= statusBarHeight;
|
||||
}
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void bindViews(){
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(instance.thumbnail, 1000, 1000));
|
||||
uri.setText(instance.title);
|
||||
setTitle(instance.title);
|
||||
setSubtitle(instance.uri);
|
||||
|
||||
updateDescription();
|
||||
collapseDescription();
|
||||
|
||||
fields.clear();
|
||||
|
||||
|
||||
if (instance.contactAccount != null) {
|
||||
AccountField admin = new AccountField();
|
||||
admin.parsedName=admin.name=getContext().getString(R.string.mo_instance_admin);
|
||||
admin.parsedValue=buildLinkText(instance.contactAccount.url, instance.contactAccount.getDisplayUsername() + "@" + instance.uri);
|
||||
fields.add(admin);
|
||||
}
|
||||
|
||||
if (instance.email != null) {
|
||||
AccountField contact = new AccountField();
|
||||
contact.parsedName=getContext().getString(R.string.mo_instance_contact);
|
||||
contact.parsedValue=buildLinkText("mailto:" + instance.email, instance.email);
|
||||
fields.add(contact);
|
||||
}
|
||||
|
||||
if (instance.stats != null) {
|
||||
AccountField activeUsers = new AccountField();
|
||||
activeUsers.parsedName=getContext().getString(R.string.mo_instance_users);
|
||||
activeUsers.parsedValue= NumberFormat.getInstance().format(instance.stats.userCount);
|
||||
fields.add(activeUsers);
|
||||
|
||||
AccountField statusCount = new AccountField();
|
||||
statusCount.parsedName=getContext().getString(R.string.mo_instance_status);
|
||||
statusCount.parsedValue= NumberFormat.getInstance().format(instance.stats.statusCount);
|
||||
fields.add(statusCount);
|
||||
}
|
||||
|
||||
AccountField registration = new AccountField();
|
||||
registration.parsedName=getContext().getString(R.string.mo_instance_registration);
|
||||
registration.parsedValue=getContext().getString(instance.registrations ? instance.approvalRequired ? R.string.mo_instance_registration_approval : R.string.mo_instance_registration_open : R.string.instance_signup_closed);
|
||||
fields.add(registration);
|
||||
|
||||
setFields(fields);
|
||||
}
|
||||
|
||||
private SpannableStringBuilder buildLinkText(String link, String text) {
|
||||
String value = "<span class=\"h-card\"><a href=" + link + " class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">" + text + "</a></span>";
|
||||
return HtmlParser.parse(value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
}
|
||||
|
||||
private void collapseDescription() {
|
||||
textScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
description.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(textWrap.getWidth(), View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
|
||||
readMore.setText(isExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
description.post(() -> {
|
||||
boolean tooBig = description.getMeasuredHeight() > textMaxHeight;
|
||||
readMore.setVisibility(tooBig ? View.VISIBLE : View.GONE);
|
||||
textScrollView.setLayoutParams(tooBig && !isExpanded ? collapseParams : wrapParams);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateToolbar(){
|
||||
getToolbar().setBackgroundColor(0);
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
toolbarSubtitleView.setTranslationY(titleTransY);
|
||||
}
|
||||
getToolbar().setOnClickListener(v->scrollToTop());
|
||||
getToolbar().setNavigationContentDescription(R.string.back);
|
||||
}
|
||||
|
||||
public void scrollToTop(){
|
||||
scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||
if(scrollY>cover.getHeight()-topBarsH){
|
||||
cover.setTranslationY(scrollY-(cover.getHeight()-topBarsH));
|
||||
cover.setTranslationZ(V.dp(10));
|
||||
cover.setTransform(cover.getHeight()/2f-topBarsH/2f, 1f);
|
||||
}else{
|
||||
cover.setTranslationY(0f);
|
||||
cover.setTranslationZ(0f);
|
||||
cover.setTransform(scrollY/2f, 1f);
|
||||
}
|
||||
coverGradient.setTopOffset(scrollY);
|
||||
cover.invalidate();
|
||||
titleTransY=getToolbar().getHeight();
|
||||
if(scrollY>textWrap.getTop()-topBarsH){
|
||||
titleTransY=Math.max(0f, titleTransY-(scrollY-(textWrap.getTop()-topBarsH)));
|
||||
}
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
toolbarSubtitleView.setTranslationY(titleTransY);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
if (instance != null) {
|
||||
inflater.inflate(R.menu.instance_info, menu);
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, instance.uri));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share){
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, instance.uri);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
} else if (id==R.id.open_timeline) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("domain", instance.uri);
|
||||
Nav.go(getActivity(), CustomLocalTimelineFragment.class, args);
|
||||
}else if (id==R.id.rules) {
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
} else if (id==R.id.moderated_servers) {
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), InstanceBlockListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
protected int getToolbarResource(){
|
||||
return R.layout.profile_toolbar;
|
||||
}
|
||||
private void onReadMoreClick(View view) {
|
||||
isExpanded = !isExpanded;
|
||||
bindViews();
|
||||
}
|
||||
|
||||
|
||||
private void onCoverClick(View v){
|
||||
Drawable drawable=cover.getDrawable();
|
||||
if(drawable==null || drawable instanceof ColorDrawable)
|
||||
return;
|
||||
new PhotoViewer(getActivity(), Attachment.createFakeAttachments(instance.thumbnail, drawable), 0,
|
||||
new SingleImagePhotoViewerListener(cover, cover, null, this, () -> {
|
||||
}, () -> drawable, null, null));
|
||||
}
|
||||
|
||||
public void setFields(ArrayList<AccountField> fields){
|
||||
metadataListData=fields;
|
||||
if (adapter != null) adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> {
|
||||
public MetadataAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AboutViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(BaseViewHolder holder, int position){
|
||||
holder.bind(metadataListData.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return metadataListData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract class BaseViewHolder extends BindableViewHolder<AccountField> {
|
||||
public BaseViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutViewHolder extends BaseViewHolder {
|
||||
private final TextView title;
|
||||
private final LinkedTextView value;
|
||||
|
||||
public AboutViewHolder(){
|
||||
super(R.layout.item_profile_about);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
|
||||
value.setCompoundDrawables(null, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
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 androidx.annotation.Nullable;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
protected boolean wantsComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -152,7 +152,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFabClick(View v){
|
||||
public void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
@@ -162,4 +162,15 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
protected void onSetFabBottomInset(int inset) {
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.HOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/lists/" + listID).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
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.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
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.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListTimelinesFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop {
|
||||
private String accountId;
|
||||
private String profileAccountId;
|
||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
private final HashMap<String, Boolean> userInList = new HashMap<>();
|
||||
private ListsAdapter adapter;
|
||||
|
||||
public ListTimelinesFragment() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
setHasOptionsMenu(true);
|
||||
E.register(this);
|
||||
|
||||
if(args.containsKey("profileAccount")){
|
||||
profileAccountId=args.getString("profileAccount");
|
||||
String profileDisplayUsername = args.getString("profileDisplayUsername");
|
||||
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
|
||||
} else {
|
||||
setTitle(R.string.sk_your_lists);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.create) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(getContext());
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_create_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountId)
|
||||
)
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||
req.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
userInListBefore.clear();
|
||||
userInList.clear();
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
if (getActivity() == null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
if (getActivity() == null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
}
|
||||
}
|
||||
userInList.putAll(userInListBefore);
|
||||
onDataLoaded(newLists, false);
|
||||
}
|
||||
}).exec(accountId);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
item.repliesPolicy = event.repliesPolicy;
|
||||
adapter.notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
|
||||
return adapter = new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return list.getChildAt(0).getTop() == 0;
|
||||
}
|
||||
|
||||
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ListViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final CheckBox listToggle;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
listToggle=findViewById(R.id.list_toggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
|
||||
listToggle.setOnClickListener(this::onClickToggle);
|
||||
} else {
|
||||
listToggle.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickToggle(View view) {
|
||||
saveListMembership(item.id, listToggle.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountId);
|
||||
args.putString("listID", item.id);
|
||||
args.putString("listTitle", item.title);
|
||||
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
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.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
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.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private String profileAccountId;
|
||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
private final HashMap<String, Boolean> userInList = new HashMap<>();
|
||||
private ListsAdapter adapter;
|
||||
|
||||
public ListsFragment() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args = getArguments();
|
||||
accountID = args.getString("account");
|
||||
setHasOptionsMenu(true);
|
||||
E.register(this);
|
||||
|
||||
if(args.containsKey("profileAccount")){
|
||||
profileAccountId=args.getString("profileAccount");
|
||||
String profileDisplayUsername = args.getString("profileDisplayUsername");
|
||||
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
|
||||
} else {
|
||||
setTitle(R.string.sk_your_lists);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.create) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(getContext());
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_create_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID)
|
||||
)
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||
req.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
userInListBefore.clear();
|
||||
userInList.clear();
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
if (getActivity() == null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
if (getActivity() == null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
}
|
||||
}
|
||||
userInList.putAll(userInListBefore);
|
||||
onDataLoaded(newLists, false);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
item.repliesPolicy = event.repliesPolicy;
|
||||
adapter.notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
|
||||
return adapter = new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/lists").build();
|
||||
}
|
||||
|
||||
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ListViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final CheckBox listToggle;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
listToggle=findViewById(R.id.list_toggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
|
||||
listToggle.setOnClickListener(this::onClickToggle);
|
||||
} else {
|
||||
listToggle.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickToggle(View view) {
|
||||
saveListMembership(item.id, listToggle.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("listID", item.id);
|
||||
args.putString("listTitle", item.title);
|
||||
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,9 @@ public abstract class MastodonToolbarFragment extends ToolbarFragment{
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted() {
|
||||
return false; // else, badged icons don't work :(
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -13,6 +14,12 @@ import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -24,12 +31,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, DomainDisplay{
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
@@ -47,12 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public String getDomain() {
|
||||
return DomainDisplay.super.getDomain() + "/notifications";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -107,6 +103,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
|
||||
tabViews=new FrameLayout[3];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
@@ -124,6 +121,18 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
|
||||
tabLayout.setTabTextSize(V.dp(16));
|
||||
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
|
||||
@@ -145,20 +154,17 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
|
||||
allNotificationsFragment=new NotificationsListFragment();
|
||||
allNotificationsFragment.setArguments(args);
|
||||
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("onlyMentions", true);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
mentionsFragment=new NotificationsListFragment();
|
||||
mentionsFragment.setArguments(args);
|
||||
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("onlyPosts", true);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
postsFragment=new NotificationsListFragment();
|
||||
postsFragment.setArguments(args);
|
||||
|
||||
@@ -190,6 +196,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> accounts) {
|
||||
if (getActivity() == null) return;
|
||||
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
||||
}
|
||||
|
||||
@@ -205,7 +212,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
if (getFragmentForPage(pager.getCurrentItem()).isScrolledToTop() && !GlobalUserPreferences.disableDoubleTapToSwipe) {
|
||||
if (getFragmentForPage(pager.getCurrentItem()).isOnTop() && GlobalUserPreferences.doubleTapToSwipe) {
|
||||
int nextPage = (pager.getCurrentItem() + 1) % tabViews.length;
|
||||
pager.setCurrentItem(nextPage, true);
|
||||
return;
|
||||
@@ -213,11 +220,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
getFragmentForPage(pager.getCurrentItem()).scrollToTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return getFragmentForPage(pager.getCurrentItem()).isScrolledToTop();
|
||||
}
|
||||
|
||||
public void loadData(){
|
||||
refreshFollowRequestsBadge();
|
||||
if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading)
|
||||
@@ -228,6 +230,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
protected void updateToolbar(){
|
||||
super.updateToolbar();
|
||||
getToolbar().setOutlineProvider(null);
|
||||
getToolbar().setOnClickListener(v->scrollToTop());
|
||||
}
|
||||
|
||||
private NotificationsListFragment getFragmentForPage(int page){
|
||||
@@ -239,6 +242,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -263,4 +271,4 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -10,18 +11,23 @@ import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
@@ -50,13 +56,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDomain() {
|
||||
return super.getDomain() + "/notifications";
|
||||
protected boolean wantsComposeButton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,7 +108,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
case UPDATE -> getString(R.string.sk_post_edited);
|
||||
case SIGN_UP -> getString(R.string.sk_signed_up);
|
||||
case REPORT -> getString(R.string.sk_reported);
|
||||
case EMOJI_REACTION -> getString(R.string.sk_reacted, n.emoji);
|
||||
case REACTION, PLEROMA_EMOJI_REACTION ->
|
||||
n.emoji != null ? getString(R.string.sk_reacted_with, n.emoji) : getString(R.string.sk_reacted);
|
||||
};
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, n.emojiUrl!=null ? HtmlParser.parseCustomEmoji(extraText, Collections.singletonList(emoji)) : extraText, n, null) : null;
|
||||
if(n.status!=null){
|
||||
@@ -156,12 +158,17 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
loadRelationships(needRelationships);
|
||||
maxID=result.maxID;
|
||||
|
||||
if(offset==0 && !result.items.isEmpty() && !result.isFromCache()){
|
||||
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
|
||||
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
|
||||
E.post(new AllNotificationsSeenEvent());
|
||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).markers
|
||||
.notifications.lastReadId = result.items.get(0).id;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
|
||||
if (isInstanceAkkoma()) {
|
||||
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -181,40 +188,23 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading){
|
||||
refreshing=true;
|
||||
loadData();
|
||||
}
|
||||
|
||||
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
// loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Notification n=getNotificationByID(id);
|
||||
if(n.status!=null){
|
||||
Status status=n.status;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}else if(n.report != null){
|
||||
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
|
||||
}else{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
if(n.status != null && n.status.inReplyToAccountId != null && knownAccounts.containsKey(n.status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(n.status.inReplyToAccountId)));
|
||||
UiUtils.showFragmentForNotification(getContext(), n, accountID, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
if (getParentFragment() instanceof NotificationsFragment) fab.setVisibility(View.GONE);
|
||||
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
@@ -240,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
|
||||
// copied from StatusListFragment.EventListener (just like the method above)
|
||||
// (which assumes this.data to be a list of statuses...)
|
||||
@Subscribe
|
||||
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
|
||||
for(Notification n:data){
|
||||
if (n.status == null) continue;
|
||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||
n.status.getContentStatus().update(ev);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||
footer.rebind();
|
||||
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||
footer.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Notification n:preloadedData){
|
||||
if (n.status == null) continue;
|
||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||
n.status.getContentStatus().update(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||
@@ -272,4 +288,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma()
|
||||
? "/users/" + getSession().self.username + "/interactions"
|
||||
: "/notifications").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment impl
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
|
||||
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
@@ -19,6 +20,7 @@ import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -52,6 +54,7 @@ import org.joinmastodon.android.DomainManager;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
@@ -82,6 +85,7 @@ import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -92,9 +96,13 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -115,7 +123,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||
private static final int AVATAR_RESULT=722;
|
||||
private static final int COVER_RESULT=343;
|
||||
|
||||
@@ -143,8 +151,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public FrameLayout noteWrap;
|
||||
public EditText noteEdit;
|
||||
private String note;
|
||||
private Account account;
|
||||
private Account account, remoteAccount;
|
||||
private String accountID;
|
||||
private String domain;
|
||||
private Relationship relationship;
|
||||
private int statusBarHeight;
|
||||
private boolean isOwnProfile;
|
||||
@@ -158,9 +167,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private WindowInsets childInsets;
|
||||
private PhotoViewer currentPhotoViewer;
|
||||
private boolean editModeLoading;
|
||||
protected int scrollDiff = 0;
|
||||
|
||||
private static final int MAX_FIELDS=4;
|
||||
private int maxFields = 4;
|
||||
|
||||
// from ProfileAboutFragment
|
||||
public UsableRecyclerView list;
|
||||
@@ -181,13 +189,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
setRetainInstance(true);
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
if(getArguments().containsKey("profileAccount")){
|
||||
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
if (getArguments().containsKey("remoteAccount")) {
|
||||
remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||
if(!getArguments().getBoolean("noAutoLoad", false))
|
||||
loadData();
|
||||
} else if(getArguments().containsKey("profileAccount")){
|
||||
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
profileAccountID=account.id;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
loaded=true;
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else if (isInstanceAkkoma() && getInstance().isPresent())
|
||||
if(getInstance().get().pleroma != null && getInstance().get().pleroma.metadata != null && getInstance().get().pleroma.metadata.fieldsLimits != null){
|
||||
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
||||
}
|
||||
}else{
|
||||
profileAccountID=getArguments().getString("profileAccountID");
|
||||
if(!getArguments().getBoolean("noAutoLoad", false))
|
||||
@@ -206,14 +223,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden) {
|
||||
super.onHiddenChanged(hidden);
|
||||
if (!hidden) {
|
||||
DomainManager.getInstance().setCurrentDomain(account.url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View content=inflater.inflate(R.layout.fragment_profile, container, false);
|
||||
@@ -232,7 +241,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followingCount=content.findViewById(R.id.following_count);
|
||||
followingLabel=content.findViewById(R.id.following_label);
|
||||
followingBtn=content.findViewById(R.id.following_btn);
|
||||
|
||||
postsCount=content.findViewById(R.id.posts_count);
|
||||
postsLabel=content.findViewById(R.id.posts_label);
|
||||
postsBtn=content.findViewById(R.id.posts_btn);
|
||||
@@ -272,7 +280,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
animate.setFillAfter(true);
|
||||
fab.startAnimation(animate);
|
||||
|
||||
noteEditConfirm.setVisibility(View.VISIBLE);
|
||||
@@ -287,7 +294,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
animate.setFillAfter(true);
|
||||
fab.startAnimation(animate);
|
||||
|
||||
noteEditConfirm.animate()
|
||||
@@ -386,10 +392,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
|
||||
|
||||
//this currently takes up the whole username
|
||||
//in the future it might need to be change to only the instance uri
|
||||
username.setOnClickListener(v -> {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("instanceDomain", Uri.parse(account.url).getHost());
|
||||
Nav.go(getActivity(), InstanceInfoFragment.class, args);
|
||||
});
|
||||
|
||||
username.setOnLongClickListener(v->{
|
||||
String usernameString=account.acct;
|
||||
if(!usernameString.contains("@")){
|
||||
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
usernameString+="@"+domain;
|
||||
}
|
||||
UiUtils.copyText(username, '@'+usernameString);
|
||||
return true;
|
||||
@@ -406,6 +422,32 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
private void onAccountLoaded(Account result) {
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
bindHeaderView();
|
||||
dataLoaded();
|
||||
if(!tabLayoutMediator.isAttached())
|
||||
tabLayoutMediator.attach();
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
if(refreshing){
|
||||
refreshing=false;
|
||||
refreshLayout.setRefreshing(false);
|
||||
if(postsFragment.loaded)
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(pinnedPostsFragment.loaded)
|
||||
pinnedPostsFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
}
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
|
||||
public void setNote(String note){
|
||||
this.note=note;
|
||||
noteWrap.setVisibility(View.VISIBLE);
|
||||
@@ -419,42 +461,35 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public void onSuccess(Relationship result) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse result) {
|
||||
Toast.makeText(getActivity(), getString(R.string.mo_personal_note_update_failed), Toast.LENGTH_LONG).show();
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
if (remoteAccount != null) {
|
||||
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
|
||||
if (getContext() == null) return;
|
||||
if (args == null || !args.containsKey("profileAccount")) {
|
||||
Toast.makeText(getContext(), getContext().getString(
|
||||
R.string.sk_error_loading_profile, domain
|
||||
), Toast.LENGTH_SHORT).show();
|
||||
Nav.finish(this);
|
||||
return;
|
||||
}
|
||||
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
currentRequest=new GetAccountByID(profileAccountID)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
if (getActivity() == null) return;
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
bindHeaderView();
|
||||
dataLoaded();
|
||||
if(!tabLayoutMediator.isAttached())
|
||||
tabLayoutMediator.attach();
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
if(refreshing){
|
||||
refreshing=false;
|
||||
refreshLayout.setRefreshing(false);
|
||||
if(postsFragment.loaded)
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(pinnedPostsFragment.loaded)
|
||||
pinnedPostsFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
}
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
onAccountLoaded(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -594,14 +629,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
|
||||
String acct = ((isSelf || account.isRemote)
|
||||
? account.getFullyQualifiedName()
|
||||
: account.acct);
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
if(isSelf){
|
||||
ssb.append('@');
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb.append(acct);
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
@@ -623,17 +656,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
// noinspection SetTextI18n
|
||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||
username.setText('@'+acct);
|
||||
}
|
||||
CharSequence parsedBio = null;
|
||||
if(account.note != null){
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
}
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
bio.setVisibility(View.VISIBLE);
|
||||
bio.setText(parsedBio);
|
||||
}
|
||||
|
||||
|
||||
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
|
||||
@@ -695,6 +729,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
if(isOwnProfile && isInEditMode){
|
||||
@@ -729,8 +768,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
));
|
||||
}
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
||||
if(isOwnProfile)
|
||||
if(isOwnProfile) {
|
||||
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MenuItem mute = menu.findItem(R.id.mute);
|
||||
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
@@ -812,7 +853,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
args.putString("profileAccount", profileAccountID);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
}
|
||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
||||
Nav.go(getActivity(), ListsFragment.class, args);
|
||||
}else if(id==R.id.followed_hashtags){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -865,13 +906,28 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
// aboutFragment.setNote(relationship.note, accountID, profileAccountID);
|
||||
}
|
||||
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
||||
DomainManager.getInstance().setCurrentDomain(account.url);
|
||||
}
|
||||
|
||||
public ImageButton getFab() {
|
||||
return fab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFab() {
|
||||
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.showFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideFab() {
|
||||
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolling() {
|
||||
return getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous
|
||||
&& fabulous.isScrolling();
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||
if(scrollY>avatarBorder.getTop()-topBarsH){
|
||||
@@ -902,36 +958,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(currentPhotoViewer!=null){
|
||||
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
|
||||
}
|
||||
|
||||
if(GlobalUserPreferences.enableFabAutoHide){
|
||||
int dy = scrollY - oldScrollY;
|
||||
|
||||
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
scrollDiff = 0;
|
||||
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
||||
if (scrollDiff > 400) {
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
scrollDiff = 0;
|
||||
} else {
|
||||
scrollDiff += Math.abs(dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
@@ -1232,11 +1258,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolledToTop() {
|
||||
return list.getChildAt(0).getTop() == 0;
|
||||
}
|
||||
|
||||
private void onFollowersOrFollowingClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -1301,6 +1322,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if (adapter != null) adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(account.url);
|
||||
}
|
||||
|
||||
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
|
||||
public MetadataAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -1331,7 +1367,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public int getItemCount(){
|
||||
if(isInEditMode){
|
||||
int size=metadataListData.size();
|
||||
if(size<MAX_FIELDS)
|
||||
if(size<maxFields)
|
||||
size++;
|
||||
return size;
|
||||
}
|
||||
@@ -1465,7 +1501,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onClick(){
|
||||
metadataListData.add(new AccountField());
|
||||
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
|
||||
if(metadataListData.size()==maxFields){ // replace this row with new row
|
||||
adapter.notifyItemChanged(metadataListData.size()-1);
|
||||
}else{
|
||||
adapter.notifyItemInserted(metadataListData.size()-1);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
@@ -32,7 +33,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
protected boolean wantsComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFabClick(View v) {
|
||||
public void onFabClick(View v) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
|
||||
@@ -65,7 +66,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onFabLongClick(View v) {
|
||||
public boolean onFabLongClick(View v) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
|
||||
@@ -80,7 +81,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, Filter.FilterContext.HOME);
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,6 +98,8 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
args.putString("sourceText", status.text);
|
||||
args.putString("sourceSpoiler", status.spoilerText);
|
||||
args.putBoolean("redraftStatus", true);
|
||||
args.putString("sourceContentType", scheduledStatus.params.contentType != null ?
|
||||
scheduledStatus.params.contentType.name() : null);
|
||||
setResult(true, null);
|
||||
|
||||
// closing this scheduled status list if another status list is opened from compose fragment
|
||||
@@ -180,4 +183,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
// TODO: adapt when frontends finally implement a scheduled posts list
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package org.joinmastodon.android.fragments;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
public interface ScrollableToTop{
|
||||
boolean isScrolledToTop();
|
||||
// boolean isScrolledToTop();
|
||||
|
||||
void scrollToTop();
|
||||
|
||||
@@ -23,7 +24,7 @@ public interface ScrollableToTop{
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
list.scrollBy(0, V.dp(300));
|
||||
list.scrollBy(0, UiUtils.SCROLL_TO_TOP_DELTA);
|
||||
list.smoothScrollToPosition(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -10,12 +10,11 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.LruCache;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -23,8 +22,6 @@ import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
@@ -38,9 +35,9 @@ import android.widget.Toast;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
|
||||
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
@@ -52,8 +49,8 @@ import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
@@ -61,14 +58,18 @@ import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -80,13 +81,13 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SettingsFragment extends MastodonToolbarFragment {
|
||||
private View view;
|
||||
public class SettingsFragment extends MastodonToolbarFragment implements ProvidesAssistContent.ProvidesWebUri {
|
||||
private UsableRecyclerView list;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private ThemeItem themeItem;
|
||||
private NotificationPolicyItem notificationPolicyItem;
|
||||
private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem;
|
||||
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
|
||||
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
|
||||
private String accountID;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean needAppRestart;
|
||||
@@ -95,7 +96,9 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
private ImageView themeTransitionWindowView;
|
||||
private TextItem checkForUpdateItem, clearImageCacheItem;
|
||||
private ImageCache imageCache;
|
||||
private Menu contentTypeMenu;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -105,22 +108,20 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
imageCache = ImageCache.getInstance(getActivity());
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
Optional<Instance> instance = session.getInstance();
|
||||
String instanceName = UiUtils.getInstanceName(accountID);
|
||||
|
||||
DomainManager.getInstance().setCurrentDomain(session.domain + "/settings");
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING && updater.getUpdateInfo() != null){
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||
items.add(new UpdateItem());
|
||||
}
|
||||
}
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
items.add(new ButtonItem(R.string.sk_settings_color_palette, R.drawable.ic_fluent_color_24_regular, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.color_palettes);
|
||||
@@ -128,35 +129,54 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick);
|
||||
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
b.setOnClickListener(v->popupMenu.show());
|
||||
b.setText(switch(GlobalUserPreferences.color){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
case NORD -> R.string.mo_color_palette_nord;
|
||||
});
|
||||
// b.setText(switch(GlobalUserPreferences.color){
|
||||
// case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
// case PINK -> R.string.sk_color_palette_pink;
|
||||
// case PURPLE -> R.string.sk_color_palette_purple;
|
||||
// case GREEN -> R.string.sk_color_palette_green;
|
||||
// case BLUE -> R.string.sk_color_palette_blue;
|
||||
// case BROWN -> R.string.sk_color_palette_brown;
|
||||
// case RED -> R.string.sk_color_palette_red;
|
||||
// case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
// });
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
items.add(new SwitchItem(R.string.sk_disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
|
||||
GlobalUserPreferences.disableMarquee=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b->{
|
||||
updatePublishText(b);
|
||||
|
||||
b.setOnClickListener(l->{
|
||||
TextInputFrameLayout input = new TextInputFrameLayout(
|
||||
getContext(),
|
||||
getString(R.string.publish),
|
||||
GlobalUserPreferences.publishButtonText.trim()
|
||||
);
|
||||
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(input)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
GlobalUserPreferences.publishButtonText = input.getEditText().getText().toString().trim();
|
||||
GlobalUserPreferences.save();
|
||||
updatePublishText(b);
|
||||
})
|
||||
.setNeutralButton(R.string.clear, (d, which) -> {
|
||||
GlobalUserPreferences.publishButtonText = "";
|
||||
GlobalUserPreferences.save();
|
||||
updatePublishText(b);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
});
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_uniform_icon_for_notifications, R.drawable.ic_ntf_logo, GlobalUserPreferences.uniformNotificationIcon, i->{
|
||||
GlobalUserPreferences.uniformNotificationIcon=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
|
||||
GlobalUserPreferences.disableMarquee=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_reduce_motion, R.drawable.ic_fluent_star_emphasis_24_regular, GlobalUserPreferences.reduceMotion, i->{
|
||||
GlobalUserPreferences.reduceMotion=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
GlobalUserPreferences.playGifs=i.checked;
|
||||
@@ -166,108 +186,61 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
GlobalUserPreferences.useCustomTabs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_hide_compose_button_while_scrolling_setting, R.drawable.ic_fluent_edit_24_regular, GlobalUserPreferences.enableFabAutoHide, i->{
|
||||
GlobalUserPreferences.enableFabAutoHide =i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_load_remote_followers, R.drawable.ic_fluent_people_24_regular, GlobalUserPreferences.loadRemoteAccountFollowers, i -> {
|
||||
GlobalUserPreferences.loadRemoteAccountFollowers=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
|
||||
GlobalUserPreferences.showInteractionCounts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
||||
items.add(alwaysRevealSpoilersItem = new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
||||
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(autoRevealSpoilersItem)) instanceof ButtonViewHolder bvh) bvh.rebind();
|
||||
}));
|
||||
items.add(autoRevealSpoilersItem = new ButtonItem(R.string.sk_settings_auto_reveal_equal_spoilers, R.drawable.ic_fluent_eye_24_regular, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.settings_auto_reveal_spoiler);
|
||||
popupMenu.setOnMenuItemClickListener(i -> onAutoRevealSpoilerClick(i, b));
|
||||
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
b.setOnClickListener(v->popupMenu.show());
|
||||
onAutoRevealSpoilerChanged(b);
|
||||
}));
|
||||
|
||||
// items.add(new SwitchItem(R.string.sk_settings_show_differentiated_notification_icons, R.drawable.ic_ntf_logo, GlobalUserPreferences.showUniformPushNoticationIcons, this::onNotificationStyleChanged));
|
||||
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
|
||||
GlobalUserPreferences.disableSwipe=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_disable_double_tap_to_swipe_between_tabs, R.drawable.ic_fluent_double_tap_swipe_right_24_regular, GlobalUserPreferences.disableDoubleTapToSwipe, i->{
|
||||
GlobalUserPreferences.disableDoubleTapToSwipe=i.checked;
|
||||
items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{
|
||||
GlobalUserPreferences.enableDeleteNotifications=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_confirm_before_reblog, R.drawable.ic_fluent_checkmark_circle_24_regular, GlobalUserPreferences.confirmBeforeReblog, i->{
|
||||
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_swap_bookmark_with_reblog, R.drawable.ic_boost, GlobalUserPreferences.swapBookmarkWithBoostAction, i -> {
|
||||
GlobalUserPreferences.swapBookmarkWithBoostAction=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
|
||||
items.add(new HeaderItem(R.string.mo_composer_behavior));
|
||||
items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b-> {
|
||||
updatePublishText(b);
|
||||
b.setOnClickListener(l -> {
|
||||
if(!GlobalUserPreferences.relocatePublishButton) {
|
||||
FrameLayout inputWrap = new FrameLayout(getContext());
|
||||
EditText input = new EditText(getContext());
|
||||
input.setHint(R.string.publish);
|
||||
input.setText(GlobalUserPreferences.publishButtonText.trim());
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(V.dp(16), V.dp(4), V.dp(16), V.dp(16));
|
||||
input.setLayoutParams(params);
|
||||
inputWrap.addView(input);
|
||||
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(inputWrap)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
GlobalUserPreferences.publishButtonText = input.getText().toString().trim();
|
||||
GlobalUserPreferences.save();
|
||||
updatePublishText(b);
|
||||
})
|
||||
.setNeutralButton(R.string.clear, (d, which) -> {
|
||||
GlobalUserPreferences.publishButtonText = "";
|
||||
GlobalUserPreferences.save();
|
||||
updatePublishText(b);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {
|
||||
})
|
||||
.show();
|
||||
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.mo_disable_relocate_publish_button_to_enable_customization,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_relocate_publish_button, R.drawable.ic_fluent_arrow_autofit_down_24_regular, GlobalUserPreferences.relocatePublishButton, i->{
|
||||
GlobalUserPreferences.relocatePublishButton=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_change_default_reply_visibility_to_unlisted, R.drawable.ic_fluent_lock_open_24_regular, GlobalUserPreferences.defaultToUnlistedReplies, i->{
|
||||
GlobalUserPreferences.defaultToUnlistedReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
// TODO find a good icon for this setting
|
||||
items.add(new SwitchItem(R.string.mo_mention_reblogger_automatically, R.drawable.ic_fluent_balloon_24_regular, GlobalUserPreferences.mentionRebloggerAutomatically, i -> {
|
||||
GlobalUserPreferences.mentionRebloggerAutomatically=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.mo_disable_reminder_to_add_alt_text, R.drawable.ic_fluent_image_alt_text_24_regular, GlobalUserPreferences.disableAltTextReminder, i->{
|
||||
items.add(new SwitchItem(R.string.sk_settings_disable_alt_text_reminder, R.drawable.ic_fluent_image_alt_text_24_regular, GlobalUserPreferences.disableAltTextReminder, i->{
|
||||
GlobalUserPreferences.disableAltTextReminder=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_single_notification, R.drawable.ic_fluent_convert_range_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{
|
||||
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{
|
||||
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_confirm_before_reblog, R.drawable.ic_fluent_checkmark_circle_24_regular, GlobalUserPreferences.confirmBeforeReblog, i->{
|
||||
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
|
||||
GlobalUserPreferences.allowRemoteLoading=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_timelines));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
GlobalUserPreferences.showReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
if (instance.pleroma != null) {
|
||||
if (isInstanceAkkoma()) {
|
||||
items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.reply_visibility);
|
||||
@@ -289,33 +262,35 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
showNewPostsButtonItem.enabled = i.checked;
|
||||
showNewPostsItem.enabled = i.checked;
|
||||
if (!i.checked) {
|
||||
GlobalUserPreferences.showNewPostsButton = false;
|
||||
showNewPostsButtonItem.checked = false;
|
||||
showNewPostsItem.checked = false;
|
||||
}
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsButtonItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(showNewPostsButtonItem = new SwitchItem(R.string.sk_settings_show_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{
|
||||
items.add(showNewPostsItem = new SwitchItem(R.string.sk_settings_show_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{
|
||||
GlobalUserPreferences.showNewPostsButton=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_alt_indicator, R.drawable.ic_fluent_scan_text_24_regular, GlobalUserPreferences.showAltIndicator, i->{
|
||||
GlobalUserPreferences.showAltIndicator=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_no_alt_indicator, R.drawable.ic_fluent_important_24_regular, GlobalUserPreferences.showNoAltIndicator, i->{
|
||||
GlobalUserPreferences.showNoAltIndicator=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_collapse_long_posts, R.drawable.ic_fluent_chevron_down_24_regular, GlobalUserPreferences.collapseLongPosts, i->{
|
||||
GlobalUserPreferences.collapseLongPosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_star_off_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
||||
GlobalUserPreferences.spectatorMode=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_hide_fab, R.drawable.ic_fluent_edit_24_regular, GlobalUserPreferences.autoHideFab, i->{
|
||||
GlobalUserPreferences.autoHideFab=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
@@ -336,16 +311,17 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
needAppRestart=true;
|
||||
}));
|
||||
compactReblogReplyLineItem.enabled=GlobalUserPreferences.replyLineAboveHeader;
|
||||
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
||||
GlobalUserPreferences.spectatorMode=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
// items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
|
||||
// GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
|
||||
//// GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
|
||||
// GlobalUserPreferences.save();
|
||||
// needAppRestart=true;
|
||||
// }));
|
||||
boolean translationAvailable = instance
|
||||
.map(i -> i.v2 != null && i.v2.configuration.translation != null && i.v2.configuration.translation.enabled)
|
||||
.orElse(false);
|
||||
items.add(new SmallTextItem(getString(translationAvailable ?
|
||||
R.string.sk_settings_translation_availability_note_available :
|
||||
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
@@ -360,24 +336,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
items.add(new SwitchItem(R.string.sk_notify_update, R.drawable.ic_fluent_history_24_regular, pushSubscription.alerts.update, i->onNotificationsChanged(PushNotification.Type.UPDATE, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.sk_notify_poll_results, R.drawable.ic_fluent_poll_24_regular, pushSubscription.alerts.poll, i->onNotificationsChanged(PushNotification.Type.POLL, i.checked), switchEnabled));
|
||||
|
||||
items.add(new HeaderItem(R.string.mo_miscellaneous_settings));
|
||||
items.add(new SwitchItem(R.string.mo_disable_dividers, R.drawable.ic_fluent_timeline_24_regular, GlobalUserPreferences.disableDividers, i->{
|
||||
GlobalUserPreferences.disableDividers=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{
|
||||
GlobalUserPreferences.enableDeleteNotifications=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_single_notification, R.drawable.ic_fluent_convert_range_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{
|
||||
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_account));
|
||||
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_posting, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/preferences/other"), R.drawable.ic_fluent_open_24_regular));
|
||||
@@ -385,23 +343,55 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
|
||||
|
||||
items.add(new HeaderItem(instanceName));
|
||||
items.add(new TextItem(R.string.sk_settings_rules, ()->{
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
items.add(new TextItem(R.string.sk_settings_rules, instance.<Runnable>map(i -> () -> {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(i));
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
|
||||
}).orElse(null), R.drawable.ic_fluent_task_list_ltr_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
|
||||
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version)));
|
||||
items.add(new SmallTextItem(instance
|
||||
.map(i -> getString(R.string.sk_settings_server_version, i.version))
|
||||
.orElse(getString(R.string.sk_instance_info_unavailable))));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_instance_features));
|
||||
items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{
|
||||
if (i.checked) {
|
||||
GlobalUserPreferences.accountsWithContentTypesEnabled.add(accountID);
|
||||
if (GlobalUserPreferences.accountsDefaultContentTypes.get(accountID) == null) {
|
||||
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, ContentType.PLAIN);
|
||||
}
|
||||
} else {
|
||||
GlobalUserPreferences.accountsWithContentTypesEnabled.remove(accountID);
|
||||
GlobalUserPreferences.accountsDefaultContentTypes.remove(accountID);
|
||||
}
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(defaultContentTypeButtonItem))
|
||||
instanceof ButtonViewHolder bvh) bvh.rebind();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_content_types_explanation)));
|
||||
items.add(defaultContentTypeButtonItem = new ButtonItem(R.string.sk_settings_default_content_type, 0, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.compose_content_type);
|
||||
popupMenu.setOnMenuItemClickListener(item -> this.onContentTypeChanged(item, b));
|
||||
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
b.setOnClickListener(v->popupMenu.show());
|
||||
ContentType contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
|
||||
b.setText(getContentTypeString(contentType));
|
||||
contentTypeMenu = popupMenu.getMenu();
|
||||
contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true);
|
||||
instance.ifPresent(i -> ContentType.adaptMenuToInstance(contentTypeMenu, i));
|
||||
}));
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation)));
|
||||
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
|
||||
glitchModeItem.enabled = i.checked;
|
||||
if (i.checked) {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
|
||||
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
if (!isInstanceAkkoma()) {
|
||||
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
}
|
||||
} else {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
|
||||
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
|
||||
@@ -422,31 +412,9 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
glitchModeItem.enabled = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_glitch_mode_explanation)));
|
||||
|
||||
|
||||
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
|
||||
items.add(new SmallTextItem(getString(translationAvailable ?
|
||||
R.string.sk_settings_translation_availability_note_available :
|
||||
R.string.sk_settings_translation_availability_note_unavailable, instance.title)));
|
||||
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_settings_about));
|
||||
|
||||
// if(BuildConfig.BUILD_TYPE.equals("nightly")){
|
||||
// items.add(new TextItem(R.string.mo_download_latest_nightly_release, ()->UiUtils.launchWebBrowser(getActivity(), "https://nightly.link/LucasGGamerM/moshidon/workflows/nightly-builds/master/moshidon-nightly.apk.zip"), R.drawable.ic_fluent_open_24_regular));
|
||||
// }
|
||||
|
||||
items.add(new TextItem(R.string.mo_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/LucasGGamerM/moshidon"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sponsors/LucasGGamerM"), R.drawable.ic_fluent_heart_24_regular));
|
||||
|
||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||
items.add(checkForUpdateItem);
|
||||
items.add(new SwitchItem(R.string.sk_updater_enable_pre_releases, 0, GlobalUserPreferences.enablePreReleases, i->{
|
||||
GlobalUserPreferences.enablePreReleases=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
}
|
||||
|
||||
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
|
||||
LruCache<?, ?> cache = imageCache == null ? null : imageCache.getLruCache();
|
||||
clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), cache != null ? cache.size() : 0, true), this::clearImageCache, 0);
|
||||
items.add(clearImageCacheItem);
|
||||
@@ -454,16 +422,18 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
GlobalUserPreferences.recentLanguages.remove(accountID);
|
||||
GlobalUserPreferences.save();
|
||||
})));
|
||||
|
||||
items.add(new TextItem(R.string.mo_clear_recent_emoji, ()-> {
|
||||
GlobalUserPreferences.recentEmojis.clear();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||
items.add(new SwitchItem(R.string.sk_updater_enable_pre_releases, 0, GlobalUserPreferences.enablePreReleases, i->{
|
||||
GlobalUserPreferences.enablePreReleases=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||
items.add(checkForUpdateItem);
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG){
|
||||
items.add(new RedHeaderItem("Debug options"));
|
||||
|
||||
items.add(new TextItem("Test E-Mail confirmation flow", ()->{
|
||||
items.add(new TextItem("Test e-mail confirmation flow", ()->{
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
@@ -472,35 +442,9 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}));
|
||||
|
||||
items.add(new TextItem("Copy preferences", ()->{
|
||||
StringBuilder prefBuilder = new StringBuilder();
|
||||
GlobalUserPreferences.load();
|
||||
GlobalUserPreferences.getPrefs().getAll().forEach((key, value) -> prefBuilder.append(key).append(": ").append(value).append('\n'));
|
||||
UiUtils.copyText(view, prefBuilder.toString());
|
||||
}));
|
||||
|
||||
items.add(new TextItem("Reset preferences", ()->{
|
||||
GlobalUserPreferences.load();
|
||||
GlobalUserPreferences.getPrefs().edit().clear().commit();
|
||||
UiUtils.restartApp();
|
||||
}, R.drawable.ic_fluent_warning_24_regular));
|
||||
|
||||
items.add(new TextItem("Open App Info", () ->
|
||||
getContext().startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", getContext().getPackageName(), null))),
|
||||
R.drawable.ic_fluent_open_24_regular
|
||||
)
|
||||
);
|
||||
|
||||
items.add(new TextItem("Open developer settings",
|
||||
()-> getContext().startActivity(new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)),
|
||||
R.drawable.ic_fluent_open_24_regular)
|
||||
);
|
||||
}
|
||||
|
||||
String version = getContext().getString(R.string.mo_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE);
|
||||
items.add(new FooterItem(version, () -> UiUtils.copyText(view, version)));
|
||||
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
private void updatePublishText(Button btn) {
|
||||
@@ -559,7 +503,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
this.view = view;
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.register(this);
|
||||
}
|
||||
@@ -589,7 +532,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
else if (id == R.id.brown_color) pref = ColorPreference.BROWN;
|
||||
else if (id == R.id.red_color) pref = ColorPreference.RED;
|
||||
else if (id == R.id.yellow_color) pref = ColorPreference.YELLOW;
|
||||
else if (id == R.id.nord_color) pref = ColorPreference.NORD;
|
||||
|
||||
if (pref == null) return false;
|
||||
|
||||
@@ -599,6 +541,35 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onAutoRevealSpoilerClick(MenuItem item, Button btn) {
|
||||
int id = item.getItemId();
|
||||
|
||||
AutoRevealMode mode = AutoRevealMode.NEVER;
|
||||
if (id == R.id.auto_reveal_threads) mode = AutoRevealMode.THREADS;
|
||||
else if (id == R.id.auto_reveal_discussions) mode = AutoRevealMode.DISCUSSIONS;
|
||||
|
||||
GlobalUserPreferences.alwaysExpandContentWarnings = false;
|
||||
GlobalUserPreferences.autoRevealEqualSpoilers = mode;
|
||||
GlobalUserPreferences.save();
|
||||
onAutoRevealSpoilerChanged(btn);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onAutoRevealSpoilerChanged(Button b) {
|
||||
if (GlobalUserPreferences.alwaysExpandContentWarnings) {
|
||||
b.setText(R.string.sk_settings_auto_reveal_always);
|
||||
} else {
|
||||
b.setText(switch(GlobalUserPreferences.autoRevealEqualSpoilers){
|
||||
case THREADS -> R.string.sk_settings_auto_reveal_threads;
|
||||
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_discussions;
|
||||
default -> R.string.sk_settings_auto_reveal_never;
|
||||
});
|
||||
if (alwaysRevealSpoilersItem.checked != GlobalUserPreferences.alwaysExpandContentWarnings) {
|
||||
alwaysRevealSpoilersItem.checked = GlobalUserPreferences.alwaysExpandContentWarnings;
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(alwaysRevealSpoilersItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||
@@ -616,6 +587,34 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private @StringRes int getContentTypeString(@Nullable ContentType contentType) {
|
||||
if (contentType == null) return R.string.sk_content_type_unspecified;
|
||||
return switch (contentType) {
|
||||
case PLAIN -> R.string.sk_content_type_plain;
|
||||
case HTML -> R.string.sk_content_type_html;
|
||||
case MARKDOWN -> R.string.sk_content_type_markdown;
|
||||
case BBCODE -> R.string.sk_content_type_bbcode;
|
||||
case MISSKEY_MARKDOWN -> R.string.sk_content_type_mfm;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean onContentTypeChanged(MenuItem item, Button btn){
|
||||
int id = item.getItemId();
|
||||
|
||||
ContentType contentType = null;
|
||||
if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
|
||||
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
|
||||
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
|
||||
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
|
||||
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
|
||||
|
||||
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
|
||||
GlobalUserPreferences.save();
|
||||
btn.setText(getContentTypeString(contentType));
|
||||
item.setChecked(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onReplyVisibilityChanged(MenuItem item, Button btn){
|
||||
String pref = null;
|
||||
int id = item.getItemId();
|
||||
@@ -685,12 +684,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void onNotificationStyleChanged(SwitchItem item){
|
||||
GlobalUserPreferences.uniformNotificationIcon=item.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}
|
||||
|
||||
|
||||
private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
PushSubscription.Policy prevPolicy=subscription.policy;
|
||||
@@ -774,7 +767,7 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING;
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind();
|
||||
|
||||
|
||||
UpdateItem updateItem = null;
|
||||
if(items.get(0) instanceof UpdateItem item0) {
|
||||
updateItem = item0;
|
||||
@@ -794,6 +787,16 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma() ? "/about" : "/settings").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
@@ -860,13 +863,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorPicker extends Item{
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThemeItem extends Item{
|
||||
|
||||
@Override
|
||||
@@ -884,7 +880,11 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}
|
||||
|
||||
private class SmallTextItem extends Item {
|
||||
private String text;
|
||||
private final String text;
|
||||
|
||||
public SmallTextItem(@StringRes int text) {
|
||||
this.text = getString(text);
|
||||
}
|
||||
|
||||
public SmallTextItem(String text) {
|
||||
this.text = text;
|
||||
@@ -928,12 +928,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public TextItem(String text, Runnable onClick, @DrawableRes int icon){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
this.icon=icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
@@ -958,11 +952,9 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
|
||||
private class FooterItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
|
||||
public FooterItem(String text, Runnable onClick){
|
||||
public FooterItem(String text){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1128,13 +1120,11 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonViewHolder extends BindableViewHolder<ButtonItem>{
|
||||
private final Button button;
|
||||
private final ImageView icon;
|
||||
private final TextView text;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public ButtonViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_button, list);
|
||||
text=findViewById(R.id.text);
|
||||
@@ -1142,10 +1132,17 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
button=findViewById(R.id.button);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(ButtonItem item){
|
||||
text.setText(item.text);
|
||||
icon.setImageResource(item.icon);
|
||||
icon.setVisibility(item.icon == 0 ? View.GONE : View.VISIBLE);
|
||||
icon.setImageResource(item.icon == 0 ? 0 : item.icon);
|
||||
// reset listeners before letting the button consumer consume the button
|
||||
// (and potentially set some listeners, but not others)
|
||||
button.setOnTouchListener(null);
|
||||
button.setOnClickListener(null);
|
||||
button.setOnLongClickListener(null);
|
||||
item.buttonConsumer.accept(button);
|
||||
}
|
||||
}
|
||||
@@ -1241,7 +1238,7 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterViewHolder extends BindableViewHolder<FooterItem> implements UsableRecyclerView.Clickable{
|
||||
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
|
||||
private final TextView text;
|
||||
public FooterViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_footer, list);
|
||||
@@ -1252,11 +1249,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
public void onBind(FooterItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
@@ -1297,10 +1289,10 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.mo_update_available, info.version));
|
||||
text.setText(getString(R.string.sk_update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
}else{
|
||||
text.setText(getString(R.string.mo_update_ready, info.version));
|
||||
text.setText(getString(R.string.sk_update_ready, info.version));
|
||||
button.setText(R.string.install_update);
|
||||
}
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
@@ -1317,7 +1309,6 @@ public class SettingsFragment extends MastodonToolbarFragment {
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
changelog.setText(info.changelog);
|
||||
// changelog.setText(getString(R.string.sk_changelog, info.changelog));
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -24,13 +25,13 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
private String id;
|
||||
|
||||
private String id, url;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
id=getArguments().getString("id");
|
||||
url=getArguments().getString("url");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, Filter.FilterContext.HOME);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null);
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
@@ -157,4 +158,14 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
|
||||
@@ -36,9 +37,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
|
||||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME);
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
|
||||
}
|
||||
|
||||
protected abstract Filter.FilterContext getFilterContext();
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Status s){
|
||||
if(!knownAccounts.containsKey(s.account.id))
|
||||
@@ -64,7 +67,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
Status status=getContentStatusByID(id);
|
||||
if(status==null)
|
||||
return;
|
||||
if(status.reloadWhenClicked){
|
||||
if(status.isRemote){
|
||||
UiUtils.lookupStatus(getContext(), status, accountID, null, status1 -> {
|
||||
status1.filterRevealed = true;
|
||||
Bundle args=new Bundle();
|
||||
@@ -79,7 +82,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
status.filterRevealed = true;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
@@ -194,7 +197,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
|
||||
}
|
||||
|
||||
@@ -1,42 +1,58 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.DomainManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
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.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
||||
protected Status mainStatus;
|
||||
|
||||
@Override
|
||||
public String getDomain() {
|
||||
return mainStatus.url;
|
||||
}
|
||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||
protected Status mainStatus, updatedStatus;
|
||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||
protected boolean contextInitiallyRendered;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -48,64 +64,83 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
|
||||
DomainManager.getInstance().setCurrentDomain(getDomain());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
for(StatusDisplayItem item:items){
|
||||
// "what the fuck is a deque"? yes
|
||||
// (it's just so the last-added item automatically comes first when looping over it)
|
||||
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
|
||||
|
||||
// modifying hidden filtered items if status is displayed as a warning
|
||||
List<StatusDisplayItem> itemsToModify =
|
||||
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
|
||||
? warning.filteredItems
|
||||
: items;
|
||||
|
||||
for(int i = 0; i < itemsToModify.size(); i++){
|
||||
StatusDisplayItem item = itemsToModify.get(i);
|
||||
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
|
||||
if (ancestryInfo != null) {
|
||||
item.setAncestryInfo(
|
||||
ancestryInfo.descendantNeighbor != null,
|
||||
ancestryInfo.ancestoringNeighbor != null,
|
||||
s.id.equals(mainStatus.id),
|
||||
Optional.ofNullable(ancestryInfo.ancestoringNeighbor)
|
||||
.map(ancestor -> ancestor.id.equals(mainStatus.id))
|
||||
.orElse(false)
|
||||
);
|
||||
}
|
||||
|
||||
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
|
||||
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
|
||||
deleteTheseItems.add(i);
|
||||
}
|
||||
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
if(item instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
else if(item instanceof FooterStatusDisplayItem footer)
|
||||
footer.hideCounts=true;
|
||||
}
|
||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
|
||||
}
|
||||
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||
if(s.id.equals(mainStatus.id)) {
|
||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, getAccountID(), s.getContentStatus()));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if (refreshing) loadMainStatus();
|
||||
currentRequest=new GetStatusContext(mainStatus.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(StatusContext result){
|
||||
if (getActivity() == null) return;
|
||||
if (getContext() == null) return;
|
||||
Map<String, Status> oldData = null;
|
||||
if(refreshing){
|
||||
oldData = new HashMap<>(data.size());
|
||||
for (Status s : data) oldData.put(s.id, s);
|
||||
data.clear();
|
||||
ancestryMap.clear();
|
||||
displayItems.clear();
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
}
|
||||
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
|
||||
if(instance.pleroma != null){
|
||||
List<String> threadIds=new ArrayList<>();
|
||||
threadIds.add(mainStatus.id);
|
||||
for(Status s:result.descendants){
|
||||
if(threadIds.contains(s.inReplyToId)){
|
||||
threadIds.add(s.id);
|
||||
}
|
||||
}
|
||||
threadIds.add(mainStatus.inReplyToId);
|
||||
for(int i=result.ancestors.size()-1; i >= 0; i--){
|
||||
Status s=result.ancestors.get(i);
|
||||
if(s.inReplyToId != null && threadIds.contains(s.id)){
|
||||
threadIds.add(s.inReplyToId);
|
||||
}
|
||||
}
|
||||
|
||||
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
|
||||
result.descendants=getDescendantsOrdered(mainStatus.id,
|
||||
result.descendants.stream()
|
||||
.filter(s -> threadIds.contains(s.id))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
// TODO: figure out how this code works
|
||||
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
|
||||
|
||||
result.descendants=filterStatuses(result.descendants);
|
||||
result.ancestors=filterStatuses(result.ancestors);
|
||||
|
||||
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
|
||||
ancestryMap.put(i.status.id, i);
|
||||
}
|
||||
|
||||
if(footerProgress!=null)
|
||||
footerProgress.setVisibility(View.GONE);
|
||||
data.addAll(result.descendants);
|
||||
@@ -114,19 +149,141 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
||||
int count=displayItems.size();
|
||||
if(!refreshing)
|
||||
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
|
||||
prependItems(result.ancestors, !refreshing);
|
||||
int prependedCount = prependItems(result.ancestors, !refreshing);
|
||||
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
|
||||
displayItems.remove(prependedCount);
|
||||
adapter.notifyItemRemoved(prependedCount);
|
||||
count--;
|
||||
}
|
||||
|
||||
for (Status s : data) {
|
||||
Status oldStatus = oldData == null ? null : oldData.get(s.id);
|
||||
// restore previous spoiler/filter revealed states when refreshing
|
||||
if (oldStatus != null) {
|
||||
s.spoilerRevealed = oldStatus.spoilerRevealed;
|
||||
s.filterRevealed = oldStatus.filterRevealed;
|
||||
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
|
||||
s.spoilerText != null &&
|
||||
s.spoilerText.equals(mainStatus.spoilerText) &&
|
||||
mainStatus.spoilerRevealed) {
|
||||
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
|
||||
s.spoilerRevealed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataLoaded();
|
||||
if(refreshing){
|
||||
refreshDone();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
list.scrollToPosition(displayItems.size()-count);
|
||||
|
||||
// no animation is going to happen, so proceeding to apply right now
|
||||
if (data.size() == 1) {
|
||||
contextInitiallyRendered = true;
|
||||
// for the case that the main status has already finished loading
|
||||
maybeApplyMainStatus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<Status> getDescendantsOrdered(String id, List<Status> statuses){
|
||||
private void loadMainStatus() {
|
||||
new GetStatusByID(mainStatus.id)
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Status status) {
|
||||
if (getContext() == null || status == null) return;
|
||||
updatedStatus = status;
|
||||
// for the case that the context has already loaded (and the animation has
|
||||
// already finished), falling back to applying it ourselves:
|
||||
maybeApplyMainStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
protected Object maybeApplyMainStatus() {
|
||||
if (updatedStatus == null || !contextInitiallyRendered) return null;
|
||||
|
||||
// restore revealed states for main status because it gets updated after doLoadData
|
||||
updatedStatus.filterRevealed = mainStatus.filterRevealed;
|
||||
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
|
||||
|
||||
// returning fired event object to facilitate testing
|
||||
Object event;
|
||||
if (updatedStatus.editedAt != null &&
|
||||
(mainStatus.editedAt == null ||
|
||||
updatedStatus.editedAt.isAfter(mainStatus.editedAt))) {
|
||||
event = new StatusUpdatedEvent(updatedStatus);
|
||||
} else {
|
||||
event = new StatusCountersUpdatedEvent(updatedStatus);
|
||||
}
|
||||
|
||||
mainStatus = updatedStatus;
|
||||
updatedStatus = null;
|
||||
E.post(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
|
||||
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
|
||||
|
||||
List<Status> statuses = new ArrayList<>(context.ancestors);
|
||||
statuses.add(mainStatus);
|
||||
statuses.addAll(context.descendants);
|
||||
|
||||
int count = statuses.size();
|
||||
for (int index = 0; index < count; index++) {
|
||||
Status current = statuses.get(index);
|
||||
ancestry.add(new NeighborAncestryInfo(
|
||||
current,
|
||||
// descendant neighbor
|
||||
Optional
|
||||
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
|
||||
.filter(s -> s.inReplyToId.equals(current.id))
|
||||
.orElse(null),
|
||||
// ancestoring neighbor
|
||||
Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
|
||||
.filter(ancestor -> Optional.ofNullable(ancestor.descendantNeighbor)
|
||||
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
|
||||
.orElse(false))
|
||||
.map(a -> a.status)
|
||||
.orElse(null)
|
||||
));
|
||||
}
|
||||
|
||||
return ancestry;
|
||||
}
|
||||
|
||||
public static void sortStatusContext(Status mainStatus, StatusContext context) {
|
||||
List<String> threadIds=new ArrayList<>();
|
||||
threadIds.add(mainStatus.id);
|
||||
for(Status s:context.descendants){
|
||||
if(threadIds.contains(s.inReplyToId)){
|
||||
threadIds.add(s.id);
|
||||
}
|
||||
}
|
||||
threadIds.add(mainStatus.inReplyToId);
|
||||
for(int i=context.ancestors.size()-1; i >= 0; i--){
|
||||
Status s=context.ancestors.get(i);
|
||||
if(s.inReplyToId != null && threadIds.contains(s.id)){
|
||||
threadIds.add(s.inReplyToId);
|
||||
}
|
||||
}
|
||||
|
||||
context.ancestors=context.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
|
||||
context.descendants=getDescendantsOrdered(mainStatus.id,
|
||||
context.descendants.stream()
|
||||
.filter(s -> threadIds.contains(s.id))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private static List<Status> getDescendantsOrdered(String id, List<Status> statuses){
|
||||
List<Status> out=new ArrayList<>();
|
||||
for(Status s:getDirectDescendants(id, statuses)){
|
||||
out.add(s);
|
||||
@@ -138,14 +295,14 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<Status> getDirectDescendants(String id, List<Status> statuses){
|
||||
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
|
||||
return statuses.stream()
|
||||
.filter(s -> s.inReplyToId.equals(id))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
|
||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
|
||||
return statuses.stream()
|
||||
.filter(statusFilterPredicate)
|
||||
.collect(Collectors.toList());
|
||||
@@ -167,17 +324,28 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
||||
showContent();
|
||||
if(!loaded)
|
||||
footerProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
list.setItemAnimator(new BetterItemAnimator() {
|
||||
@Override
|
||||
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.onAnimationFinished(viewHolder);
|
||||
contextInitiallyRendered = true;
|
||||
// for the case that both requests are already done (and thus won't apply it)
|
||||
maybeApplyMainStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
|
||||
data.add(ev.status);
|
||||
onAppendItems(Collections.singletonList(ev.status));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemEnabled(String id){
|
||||
return !id.equals(mainStatus.id);
|
||||
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,4 +357,52 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
|
||||
public boolean wantsLightNavigationBar(){
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.THREAD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(mainStatus.url);
|
||||
}
|
||||
|
||||
protected static class NeighborAncestryInfo {
|
||||
protected Status status, descendantNeighbor, ancestoringNeighbor;
|
||||
|
||||
protected NeighborAncestryInfo(@NonNull Status status, Status descendantNeighbor, Status ancestoringNeighbor) {
|
||||
this.status = status;
|
||||
this.descendantNeighbor = descendantNeighbor;
|
||||
this.ancestoringNeighbor = ancestoringNeighbor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
NeighborAncestryInfo that = (NeighborAncestryInfo) o;
|
||||
return status.equals(that.status)
|
||||
&& Objects.equals(descendantNeighbor, that.descendantNeighbor)
|
||||
&& Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onErrorRetryClick(){
|
||||
if(preloadingFailed){
|
||||
preloadingFailed=false;
|
||||
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
|
||||
V.setVisibilityAnimated(footerError, View.GONE);
|
||||
doLoadData();
|
||||
return;
|
||||
}
|
||||
super.onErrorRetryClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,68 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
|
||||
protected Account account;
|
||||
protected String initialSubtitle = "";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
if (getArguments().containsKey("remoteAccount")) {
|
||||
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||
}
|
||||
setTitle("@"+account.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma()
|
||||
? "/users/" + account.id
|
||||
: '@' + account.acct).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteDomain() {
|
||||
return account.getDomainFromURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getCurrentInfo() {
|
||||
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MastodonAPIRequest<Account> loadRemoteInfo() {
|
||||
return new GetAccountByHandle(account.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AccountSession getRemoteSession() {
|
||||
return Optional.ofNullable(remoteInfo)
|
||||
.map(AccountSessionManager.getInstance()::tryGetAccount)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoteLoadingFailed() {
|
||||
super.onRemoteLoadingFailed();
|
||||
String prefix = initialSubtitle == null ? "" :
|
||||
initialSubtitle + " " + getContext().getString(R.string.sk_separator) + " ";
|
||||
String str = prefix +
|
||||
getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain);
|
||||
setSubtitle(str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Animatable;
|
||||
@@ -23,7 +25,8 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||
import org.joinmastodon.android.fragments.HasAccountID;
|
||||
import org.joinmastodon.android.fragments.ListsFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.RecyclerFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
@@ -34,6 +37,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -44,6 +48,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
@@ -57,7 +62,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> {
|
||||
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
@@ -79,10 +84,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
if(refreshing){
|
||||
relationships.clear();
|
||||
}
|
||||
if(!d.isEmpty() && !d.get(0).account.reloadWhenClicked){
|
||||
loadRelationships(d);
|
||||
}
|
||||
|
||||
loadRelationships(d);
|
||||
super.onDataLoaded(d, more);
|
||||
}
|
||||
|
||||
@@ -175,6 +177,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -235,22 +247,20 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(AccountItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
username.setText("@"+ (item.account.isRemote
|
||||
? item.account.getFullyQualifiedName()
|
||||
: item.account.acct));
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
public void bindRelationship(){
|
||||
Relationship rel=relationships.get(item.account.id);
|
||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||
if(item.account.reloadWhenClicked){
|
||||
button.setVisibility(View.VISIBLE);
|
||||
button.setText(R.string.button_follow);
|
||||
} else {
|
||||
button.setVisibility(View.GONE);
|
||||
}
|
||||
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||
button.setVisibility(View.GONE);
|
||||
}else{
|
||||
button.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButton(rel, button);
|
||||
@@ -277,18 +287,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(item.account.reloadWhenClicked){
|
||||
UiUtils.lookupAccount(getContext(), item.account, accountID, null, account -> {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
});
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
|
||||
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -406,7 +408,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
args.putString("account", accountID);
|
||||
args.putString("profileAccount", account.id);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
||||
Nav.go(getActivity(), ListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -429,5 +431,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof AccountItem i && i.account.url.equals(account.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||