Compare commits
1176 Commits
feature/re
...
v2.0.3+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937304f27b | ||
|
|
6b4ce0ea69 | ||
|
|
7f0c4860f8 | ||
|
|
9b4c70a5ed | ||
|
|
49137273ae | ||
|
|
647e3e5e85 | ||
|
|
4920bf63e3 | ||
|
|
0afcdb2cdf | ||
|
|
d96c3c3c8a | ||
|
|
f5e5408d70 | ||
|
|
d62899c990 | ||
|
|
e5bdeba1d7 | ||
|
|
8d7db7774f | ||
|
|
78d22c670c | ||
|
|
0a679109f5 | ||
|
|
e843142b7e | ||
|
|
72e728f655 | ||
|
|
ef56792f56 | ||
|
|
504a6959e8 | ||
|
|
6054a3d65c | ||
|
|
f50eac02d8 | ||
|
|
9634db9061 | ||
|
|
97e3e283dd | ||
|
|
f1e233569b | ||
|
|
04dd637fa9 | ||
|
|
c48a4105a9 | ||
|
|
aac53d949b | ||
|
|
9bb4e5b467 | ||
|
|
fb0391d5cd | ||
|
|
e4d898c903 | ||
|
|
da222f75bb | ||
|
|
25fbd91eb3 | ||
|
|
d458cca7bf | ||
|
|
3933a61b5a | ||
|
|
29092bbf36 | ||
|
|
a33d2578c9 | ||
|
|
9afe4b5ac6 | ||
|
|
6782006b05 | ||
|
|
90bdbefd48 | ||
|
|
b7bcf1082e | ||
|
|
ba9bbc5b6e | ||
|
|
0fecfbd50c | ||
|
|
0031dc6119 | ||
|
|
79e606698e | ||
|
|
3c9fc43780 | ||
|
|
adb9b7394a | ||
|
|
6191fdfaef | ||
|
|
446754e8a6 | ||
|
|
30c67b0b39 | ||
|
|
1043ea7b11 | ||
|
|
b449bcd006 | ||
|
|
a9d513b564 | ||
|
|
5cef527810 | ||
|
|
8bb907747d | ||
|
|
9c889f8df3 | ||
|
|
cbc164d844 | ||
|
|
b8e3060887 | ||
|
|
1aa1ede421 | ||
|
|
480dba7629 | ||
|
|
9b9c66a149 | ||
|
|
0f5eb923ee | ||
|
|
7f521b3129 | ||
|
|
9ce217d1f2 | ||
|
|
7b1bd3ccad | ||
|
|
13b1cbde6b | ||
|
|
8d898a1a78 | ||
|
|
51c2890ede | ||
|
|
03642faa9c | ||
|
|
084c6e1e59 | ||
|
|
90ed28e7a0 | ||
|
|
d2b45c1c84 | ||
|
|
a119ba5f80 | ||
|
|
8c1191a08f | ||
|
|
4275d596e6 | ||
|
|
2709d5226d | ||
|
|
8f613e3255 | ||
|
|
6831e846cf | ||
|
|
034eb9427d | ||
|
|
f73c325db3 | ||
|
|
5e2b11c504 | ||
|
|
ec13133431 | ||
|
|
a8a56a3ed8 | ||
|
|
4e9c7c4de2 | ||
|
|
ffb7894098 | ||
|
|
0d9520ac45 | ||
|
|
be852e57df | ||
|
|
157b38b8ae | ||
|
|
83196a1a0d | ||
|
|
306225b054 | ||
|
|
6efc71d8d2 | ||
|
|
cc4cd4d3f8 | ||
|
|
00e3292205 | ||
|
|
316952423c | ||
|
|
61c2abd014 | ||
|
|
ee5f299b90 | ||
|
|
f153846381 | ||
|
|
0656db0858 | ||
|
|
7f250cb8df | ||
|
|
a1e73eca89 | ||
|
|
1dc6936da6 | ||
|
|
0431d80a8d | ||
|
|
eaa78093f7 | ||
|
|
25b7151fde | ||
|
|
0438b579b6 | ||
|
|
afa50a4e8c | ||
|
|
85bdb0067b | ||
|
|
760cbc7f9a | ||
|
|
d0e34fcd90 | ||
|
|
da434b9a9b | ||
|
|
48863dd510 | ||
|
|
2ca34278f9 | ||
|
|
a79779f813 | ||
|
|
cc83f2baf3 | ||
|
|
728496b831 | ||
|
|
bbc99162c6 | ||
|
|
eed3af9e3e | ||
|
|
50187ff376 | ||
|
|
5f30919fb4 | ||
|
|
14c3cfac85 | ||
|
|
e978f02765 | ||
|
|
8d877c480f | ||
|
|
c53efee9a1 | ||
|
|
148c461e86 | ||
|
|
fcadb9883d | ||
|
|
bb6491e10a | ||
|
|
6248ccf376 | ||
|
|
c9e08f36fa | ||
|
|
10b95d753b | ||
|
|
c3989083cf | ||
|
|
01db585094 | ||
|
|
cc67cb330c | ||
|
|
52ed3c5a04 | ||
|
|
5976f6230a | ||
|
|
3553f03a95 | ||
|
|
d6e2d889c3 | ||
|
|
a777b3b450 | ||
|
|
9957efbea0 | ||
|
|
22e7b9730f | ||
|
|
91470b8509 | ||
|
|
c9d5327328 | ||
|
|
1aa61b72e5 | ||
|
|
3ca5edc3fc | ||
|
|
a092ebaeb3 | ||
|
|
5b9e84c255 | ||
|
|
9c058b926f | ||
|
|
4f2d2ae6e8 | ||
|
|
75aa26a018 | ||
|
|
0f795254e5 | ||
|
|
33592f0a83 | ||
|
|
d6fd01eaca | ||
|
|
1cdc58378a | ||
|
|
584b11fce3 | ||
|
|
fe2039062b | ||
|
|
0269756b52 | ||
|
|
df1a6cf764 | ||
|
|
6d2385b6b3 | ||
|
|
44eaa36cef | ||
|
|
50b40c4a07 | ||
|
|
ee6e0ff26c | ||
|
|
4d9574bf38 | ||
|
|
813be9a2be | ||
|
|
cc76ebfafb | ||
|
|
7989ee0243 | ||
|
|
3aa1997cfd | ||
|
|
c3da15552e | ||
|
|
a014fe9443 | ||
|
|
92551d4ca3 | ||
|
|
8010858e85 | ||
|
|
4efb4875b0 | ||
|
|
c5d041e46d | ||
|
|
53c2223aae | ||
|
|
25034ac0ae | ||
|
|
ac9de72b75 | ||
|
|
1f48ad93f2 | ||
|
|
38f7f7aa00 | ||
|
|
fe8175c63a | ||
|
|
2d9e01bbc1 | ||
|
|
022a227b08 | ||
|
|
a2228259f1 | ||
|
|
a61af7c56f | ||
|
|
5d6a646976 | ||
|
|
628d0d7492 | ||
|
|
b76c8745ec | ||
|
|
51e67bc441 | ||
|
|
8887f75b70 | ||
|
|
9436a838c0 | ||
|
|
ef120fa36f | ||
|
|
8c6385e2c5 | ||
|
|
0bd85d9905 | ||
|
|
ce0dab7b28 | ||
|
|
bdcebf1576 | ||
|
|
ddcc5670ce | ||
|
|
86afa184e2 | ||
|
|
77f341f139 | ||
|
|
918b5d99c2 | ||
|
|
7a098d6eff | ||
|
|
239b6f8202 | ||
|
|
8404c79148 | ||
|
|
5b2d04e09d | ||
|
|
6bd13f99d2 | ||
|
|
2e8e12c1c8 | ||
|
|
17929a6b2d | ||
|
|
9455eaf820 | ||
|
|
cc054487ba | ||
|
|
e2df320d00 | ||
|
|
d74313f996 | ||
|
|
b3cab67049 | ||
|
|
996f0b22b9 | ||
|
|
67952ea98e | ||
|
|
7a02ca435f | ||
|
|
71f81283f5 | ||
|
|
058c7c3c33 | ||
|
|
870e33879b | ||
|
|
3ca82bdfc5 | ||
|
|
4721bad286 | ||
|
|
f040cf2f07 | ||
|
|
8d50717c90 | ||
|
|
2512ad3c95 | ||
|
|
8d55f62da9 | ||
|
|
bc7e007634 | ||
|
|
1f3c87e0c7 | ||
|
|
ee0048a406 | ||
|
|
14dcc769f2 | ||
|
|
f2e6255eb3 | ||
|
|
7d392e20fb | ||
|
|
3f2e6d2be6 | ||
|
|
a74c285f77 | ||
|
|
8aec4a2717 | ||
|
|
a106193039 | ||
|
|
5736605771 | ||
|
|
9f0a710e40 | ||
|
|
f6f8cfb8e8 | ||
|
|
4e28f011dd | ||
|
|
4bb255e0bb | ||
|
|
a48d545989 | ||
|
|
e58cf54a5a | ||
|
|
2a45776f01 | ||
|
|
332aa906b5 | ||
|
|
ef624dfe30 | ||
|
|
1b0b9bedea | ||
|
|
e2672bea6e | ||
|
|
5916001462 | ||
|
|
cacd403b89 | ||
|
|
2732f710da | ||
|
|
070268eafd | ||
|
|
7149971351 | ||
|
|
f46668c023 | ||
|
|
74d0937078 | ||
|
|
b236e88ef2 | ||
|
|
12acf6761b | ||
|
|
8522f1fd29 | ||
|
|
7677ad39ca | ||
|
|
73e08faee9 | ||
|
|
02dc7711e4 | ||
|
|
67b4d80e5b | ||
|
|
5168d2bb39 | ||
|
|
57190a75bf | ||
|
|
f10e865895 | ||
|
|
91b4dc412b | ||
|
|
3cfea0e660 | ||
|
|
c896fd8df8 | ||
|
|
72cd987284 | ||
|
|
d868d05080 | ||
|
|
9dfb039a69 | ||
|
|
f2920e877b | ||
|
|
ecb0b3f9d7 | ||
|
|
13ff54663c | ||
|
|
604d9b008a | ||
|
|
7044a69a71 | ||
|
|
67c128be69 | ||
|
|
1ecbbc2d4b | ||
|
|
82c481b014 | ||
|
|
e7da6d7897 | ||
|
|
e9f1e3038b | ||
|
|
a591096819 | ||
|
|
f1ef60475f | ||
|
|
ce75bb3984 | ||
|
|
d59235e04c | ||
|
|
42f8f7e58f | ||
|
|
f9cbc9ae27 | ||
|
|
a50d6599bf | ||
|
|
cc578b496e | ||
|
|
662944d246 | ||
|
|
42810df4a5 | ||
|
|
c3a058d2e1 | ||
|
|
ce5d835ae5 | ||
|
|
60dd561729 | ||
|
|
08c9f9ad7d | ||
|
|
d47907906d | ||
|
|
935f0f6e05 | ||
|
|
ac0b21d574 | ||
|
|
9b5d05369f | ||
|
|
489f9f5e59 | ||
|
|
e401dc8d6e | ||
|
|
3e89087aba | ||
|
|
e62f7c23c9 | ||
|
|
91ed7d49b5 | ||
|
|
c9e9abd811 | ||
|
|
b1e5023f62 | ||
|
|
604690b3f5 | ||
|
|
13ada6ecc6 | ||
|
|
b897eb913e | ||
|
|
c25602a650 | ||
|
|
2defc9af3f | ||
|
|
446525389b | ||
|
|
756b30d04f | ||
|
|
51ec842815 | ||
|
|
c38822849e | ||
|
|
3c69201f67 | ||
|
|
ed9d701406 | ||
|
|
e70c5aa2e9 | ||
|
|
0c4589b257 | ||
|
|
84d08392fb | ||
|
|
8ff117308d | ||
|
|
b6c703adbc | ||
|
|
22e6934de5 | ||
|
|
1b8a1d69ac | ||
|
|
b6ae83937b | ||
|
|
7115556663 | ||
|
|
cb3296661e | ||
|
|
6dd20a6df9 | ||
|
|
71c1d0e59a | ||
|
|
2b275e1ff7 | ||
|
|
8520ee42cb | ||
|
|
a7fcae1033 | ||
|
|
19bd189b33 | ||
|
|
2d5089c047 | ||
|
|
be7469bd54 | ||
|
|
146d8daa6e | ||
|
|
f3928d9e09 | ||
|
|
d4090d459d | ||
|
|
7dd7554c08 | ||
|
|
9de9a1d97d | ||
|
|
04ee366fbe | ||
|
|
c8784150fc | ||
|
|
7b7bccb37a | ||
|
|
84e2636bca | ||
|
|
dc73613b56 | ||
|
|
fd8868ef4d | ||
|
|
f443d659a3 | ||
|
|
24eb82a79d | ||
|
|
43d806fb01 | ||
|
|
127df0b8e0 | ||
|
|
c2989df902 | ||
|
|
3f9ee99b69 | ||
|
|
d47c4e63d7 | ||
|
|
7204c4e804 | ||
|
|
f1131cf8e7 | ||
|
|
5b9a8beb07 | ||
|
|
9e18e35c66 | ||
|
|
9db3dfa955 | ||
|
|
4a3e56d300 | ||
|
|
3bb4125c50 | ||
|
|
ce58883618 | ||
|
|
e7d3c60bac | ||
|
|
08e90139ad | ||
|
|
0d4dc34453 | ||
|
|
fe142c4626 | ||
|
|
d8dfa6017d | ||
|
|
b7a5d4296b | ||
|
|
85d4c1fc24 | ||
|
|
66489d79be | ||
|
|
30a66a26c6 | ||
|
|
fbc3081e68 | ||
|
|
4ac7615cfb | ||
|
|
a96b0d06a4 | ||
|
|
7a66c94907 | ||
|
|
4e38bc5769 | ||
|
|
0dc428dbd6 | ||
|
|
6ac7fc94ea | ||
|
|
af28ed1783 | ||
|
|
00daf084f2 | ||
|
|
5518848e28 | ||
|
|
6ded856b2f | ||
|
|
d302f5132e | ||
|
|
012e29ee3a | ||
|
|
dbe9579d7f | ||
|
|
c8d0221d9b | ||
|
|
885c663d93 | ||
|
|
3b399d5815 | ||
|
|
9f18d1bc8b | ||
|
|
9a8cf61e38 | ||
|
|
eb822282c0 | ||
|
|
273823a65f | ||
|
|
f2aa1400c5 | ||
|
|
cf8a9e1823 | ||
|
|
b3320d534b | ||
|
|
f58b4c2989 | ||
|
|
47b3b1e307 | ||
|
|
e5649c4a42 | ||
|
|
cea77eca02 | ||
|
|
4fbbcfba59 | ||
|
|
6a24f70537 | ||
|
|
4681160924 | ||
|
|
6f8c2f4a44 | ||
|
|
69de9dce38 | ||
|
|
721ae9c68d | ||
|
|
6a6ed89d29 | ||
|
|
3e4377a366 | ||
|
|
287de66e0c | ||
|
|
df2ae9d964 | ||
|
|
277282d7f5 | ||
|
|
4f983829b7 | ||
|
|
ef55f1f49b | ||
|
|
aebf7e9f1f | ||
|
|
a014ce6eb5 | ||
|
|
aed1efceb9 | ||
|
|
124c375b14 | ||
|
|
1d46e22a7f | ||
|
|
28c334429d | ||
|
|
4726f98d4f | ||
|
|
985f382436 | ||
|
|
f822c788b0 | ||
|
|
664851cac5 | ||
|
|
46f8982aa6 | ||
|
|
7b3cec9289 | ||
|
|
a3f227cb8d | ||
|
|
d71feb2cfc | ||
|
|
b69565e9e6 | ||
|
|
bf996feccf | ||
|
|
b1143b0eec | ||
|
|
4c6914c271 | ||
|
|
e03a62064e | ||
|
|
ce977163c2 | ||
|
|
ded74bda83 | ||
|
|
7180113397 | ||
|
|
81ac8a3bc9 | ||
|
|
4b74da5d38 | ||
|
|
0375cfa260 | ||
|
|
52108a675a | ||
|
|
406e95d3f7 | ||
|
|
a9f355dea9 | ||
|
|
1a63c12327 | ||
|
|
7bc2f8b352 | ||
|
|
d56ef227a7 | ||
|
|
fcc371ab5a | ||
|
|
9f43a99772 | ||
|
|
9fc5a4e390 | ||
|
|
a2b3f873f6 | ||
|
|
f6c1509e48 | ||
|
|
d9bbb32a28 | ||
|
|
d2ef6e77af | ||
|
|
6133e9bac3 | ||
|
|
6ca48f35f1 | ||
|
|
cb016b4383 | ||
|
|
d0b21df28b | ||
|
|
ce48ee888a | ||
|
|
5cd8bc5a46 | ||
|
|
4109cd75d3 | ||
|
|
97a889e019 | ||
|
|
b9a1b3591d | ||
|
|
fde84e3cfb | ||
|
|
0985eb4fac | ||
|
|
5957c1a221 | ||
|
|
e7e34aa2c8 | ||
|
|
b8742591b8 | ||
|
|
50e73ac12e | ||
|
|
9ed277a9b2 | ||
|
|
c9e2984d68 | ||
|
|
00aaff10a7 | ||
|
|
6da4256adf | ||
|
|
da9c826791 | ||
|
|
bed02e248e | ||
|
|
f53531889f | ||
|
|
ef3246ae2a | ||
|
|
5504b534f7 | ||
|
|
0e553d7868 | ||
|
|
38be51367e | ||
|
|
0d4af0970c | ||
|
|
b51335ffd6 | ||
|
|
444fa07984 | ||
|
|
feb0f304fb | ||
|
|
ccd4a1aa9f | ||
|
|
b3723c2977 | ||
|
|
c5b70a9ada | ||
|
|
82789179e7 | ||
|
|
b0a309a817 | ||
|
|
32a27b6e59 | ||
|
|
f73a318dad | ||
|
|
4fff2c5f5c | ||
|
|
81abac657f | ||
|
|
74decd3ec7 | ||
|
|
11d17d1f3f | ||
|
|
ca2384ba8c | ||
|
|
ded23342db | ||
|
|
a35c14865f | ||
|
|
0952d97557 | ||
|
|
e1db5f15ca | ||
|
|
a99741c732 | ||
|
|
d6c8e8afc1 | ||
|
|
879981e335 | ||
|
|
87840dd731 | ||
|
|
c9e467ac2f | ||
|
|
74a5e970d9 | ||
|
|
935de7d02e | ||
|
|
cb8fddd156 | ||
|
|
40ca834880 | ||
|
|
c10206ef6f | ||
|
|
e752d10e31 | ||
|
|
119bbc2b5c | ||
|
|
6136260adc | ||
|
|
979ee9fdff | ||
|
|
ab71e06ef1 | ||
|
|
6d487f011f | ||
|
|
50403cf674 | ||
|
|
276df264cf | ||
|
|
55e18cf9af | ||
|
|
e5392d3265 | ||
|
|
2de7c1d3b9 | ||
|
|
c21885139c | ||
|
|
275cbaa924 | ||
|
|
4b6c35c9c0 | ||
|
|
d339fa1e12 | ||
|
|
31a5fc9153 | ||
|
|
966f758d9f | ||
|
|
6979c5097d | ||
|
|
68005c762f | ||
|
|
06543d5fc2 | ||
|
|
a70d39065c | ||
|
|
dc75882cee | ||
|
|
e47f253c0e | ||
|
|
d05f3932b2 | ||
|
|
be425282a6 | ||
|
|
a04522ff72 | ||
|
|
90a93ffba6 | ||
|
|
069c55d4b9 | ||
|
|
1efaf4b605 | ||
|
|
ff74516ce2 | ||
|
|
aee4b7aaab | ||
|
|
1d94479bde | ||
|
|
85c95d899e | ||
|
|
18d4210e7d | ||
|
|
ac25bc6d42 | ||
|
|
ec05ef3b4c | ||
|
|
9827d97374 | ||
|
|
4fab92d516 | ||
|
|
44bc9c4e40 | ||
|
|
1030773ef6 | ||
|
|
1a0cb4b8c8 | ||
|
|
4295a3672c | ||
|
|
fd2a8fe230 | ||
|
|
e2d1eccfb9 | ||
|
|
bb4a52f03a | ||
|
|
50360059ce | ||
|
|
63bcef990b | ||
|
|
94eb6b5775 | ||
|
|
6595a088fb | ||
|
|
b463ef65ce | ||
|
|
b22a25e7af | ||
|
|
05d1d3e725 | ||
|
|
37261928c2 | ||
|
|
63132110d9 | ||
|
|
ffd7e415a2 | ||
|
|
d43664d018 | ||
|
|
8e06362ff8 | ||
|
|
625ccfd31f | ||
|
|
d7b5d242ff | ||
|
|
7973c87b9a | ||
|
|
3085b1507b | ||
|
|
f17ef17492 | ||
|
|
8d0a31d0f9 | ||
|
|
6c6d3fed05 | ||
|
|
b616e2e11b | ||
|
|
b18771bb79 | ||
|
|
40ef4c179a | ||
|
|
62212dc6c9 | ||
|
|
3ed2b67037 | ||
|
|
d240750606 | ||
|
|
ed009d3e2e | ||
|
|
3af7518cf4 | ||
|
|
91b9fdf5ce | ||
|
|
be2fa0f217 | ||
|
|
22f5667549 | ||
|
|
8115578e94 | ||
|
|
08dc122b6b | ||
|
|
e3199c009e | ||
|
|
a8d9b4538b | ||
|
|
2b7b3de043 | ||
|
|
2e09010151 | ||
|
|
3d79e87ec8 | ||
|
|
09ed3c647a | ||
|
|
172515ba0a | ||
|
|
a2f0fc8c87 | ||
|
|
888cee4556 | ||
|
|
c005e9bf18 | ||
|
|
64362968fc | ||
|
|
320027ca9b | ||
|
|
ad2678da7c | ||
|
|
c56c7448d0 | ||
|
|
731e67725c | ||
|
|
8178f81c85 | ||
|
|
6fbf00a132 | ||
|
|
968cde9e4c | ||
|
|
aa9caefed1 | ||
|
|
14bbe1ffef | ||
|
|
f2ab2acef7 | ||
|
|
81d1ecc5f8 | ||
|
|
513e6439ff | ||
|
|
1f0108b14e | ||
|
|
1433d0717e | ||
|
|
be91775f4b | ||
|
|
7b2f8d2be3 | ||
|
|
54c386ccec | ||
|
|
9d9e98959f | ||
|
|
a3cd7224bd | ||
|
|
6cf214f127 | ||
|
|
b6976fb519 | ||
|
|
871ada23ab | ||
|
|
040237de2b | ||
|
|
29b2a25840 | ||
|
|
bd2f05be67 | ||
|
|
ed075b276f | ||
|
|
dd25f3380a | ||
|
|
1a2d1efa29 | ||
|
|
f3e1fa4b2b | ||
|
|
fc307ff43f | ||
|
|
04304b3397 | ||
|
|
23f4b63195 | ||
|
|
edeae13dda | ||
|
|
44558534e9 | ||
|
|
2b760bb215 | ||
|
|
44154a987d | ||
|
|
7260db6668 | ||
|
|
1dadc51ddf | ||
|
|
fe85351869 | ||
|
|
74a83c6ac4 | ||
|
|
acb1369e88 | ||
|
|
8e0d74f9c2 | ||
|
|
df77ba61ad | ||
|
|
ed40f74d59 | ||
|
|
42e26bef68 | ||
|
|
af60adb55f | ||
|
|
b94741feae | ||
|
|
e43d6c35d8 | ||
|
|
4a6f9e80b1 | ||
|
|
ec02680507 | ||
|
|
5fc569a45a | ||
|
|
4bc9c5691d | ||
|
|
19b68855ac | ||
|
|
70fdfb612e | ||
|
|
0a32c217d8 | ||
|
|
5dfa9237ad | ||
|
|
573ff75498 | ||
|
|
87c37df370 | ||
|
|
7fb0944e66 | ||
|
|
35c8a3d121 | ||
|
|
9e58413d1a | ||
|
|
90e60aef84 | ||
|
|
8547ce05ed | ||
|
|
0825faee5c | ||
|
|
d43a697df7 | ||
|
|
3b742c4391 | ||
|
|
a43a396043 | ||
|
|
bcb4fac553 | ||
|
|
35bf858a83 | ||
|
|
870bfaf08c | ||
|
|
c4238fb19b | ||
|
|
ba7aeb358b | ||
|
|
6f3fd4d454 | ||
|
|
c890195567 | ||
|
|
b50a327b17 | ||
|
|
97547f334f | ||
|
|
1ab953d819 | ||
|
|
dbe7eb25ff | ||
|
|
45ecec09f5 | ||
|
|
9b4556d293 | ||
|
|
307d483a56 | ||
|
|
9612248695 | ||
|
|
1f63401e5b | ||
|
|
d35ec18a88 | ||
|
|
b93b1847c3 | ||
|
|
cd46ed565f | ||
|
|
4a0e4edef8 | ||
|
|
2ea7333daa | ||
|
|
fa7a66809d | ||
|
|
71884ab760 | ||
|
|
f31205c670 | ||
|
|
0091ae87ce | ||
|
|
17957b69d1 | ||
|
|
ad13b1e927 | ||
|
|
a354ea80ab | ||
|
|
9f65b8112a | ||
|
|
6ac5d957fe | ||
|
|
4258c55b88 | ||
|
|
969f29e2e9 | ||
|
|
68921d0f0b | ||
|
|
c4ac4ee173 | ||
|
|
659b4e2fcd | ||
|
|
24e5bda8d3 | ||
|
|
02b1ad8d7a | ||
|
|
47eeb01b75 | ||
|
|
4288814138 | ||
|
|
ac4458e106 | ||
|
|
3d24b2de10 | ||
|
|
ed994b23e9 | ||
|
|
8c4678aba5 | ||
|
|
3d5fb2dfea | ||
|
|
ef6238b593 | ||
|
|
bc9bec3d66 | ||
|
|
d16e199dd1 | ||
|
|
a9c2df2e83 | ||
|
|
4673a4b9f7 | ||
|
|
a24b4363d7 | ||
|
|
02ddad22e7 | ||
|
|
a705512dc5 | ||
|
|
cfd6954755 | ||
|
|
702ac43f86 | ||
|
|
d4a5286895 | ||
|
|
1b4579346b | ||
|
|
0665b8dd3b | ||
|
|
853124e2ce | ||
|
|
5dcd6e5a0d | ||
|
|
6f25c8be0f | ||
|
|
1db4b1319e | ||
|
|
76a97fcb47 | ||
|
|
4baaa39f35 | ||
|
|
52f025ae5a | ||
|
|
14b805e883 | ||
|
|
433a7b15fe | ||
|
|
6c8cbbc34a | ||
|
|
d4fbb298c1 | ||
|
|
2aeb5f03d6 | ||
|
|
6522403c37 | ||
|
|
6ede2d22bb | ||
|
|
f090ca7f75 | ||
|
|
2f02a238df | ||
|
|
0d5fa97800 | ||
|
|
b102deaee1 | ||
|
|
968b2ee460 | ||
|
|
890340de94 | ||
|
|
4ca1a7b29e | ||
|
|
5432f2590c | ||
|
|
315d26ad52 | ||
|
|
60ccf5cf0a | ||
|
|
bc717f5b10 | ||
|
|
a78b0687f7 | ||
|
|
486eef21dd | ||
|
|
148b8e9369 | ||
|
|
44a4d02815 | ||
|
|
336a8194bd | ||
|
|
ae6ce0f9b0 | ||
|
|
31c8665653 | ||
|
|
7859f4cd05 | ||
|
|
37622ba9ce | ||
|
|
7a6af89375 | ||
|
|
056bfaacfe | ||
|
|
6684311ec5 | ||
|
|
11943571ad | ||
|
|
f696fcd412 | ||
|
|
2919e109ca | ||
|
|
995f478708 | ||
|
|
fb8764bcd7 | ||
|
|
d7f73e02c5 | ||
|
|
e897b3af57 | ||
|
|
e04fd8a004 | ||
|
|
ada70ae1b5 | ||
|
|
5fdec0900e | ||
|
|
56a93288c4 | ||
|
|
02e3421f98 | ||
|
|
fdbf331432 | ||
|
|
aed86ac6f0 | ||
|
|
3a13d4d6c0 | ||
|
|
f5336564d0 | ||
|
|
1ce49c68fe | ||
|
|
d37e880993 | ||
|
|
6fdb81a01f | ||
|
|
f9d6827572 | ||
|
|
10bf72b9ff | ||
|
|
800f929a15 | ||
|
|
bfcff1e19f | ||
|
|
f373e7df3e | ||
|
|
3985de5b14 | ||
|
|
e175a721d4 | ||
|
|
d9784ebc31 | ||
|
|
f241092277 | ||
|
|
0702703d78 | ||
|
|
2c4504bad3 | ||
|
|
07ca5a8b77 | ||
|
|
798a43906f | ||
|
|
41cb0f2e09 | ||
|
|
e12c0fb81f | ||
|
|
ac39f119e2 | ||
|
|
016faf3df0 | ||
|
|
b2d6879282 | ||
|
|
6926a212f4 | ||
|
|
89afc05d5c | ||
|
|
936f39161b | ||
|
|
ee20ee0722 | ||
|
|
02f9f8c8ea | ||
|
|
de3a252884 | ||
|
|
5e7a00de3e | ||
|
|
2858aeb55e | ||
|
|
357104efa9 | ||
|
|
bb8027c7ef | ||
|
|
f9dd787009 | ||
|
|
e005731ba6 | ||
|
|
18ae3f4f61 | ||
|
|
10dfe0327e | ||
|
|
7c6ec2e3d7 | ||
|
|
1d1e921137 | ||
|
|
0985a4c968 | ||
|
|
8df589c103 | ||
|
|
71b6b2f451 | ||
|
|
d85940ded8 | ||
|
|
e9e491c0b0 | ||
|
|
c73562fb75 | ||
|
|
3feacb59c8 | ||
|
|
a033d711c1 | ||
|
|
32081b71f5 | ||
|
|
7849c34d1f | ||
|
|
24977ec613 | ||
|
|
786bbab0d5 | ||
|
|
1facb07c28 | ||
|
|
bba5aba22d | ||
|
|
d7b85d6eba | ||
|
|
6832bfb95c | ||
|
|
4c379b67a3 | ||
|
|
3a2ae1ce71 | ||
|
|
c80afaf9c0 | ||
|
|
31d22bac47 | ||
|
|
b5f6687925 | ||
|
|
b3f25af923 | ||
|
|
78c141e946 | ||
|
|
83d36ce736 | ||
|
|
b928357ff1 | ||
|
|
c074bc57bc | ||
|
|
0e80c88b7d | ||
|
|
5ffa5b01fc | ||
|
|
61d9929485 | ||
|
|
231f19d113 | ||
|
|
31c7116a15 | ||
|
|
bb41f62db5 | ||
|
|
47edc3180b | ||
|
|
9939d99c4b | ||
|
|
8053e8bb05 | ||
|
|
b7e9380bc4 | ||
|
|
83600087e1 | ||
|
|
fe84dc4823 | ||
|
|
c38eb545b1 | ||
|
|
5c480b37b3 | ||
|
|
1fc2f81dab | ||
|
|
69ddc95c2c | ||
|
|
a6ac68499c | ||
|
|
c10d7cfee4 | ||
|
|
f933bdbc53 | ||
|
|
274bca84d9 | ||
|
|
6abfe6ddd7 | ||
|
|
ab7489a049 | ||
|
|
a6fd6ae135 | ||
|
|
b30d4a025f | ||
|
|
5b747bfc74 | ||
|
|
a410d19114 | ||
|
|
a8589cc5b0 | ||
|
|
b057c9f7a8 | ||
|
|
96e4a4933c | ||
|
|
630064500d | ||
|
|
9543294996 | ||
|
|
56e9cc3406 | ||
|
|
be569cbe72 | ||
|
|
99f0817bdb | ||
|
|
220cd35d82 | ||
|
|
07f4ef1697 | ||
|
|
f20732ddc2 | ||
|
|
b1e0dc5843 | ||
|
|
92335d8678 | ||
|
|
285eb25706 | ||
|
|
ec556511e6 | ||
|
|
85c3d9f65f | ||
|
|
a7ebadf269 | ||
|
|
94c09d46c2 | ||
|
|
1dcb5717ea | ||
|
|
36f1a557d7 | ||
|
|
f6f08d176c | ||
|
|
66cdd63496 | ||
|
|
8b502b605c | ||
|
|
bd7157c172 | ||
|
|
2c0ec28803 | ||
|
|
2e1795dc6f | ||
|
|
cd1be782fa | ||
|
|
67059f3d71 | ||
|
|
15f4d3326b | ||
|
|
a9ab9cb249 | ||
|
|
e65404a466 | ||
|
|
3d47d1b4db | ||
|
|
d1749ab610 | ||
|
|
806c264686 | ||
|
|
34a9cb5a74 | ||
|
|
64fad2e871 | ||
|
|
961c69b525 | ||
|
|
c70f393559 | ||
|
|
9abdc174f4 | ||
|
|
2e5bfa1d9c | ||
|
|
34a2af8429 | ||
|
|
9c89c26097 | ||
|
|
e3b6a5d389 | ||
|
|
0fb54efde5 | ||
|
|
a4a3f32dba | ||
|
|
15883f2138 | ||
|
|
03a1e29e0c | ||
|
|
eda9ff272b | ||
|
|
89501271ce | ||
|
|
b3728e06ac | ||
|
|
968a6ea9b3 | ||
|
|
33cbd85e19 | ||
|
|
8cb1f3f387 | ||
|
|
e253d8f4f3 | ||
|
|
3f0c6fcec5 | ||
|
|
cfabe47e10 | ||
|
|
d3fe7857b7 | ||
|
|
642e96a439 | ||
|
|
797cf893da | ||
|
|
a3564b70e1 | ||
|
|
43004307b8 | ||
|
|
acd1e4ced3 | ||
|
|
6717070f93 | ||
|
|
387499ae49 | ||
|
|
8ab140c55d | ||
|
|
914abb95dd | ||
|
|
5360c0f0f7 | ||
|
|
243d803b51 | ||
|
|
b343fe3835 | ||
|
|
3c42c1120f | ||
|
|
ad840dcef6 | ||
|
|
f73072d95e | ||
|
|
95cb9b5079 | ||
|
|
c6684d3c9b | ||
|
|
5c5989d8c0 | ||
|
|
60e92d30b0 | ||
|
|
8bf8e3f86b | ||
|
|
891ee2d06b | ||
|
|
b450bc7ae8 | ||
|
|
4ca1e0d5db | ||
|
|
859213dd9e | ||
|
|
ad2857791d | ||
|
|
497827f2e2 | ||
|
|
967e333022 | ||
|
|
8df1406006 | ||
|
|
4af42fafdc | ||
|
|
a9e6a452c1 | ||
|
|
a4a4632397 | ||
|
|
421f39e414 | ||
|
|
f8121e2dc4 | ||
|
|
b1784fc51c | ||
|
|
96db0d7de7 | ||
|
|
3837ed9cb1 | ||
|
|
2be789a43c | ||
|
|
2b8451e045 | ||
|
|
fd8d96169a | ||
|
|
1562dc32c1 | ||
|
|
62074e554a | ||
|
|
0434cda2da | ||
|
|
38f377ca09 | ||
|
|
cc28bba884 | ||
|
|
beb3081918 | ||
|
|
1b3c9106b5 | ||
|
|
385b91761b | ||
|
|
d7b76ed70a | ||
|
|
43600756c0 | ||
|
|
3c3e0633ad | ||
|
|
f819ad6917 | ||
|
|
2e84faa505 | ||
|
|
e7e8d13d9e | ||
|
|
a683c2cb11 | ||
|
|
addf7de316 | ||
|
|
44d4eada51 | ||
|
|
40bfdea5b1 | ||
|
|
55138c1e86 | ||
|
|
e7ad396fc6 | ||
|
|
0aef680572 | ||
|
|
6dc37d6bde | ||
|
|
60ea7cedf6 | ||
|
|
c986b10e14 | ||
|
|
d52174bd9e | ||
|
|
c65d138911 | ||
|
|
ad9bb8ad58 | ||
|
|
63e536c66c | ||
|
|
b5a08b1b98 | ||
|
|
226e2a7cdc | ||
|
|
4d7c4aed4c | ||
|
|
c9bcd000c3 | ||
|
|
b1cb4d4257 | ||
|
|
de42145f30 | ||
|
|
7bcdd6070a | ||
|
|
8a215e90d0 | ||
|
|
b736fa18bb | ||
|
|
43c19e4942 | ||
|
|
ffc18029bb | ||
|
|
b88b3d15f8 | ||
|
|
c817886a2d | ||
|
|
aae239494e | ||
|
|
b0b2daa5d5 | ||
|
|
eea2e38f1b | ||
|
|
f894ecd25b | ||
|
|
e0b6ed7103 | ||
|
|
a78e75747a | ||
|
|
3b25e367bb | ||
|
|
08b29dff3d | ||
|
|
2f2e053d26 | ||
|
|
191d582c30 | ||
|
|
8d3380ff6e | ||
|
|
ba85d18574 | ||
|
|
0f53b17515 | ||
|
|
cb9c869712 | ||
|
|
aa3d9e7b8f | ||
|
|
b3a9b5824d | ||
|
|
b6186a349f | ||
|
|
100bd4b062 | ||
|
|
7da09d9b37 | ||
|
|
f46eb07228 | ||
|
|
7627b5eb25 | ||
|
|
c710448c6b | ||
|
|
4b4c88d44d | ||
|
|
1ad270b1d6 | ||
|
|
099e253b2b | ||
|
|
66de4a5b91 | ||
|
|
41437d91d5 | ||
|
|
d33d5a6efa | ||
|
|
4f9248d040 | ||
|
|
f40c0e41f3 | ||
|
|
15fcb0e25d | ||
|
|
2dae662333 | ||
|
|
3ad46926f1 | ||
|
|
2385d102ae | ||
|
|
deeb03ff2b | ||
|
|
5c2a09e243 | ||
|
|
2473c999db | ||
|
|
ea2cc265e3 | ||
|
|
a0cd2d42cf | ||
|
|
9e116bec97 | ||
|
|
0a17ceb984 | ||
|
|
4ef18f1f4a | ||
|
|
0de227ab9c | ||
|
|
19cb8703a6 | ||
|
|
e18567dd82 | ||
|
|
bfb3bcdbfb | ||
|
|
565cd14d88 | ||
|
|
ebc37eac75 | ||
|
|
c3702db577 | ||
|
|
e15c4fa342 | ||
|
|
8330b9f1c5 | ||
|
|
f759150982 | ||
|
|
6af177b596 | ||
|
|
657bb94975 | ||
|
|
3c946212b1 | ||
|
|
b4e80f7fca | ||
|
|
c9efc2cb2b | ||
|
|
0d62e33dc7 | ||
|
|
ac88b9e19c | ||
|
|
871dfda79e | ||
|
|
e0c2c208ae | ||
|
|
22ac112bdb | ||
|
|
afd0cca176 | ||
|
|
c083c8bce5 | ||
|
|
63bde032b3 | ||
|
|
49492c0788 | ||
|
|
b439c64add | ||
|
|
1868bfe8e3 | ||
|
|
f240a3d996 | ||
|
|
788e5bd12e | ||
|
|
a55fed4502 | ||
|
|
a8fdaf1a47 | ||
|
|
4a758bd488 | ||
|
|
2c9731ec2a | ||
|
|
eef33266fc | ||
|
|
58d2c3e5a6 | ||
|
|
9e6a355db0 | ||
|
|
0d10e09fd6 | ||
|
|
f85bb995ba | ||
|
|
268e5639f6 | ||
|
|
51809df8ca | ||
|
|
94fb676b0c | ||
|
|
a47106594b | ||
|
|
d93d66f702 | ||
|
|
b2f9f7ae54 | ||
|
|
de7b908c78 | ||
|
|
dc25e16c00 | ||
|
|
75d3c2fdce | ||
|
|
ea1b6c5835 | ||
|
|
cb7887da41 | ||
|
|
a80313ee6b | ||
|
|
e1a821bc43 | ||
|
|
42a7c324fa | ||
|
|
924ea2d03a | ||
|
|
55270fe654 | ||
|
|
a125fab57b | ||
|
|
395ee0aa99 | ||
|
|
0f50fa6ba1 | ||
|
|
adb7df3c71 | ||
|
|
5d7bcb629b | ||
|
|
a00f1417d2 | ||
|
|
8efd7e8ebf | ||
|
|
b016d277e0 | ||
|
|
fdb39617d1 | ||
|
|
89f83fbf62 | ||
|
|
ecee9e01a6 | ||
|
|
20dc9bb8b9 | ||
|
|
2c47d0e9ed | ||
|
|
8e13d52e51 | ||
|
|
cc40198c9e | ||
|
|
290897ea41 | ||
|
|
b9e1c84304 | ||
|
|
3c44c80e2e | ||
|
|
dffa4e4594 | ||
|
|
fa2d9fec58 | ||
|
|
09c1a2cfa0 | ||
|
|
d1f90eb231 | ||
|
|
1f7d97134b | ||
|
|
79be91784d | ||
|
|
de2654def3 | ||
|
|
56343dacff | ||
|
|
0e677f8ce7 | ||
|
|
aa911896d6 | ||
|
|
c62a8635b9 | ||
|
|
4e900247c5 | ||
|
|
b3bd62bc6c | ||
|
|
8e5fd48ecd | ||
|
|
b2bca9dd2c | ||
|
|
b8c0dc3181 | ||
|
|
cccdc5292e | ||
|
|
76d77a0e7a | ||
|
|
e737f4bf9a | ||
|
|
391db2f1c9 | ||
|
|
359d61183c | ||
|
|
46fd05d88e | ||
|
|
cde22a0945 | ||
|
|
111b7e25c5 | ||
|
|
4f8d8f0c8d | ||
|
|
915b0603d0 | ||
|
|
849888d128 | ||
|
|
075aab8074 | ||
|
|
6ebe4c86af | ||
|
|
0925c8c582 | ||
|
|
a683fdce62 | ||
|
|
b958299446 | ||
|
|
3f80be8377 | ||
|
|
ced0accde5 | ||
|
|
b454ff5ec7 | ||
|
|
45af198f32 | ||
|
|
ff374f8899 | ||
|
|
faecb3bc4b | ||
|
|
6b893fadef | ||
|
|
c328467a41 | ||
|
|
182325470b | ||
|
|
f330ad71ac | ||
|
|
ba0c064f36 | ||
|
|
8d7aaee5b9 | ||
|
|
8a7e910e7c | ||
|
|
72d72d443e | ||
|
|
68cba2de63 | ||
|
|
5a914f9c0e | ||
|
|
b0e6805a20 | ||
|
|
21e7e44c01 | ||
|
|
7b0a3f0f96 | ||
|
|
f7df4abdae | ||
|
|
7674ceefe9 | ||
|
|
4be575c534 | ||
|
|
dd0f0a7d5a | ||
|
|
759b44c224 | ||
|
|
cf4604e0d8 | ||
|
|
e873dd7d0a | ||
|
|
4492e940e5 | ||
|
|
c833c03dc3 | ||
|
|
30b0d226b5 | ||
|
|
8afad21113 | ||
|
|
6b5e5b0f25 | ||
|
|
3c0ab6822f | ||
|
|
f7215d00ca | ||
|
|
43bbe9be0f | ||
|
|
477a691c9e | ||
|
|
e5d60050a2 | ||
|
|
9a698fda18 | ||
|
|
d5701c1073 | ||
|
|
955b9a4b2b | ||
|
|
09ffda2605 | ||
|
|
039fb0c505 | ||
|
|
20799ef1a8 |
11
.github/workflows/validate-gradle-wrapper.yml
vendored
Normal file
11
.github/workflows/validate-gradle-wrapper.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
name: Validate Gradle Wrapper
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
70
README.md
70
README.md
@@ -10,50 +10,59 @@
|
||||
|
||||
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly won’t ever be implemented, such as the federated timeline, unlisted posting and an image description viewer.
|
||||
> A fork of the [Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app, focusing on [Glitch](https://glitch-soc.github.io/docs) compatibility, a pretty UI and adding new features that I feel make using the Fediverse a more pleasant experience.
|
||||
|
||||
|
||||
## Key features
|
||||
|
||||
### **Unlisted posting**
|
||||
|
||||
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).**
|
||||
<details>
|
||||
<p><summary>Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).</summary></p>
|
||||
|
||||
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in people’s Home timelines, but only if they follow you or someone they follow reblogged/replied to your post.
|
||||
|
||||
|
||||
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
|
||||
</details>
|
||||
|
||||
### **Federated timeline**
|
||||
|
||||
**This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.**
|
||||
<details>
|
||||
<p><summary>This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.</summary></p>
|
||||
|
||||
Despite being one of the main features of federated social media, the Federated timeline wasn’t included in the official Mastodon app – supposedly, because this conflicts with Google’s safety requirements for apps on the Play Store.
|
||||
|
||||
That’s one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
|
||||
</details>
|
||||
|
||||
### **Customizable timelines**
|
||||
|
||||
<details>
|
||||
<p><summary>You can customize Megalodon’s home tab and not only add local and federated timelines, but also pin lists and hashtags.</summary></p>
|
||||
|
||||
Even better: You can rename every timeline however you please and pick a distinct icon for each timeline. This way, you can pin the hashtag “#Caturday”, rename your timeline to “CUTENESS OVERLOAD” and set <img src="img/ic_fluent_animal_cat_24_regular.svg" alt="Cat icon from Microsoft Fluent UI icons"> as its icon. :3 You can find the timelines editor by opening your home tab, tapping the `⋮` button in the top right and going to “Edit timelines”.
|
||||
</details>
|
||||
|
||||
### **Draft and schedule posts**
|
||||
|
||||
**Allows for preparing a post and scheduling it to send it automatically at a specific time.**
|
||||
<details>
|
||||
<p><summary>
|
||||
Allows to prepare a post and schedule it to send it automatically at a specific time.</summary></p>
|
||||
|
||||
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
|
||||
|
||||
### **Image description viewer**
|
||||
|
||||
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
|
||||
|
||||
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!
|
||||
|
||||
### **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.**
|
||||
|
||||
On the Fediverse, it’s quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
|
||||
### IzzyOnDroid
|
||||
### Google Play Store
|
||||
|
||||
[apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
|
||||
[https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
|
||||
|
||||
### F-Droid via IzzyOnDroid
|
||||
|
||||
[https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
|
||||
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
@@ -61,11 +70,11 @@ Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first
|
||||
|
||||
[`https://apt.izzysoft.de/fdroid/repo`](https://apt.izzysoft.de/fdroid/repo)
|
||||
|
||||
### Google Play Store
|
||||
### F-Droid via saunarepo
|
||||
|
||||
[play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
|
||||
[https://repo.the-sauna.icu](https://repo.the-sauna.icu/)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
|
||||
<a href="https://repo.the-sauna.icu"><img height="28" alt="Get it on SaunaRepo" src="img/saunarepo-badge.svg"></a>
|
||||
|
||||
### F-Droid
|
||||
|
||||
@@ -88,7 +97,7 @@ Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastod
|
||||
|
||||
## Release variants
|
||||
|
||||
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page.
|
||||
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page. When downloading a pre-release, expect to see unfinished features and bugs. If you don’t want that, just download the [latest full release](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk).
|
||||
|
||||
**`megalodon.apk`**
|
||||
|
||||
@@ -108,11 +117,11 @@ Variant without the integrated updater. This is the variant to be published to F
|
||||
|
||||
### Translation
|
||||
|
||||
As with the source code, the translation is sourced from the official project, which you can contribute to on the official “**Mastodon for Android**” Crowdin project: https://crowdin.com/project/mastodon-for-android
|
||||
The translation for the base of the app is sourced from the upstream **Mastodon for Android** project, which you can contribute to on its Crowdin project: [https://crowdin.com/project/mastodon-for-android](https://crowdin.com/project/mastodon-for-android)
|
||||
|
||||
There's also a handful of custom strings exclusive to this projects that would need to be translated. You can help translate **Megalodon** on Weblate: https://translate.codeberg.org/projects/megalodon/
|
||||
There's also a bunch of custom strings exclusive to this project that need to be translated. You can help translate **Megalodon** on Weblate: [https://translate.codeberg.org/projects/megalodon](https://translate.codeberg.org/projects/megalodon)
|
||||
|
||||
[](https://translate.codeberg.org/engage/megalodon/)
|
||||
[](https://translate.codeberg.org/engage/megalodon)
|
||||
|
||||
|
||||
---
|
||||
@@ -154,6 +163,10 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
|
||||
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
|
||||
* Support for local-only posts
|
||||
* Support for copying the URL to posts/accounts/… in Pixel launcher’s Recent apps view
|
||||
* Compatibility for Akkoma Bubble timeline
|
||||
* Listings of followers/following/favorites/boosts can be loaded from the origin instance (there’s an option to disable this in in the settings)
|
||||
* Allow opening posts/accounts in-app by sharing a URL/handle to Megalodon (Originally implemented in [Moshidon](https://github.com/LucasGGamerM/moshidon), [PR](https://github.com/sk22/megalodon/pull/531))
|
||||
|
||||
|
||||
### Behavior
|
||||
@@ -179,6 +192,8 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
|
||||
* [Support admin notifications](https://github.com/sk22/megalodon/commit/c12a6eaee6b609bc53eb0a45d9199f37d5241801) and [notifications for edited reblogged posts](https://github.com/sk22/megalodon/commit/900e8fb2e9353002c16d15e06b78d2731e121601)
|
||||
* [Android file opener added back in addition to image picker](https://github.com/sk22/megalodon/commit/3a6ace53d5ab01e28077c9c930cb6ed487b78031)
|
||||
* [Replies are inserted below the replied-to post in thread view](https://github.com/sk22/megalodon/commit/87c37df370ec24aeea0d2dbaeb29468aa4fb5808)
|
||||
* Option to auto-reveal equal content warnings in threads
|
||||
|
||||
|
||||
### Visual
|
||||
@@ -196,6 +211,7 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* Scale text according to system settings
|
||||
* Header in timeline for followed hashtags
|
||||
* [Indicator for missing alt texts](https://github.com/sk22/megalodon/commit/c0c276f03e793b78c478c17dfdef24a66ef7cedb)
|
||||
* Visually grouped (by removing divider lines and reducing padding) threaded replies in thread view
|
||||
|
||||
|
||||
## Building
|
||||
@@ -206,6 +222,8 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
Note that Megalodon might be depending on an in-development version of [AppKit](https://github.com/grishka/appkit) – a library by Mastodon for Android’s developer. In case the used AppKit version isn’t published to Maven Central yet, you might have to clone, build and publish it to your local Maven repository. For more information, see [this GitHub issue](https://github.com/mastodon/mastodon-android/issues/375#issuecomment-1507678585).
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the [GPL-3 License](./LICENSE).
|
||||
|
||||
@@ -3,9 +3,15 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
content {
|
||||
includeModule 'com.github.UnifiedPush', 'android-connector'
|
||||
}
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath 'com.android.tools.build:gradle:8.0.0'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
||||
@@ -16,4 +16,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=false
|
||||
android.enableJetifier=false
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,7 @@
|
||||
#Thu Jan 13 11:33:43 MSK 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
288
gradlew
vendored
288
gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,67 +17,98 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,7 +129,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@@ -106,80 +137,109 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
15
gradlew.bat
vendored
15
gradlew.bat
vendored
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
9
img/ic_fluent_animal_cat_24_regular.svg
Normal file
9
img/ic_fluent_animal_cat_24_regular.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
path { fill: black; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: white; }
|
||||
}
|
||||
</style>
|
||||
<path d="M15.4925 3.50673C14.652 3.58251 13.9933 4.28929 13.9933 5.15V10C13.9933 10.4142 13.6577 10.75 13.2437 10.75C11.8002 10.75 10.7863 11.3378 10.0365 12.238C9.26389 13.1656 8.7607 14.444 8.44554 15.7954C8.13254 17.1376 8.01871 18.4912 7.98453 19.5172C7.97182 19.8987 7.9702 20.2324 7.97313 20.5H14.9928V19.75C14.9928 18.5074 13.986 17.5 12.744 17.5H11.4947C11.0807 17.5 10.7451 17.1642 10.7451 16.75C10.7451 16.3358 11.0807 16 11.4947 16H12.744C14.8139 16 16.4919 17.6789 16.4919 19.75V20.5H17.2415C17.6555 20.5 17.9911 20.1642 17.9911 19.75V9.75C17.9911 9.33579 18.3267 9 18.7407 9H19.2472C20.2264 9 20.8249 7.92404 20.309 7.09132L19.6893 6.09132C19.4615 5.72367 19.0599 5.5 18.6275 5.5H16.2421C15.8281 5.5 15.4925 5.16421 15.4925 4.75V3.50673ZM6.47388 20.5C6.47098 20.2156 6.47293 19.8655 6.4862 19.4672C6.52229 18.3838 6.64271 16.9249 6.98559 15.4546C7.32631 13.9935 7.90065 12.4594 8.88484 11.2777C9.75681 10.2307 10.9399 9.47669 12.4942 9.29318V5.15C12.4942 3.4103 13.9037 2 15.6424 2C16.3876 2 16.9916 2.60442 16.9916 3.35V4H18.6275C19.5787 4 20.4622 4.49207 20.9634 5.30092L21.5831 6.30092C22.6749 8.06291 21.4985 10.32 19.4903 10.4898V19.75C19.4903 20.9926 18.4835 22 17.2415 22H7.24708L7.24537 22H5.79625C3.69964 22 2 20.2994 2 18.2016C2 17.2395 2.36489 16.3133 3.02098 15.6099L4.15612 14.393C4.92005 13.5741 5.17521 12.4027 4.82117 11.3399C4.67114 10.8896 4.41837 10.4804 4.08288 10.1447L2.96914 9.03042C2.67641 8.73753 2.67639 8.26266 2.96912 7.96976C3.26184 7.67686 3.73645 7.67685 4.02919 7.96974L5.14293 9.08405C5.643 9.58438 6.01977 10.1943 6.2434 10.8656C6.77114 12.4497 6.3908 14.1958 5.25209 15.4165L4.11695 16.6334C3.71996 17.059 3.49916 17.6195 3.49916 18.2016C3.49916 19.471 4.52761 20.5 5.79625 20.5H6.47388Z" fill="#212121"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
img/saunarepo-badge.svg
Normal file
1
img/saunarepo-badge.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="124.25" height="28" role="img" aria-label="SAUNAREPO"><title>SAUNAREPO</title><g shape-rendering="crispEdges"><rect width="124.25" height="28" fill="#fb8441"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><image x="9" y="7" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkFuZHJvaWQ8L3RpdGxlPjxwYXRoIGQ9Ik0xNy41MjMgMTUuMzQxNGMtLjU1MTEgMC0uOTk5My0uNDQ4Ni0uOTk5My0uOTk5N3MuNDQ4My0uOTk5My45OTkzLS45OTkzYy41NTExIDAgLjk5OTMuNDQ4My45OTkzLjk5OTMuMDAwMS41NTExLS40NDgyLjk5OTctLjk5OTMuOTk5N20tMTEuMDQ2IDBjLS41NTExIDAtLjk5OTMtLjQ0ODYtLjk5OTMtLjk5OTdzLjQ0ODItLjk5OTMuOTk5My0uOTk5M2MuNTUxMSAwIC45OTkzLjQ0ODMuOTk5My45OTkzIDAgLjU1MTEtLjQ0ODMuOTk5Ny0uOTk5My45OTk3bTExLjQwNDUtNi4wMmwxLjk5NzMtMy40NTkyYS40MTYuNDE2IDAgMDAtLjE1MjEtLjU2NzYuNDE2LjQxNiAwIDAwLS41Njc2LjE1MjFsLTIuMDIyMyAzLjUwM0MxNS41OTAyIDguMjQzOSAxMy44NTMzIDcuODUwOCAxMiA3Ljg1MDhzLTMuNTkwMi4zOTMxLTUuMTM2NyAxLjA5ODlMNC44NDEgNS40NDY3YS40MTYxLjQxNjEgMCAwMC0uNTY3Ny0uMTUyMS40MTU3LjQxNTcgMCAwMC0uMTUyMS41Njc2bDEuOTk3MyAzLjQ1OTJDMi42ODg5IDExLjE4NjcuMzQzMiAxNC42NTg5IDAgMTguNzYxaDI0Yy0uMzQzNS00LjEwMjEtMi42ODkyLTcuNTc0My02LjExODUtOS40Mzk2Ii8+PC9zdmc+"/><text transform="scale(.1)" x="721.25" y="175" textLength="802.5" fill="#fff" font-weight="bold">SAUNAREPO</text></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
279
mastodon/.editorconfig
Normal file
279
mastodon/.editorconfig
Normal file
@@ -0,0 +1,279 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
insert_final_newline = false
|
||||
max_line_length = 300
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = none
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
ij_java_align_consecutive_variable_declarations = false
|
||||
ij_java_align_group_field_declarations = false
|
||||
ij_java_align_multiline_annotation_parameters = false
|
||||
ij_java_align_multiline_array_initializer_expression = false
|
||||
ij_java_align_multiline_assignment = false
|
||||
ij_java_align_multiline_binary_operation = false
|
||||
ij_java_align_multiline_chained_methods = false
|
||||
ij_java_align_multiline_extends_list = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_align_multiline_method_parentheses = false
|
||||
ij_java_align_multiline_parameters = true
|
||||
ij_java_align_multiline_parameters_in_calls = false
|
||||
ij_java_align_multiline_parenthesized_expression = false
|
||||
ij_java_align_multiline_records = true
|
||||
ij_java_align_multiline_resources = true
|
||||
ij_java_align_multiline_ternary_operation = false
|
||||
ij_java_align_multiline_text_blocks = false
|
||||
ij_java_align_multiline_throws_list = false
|
||||
ij_java_align_subsequent_simple_methods = false
|
||||
ij_java_align_throws_keyword = false
|
||||
ij_java_align_types_in_multi_catch = true
|
||||
ij_java_annotation_parameter_wrap = off
|
||||
ij_java_array_initializer_new_line_after_left_brace = false
|
||||
ij_java_array_initializer_right_brace_on_new_line = false
|
||||
ij_java_array_initializer_wrap = off
|
||||
ij_java_assert_statement_colon_on_next_line = false
|
||||
ij_java_assert_statement_wrap = off
|
||||
ij_java_assignment_wrap = off
|
||||
ij_java_binary_operation_sign_on_next_line = false
|
||||
ij_java_binary_operation_wrap = off
|
||||
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||
ij_java_blank_lines_after_class_header = 0
|
||||
ij_java_blank_lines_after_imports = 1
|
||||
ij_java_blank_lines_after_package = 1
|
||||
ij_java_blank_lines_around_class = 1
|
||||
ij_java_blank_lines_around_field = 0
|
||||
ij_java_blank_lines_around_field_in_interface = 0
|
||||
ij_java_blank_lines_around_initializer = 1
|
||||
ij_java_blank_lines_around_method = 1
|
||||
ij_java_blank_lines_around_method_in_interface = 1
|
||||
ij_java_blank_lines_before_class_end = 0
|
||||
ij_java_blank_lines_before_imports = 1
|
||||
ij_java_blank_lines_before_method_body = 0
|
||||
ij_java_blank_lines_before_package = 0
|
||||
ij_java_block_brace_style = end_of_line
|
||||
ij_java_block_comment_add_space = false
|
||||
ij_java_block_comment_at_first_column = true
|
||||
ij_java_builder_methods = none
|
||||
ij_java_call_parameters_new_line_after_left_paren = false
|
||||
ij_java_call_parameters_right_paren_on_new_line = false
|
||||
ij_java_call_parameters_wrap = off
|
||||
ij_java_case_statement_on_separate_line = true
|
||||
ij_java_catch_on_new_line = false
|
||||
ij_java_class_annotation_wrap = split_into_lines
|
||||
ij_java_class_brace_style = end_of_line
|
||||
ij_java_class_count_to_use_import_on_demand = 99
|
||||
ij_java_class_names_in_javadoc = 1
|
||||
ij_java_do_not_indent_top_level_class_members = false
|
||||
ij_java_do_not_wrap_after_single_annotation = false
|
||||
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
|
||||
ij_java_do_while_brace_force = never
|
||||
ij_java_doc_add_blank_line_after_description = true
|
||||
ij_java_doc_add_blank_line_after_param_comments = false
|
||||
ij_java_doc_add_blank_line_after_return = false
|
||||
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_do_not_wrap_if_one_line = false
|
||||
ij_java_doc_enable_formatting = true
|
||||
ij_java_doc_enable_leading_asterisks = true
|
||||
ij_java_doc_indent_on_continuation = false
|
||||
ij_java_doc_keep_empty_lines = true
|
||||
ij_java_doc_keep_empty_parameter_tag = true
|
||||
ij_java_doc_keep_empty_return_tag = true
|
||||
ij_java_doc_keep_empty_throws_tag = true
|
||||
ij_java_doc_keep_invalid_tags = true
|
||||
ij_java_doc_param_description_on_new_line = false
|
||||
ij_java_doc_preserve_line_breaks = false
|
||||
ij_java_doc_use_throws_not_exception_tag = true
|
||||
ij_java_else_on_new_line = false
|
||||
ij_java_enum_constants_wrap = off
|
||||
ij_java_extends_keyword_wrap = off
|
||||
ij_java_extends_list_wrap = off
|
||||
ij_java_field_annotation_wrap = split_into_lines
|
||||
ij_java_finally_on_new_line = false
|
||||
ij_java_for_brace_force = never
|
||||
ij_java_for_statement_new_line_after_left_paren = false
|
||||
ij_java_for_statement_right_paren_on_new_line = false
|
||||
ij_java_for_statement_wrap = off
|
||||
ij_java_generate_final_locals = false
|
||||
ij_java_generate_final_parameters = false
|
||||
ij_java_if_brace_force = never
|
||||
ij_java_imports_layout = android.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|,$*,|
|
||||
ij_java_indent_case_from_switch = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_insert_override_annotation = true
|
||||
ij_java_keep_blank_lines_before_right_brace = 2
|
||||
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||
ij_java_keep_blank_lines_in_code = 2
|
||||
ij_java_keep_blank_lines_in_declarations = 2
|
||||
ij_java_keep_builder_methods_indents = false
|
||||
ij_java_keep_control_statement_in_one_line = true
|
||||
ij_java_keep_first_column_comment = true
|
||||
ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
ij_java_label_indent_size = 0
|
||||
ij_java_lambda_brace_style = end_of_line
|
||||
ij_java_layout_static_imports_separately = true
|
||||
ij_java_line_comment_add_space = false
|
||||
ij_java_line_comment_add_space_on_reformat = false
|
||||
ij_java_line_comment_at_first_column = true
|
||||
ij_java_method_annotation_wrap = split_into_lines
|
||||
ij_java_method_brace_style = end_of_line
|
||||
ij_java_method_call_chain_wrap = off
|
||||
ij_java_method_parameters_new_line_after_left_paren = false
|
||||
ij_java_method_parameters_right_paren_on_new_line = false
|
||||
ij_java_method_parameters_wrap = off
|
||||
ij_java_modifier_list_wrap = false
|
||||
ij_java_multi_catch_types_wrap = normal
|
||||
ij_java_names_count_to_use_import_on_demand = 99
|
||||
ij_java_new_line_after_lparen_in_annotation = false
|
||||
ij_java_new_line_after_lparen_in_record_header = false
|
||||
ij_java_parameter_annotation_wrap = off
|
||||
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_java_place_assignment_sign_on_next_line = false
|
||||
ij_java_prefer_longer_names = true
|
||||
ij_java_prefer_parameters_wrap = false
|
||||
ij_java_record_components_wrap = normal
|
||||
ij_java_repeat_synchronized = true
|
||||
ij_java_replace_instanceof_and_cast = false
|
||||
ij_java_replace_null_check = true
|
||||
ij_java_replace_sum_lambda_with_method_ref = true
|
||||
ij_java_resource_list_new_line_after_left_paren = false
|
||||
ij_java_resource_list_right_paren_on_new_line = false
|
||||
ij_java_resource_list_wrap = off
|
||||
ij_java_rparen_on_new_line_in_annotation = false
|
||||
ij_java_rparen_on_new_line_in_record_header = false
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
ij_java_space_after_colon = true
|
||||
ij_java_space_after_comma = true
|
||||
ij_java_space_after_comma_in_type_arguments = true
|
||||
ij_java_space_after_for_semicolon = true
|
||||
ij_java_space_after_quest = true
|
||||
ij_java_space_after_type_cast = true
|
||||
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||
ij_java_space_before_annotation_parameter_list = false
|
||||
ij_java_space_before_array_initializer_left_brace = false
|
||||
ij_java_space_before_catch_keyword = false
|
||||
ij_java_space_before_catch_left_brace = false
|
||||
ij_java_space_before_catch_parentheses = false
|
||||
ij_java_space_before_class_left_brace = false
|
||||
ij_java_space_before_colon = true
|
||||
ij_java_space_before_colon_in_foreach = true
|
||||
ij_java_space_before_comma = false
|
||||
ij_java_space_before_do_left_brace = false
|
||||
ij_java_space_before_else_keyword = false
|
||||
ij_java_space_before_else_left_brace = false
|
||||
ij_java_space_before_finally_keyword = false
|
||||
ij_java_space_before_finally_left_brace = false
|
||||
ij_java_space_before_for_left_brace = false
|
||||
ij_java_space_before_for_parentheses = false
|
||||
ij_java_space_before_for_semicolon = false
|
||||
ij_java_space_before_if_left_brace = false
|
||||
ij_java_space_before_if_parentheses = false
|
||||
ij_java_space_before_method_call_parentheses = false
|
||||
ij_java_space_before_method_left_brace = false
|
||||
ij_java_space_before_method_parentheses = false
|
||||
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||
ij_java_space_before_quest = true
|
||||
ij_java_space_before_switch_left_brace = false
|
||||
ij_java_space_before_switch_parentheses = false
|
||||
ij_java_space_before_synchronized_left_brace = false
|
||||
ij_java_space_before_synchronized_parentheses = false
|
||||
ij_java_space_before_try_left_brace = false
|
||||
ij_java_space_before_try_parentheses = false
|
||||
ij_java_space_before_type_parameter_list = false
|
||||
ij_java_space_before_while_keyword = false
|
||||
ij_java_space_before_while_left_brace = false
|
||||
ij_java_space_before_while_parentheses = false
|
||||
ij_java_space_inside_one_line_enum_braces = false
|
||||
ij_java_space_within_empty_array_initializer_braces = false
|
||||
ij_java_space_within_empty_method_call_parentheses = false
|
||||
ij_java_space_within_empty_method_parentheses = false
|
||||
ij_java_spaces_around_additive_operators = false
|
||||
ij_java_spaces_around_annotation_eq = true
|
||||
ij_java_spaces_around_assignment_operators = false
|
||||
ij_java_spaces_around_bitwise_operators = false
|
||||
ij_java_spaces_around_equality_operators = false
|
||||
ij_java_spaces_around_lambda_arrow = false
|
||||
ij_java_spaces_around_logical_operators = true
|
||||
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||
ij_java_spaces_around_multiplicative_operators = false
|
||||
ij_java_spaces_around_relational_operators = false
|
||||
ij_java_spaces_around_shift_operators = false
|
||||
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||
ij_java_spaces_around_unary_operator = false
|
||||
ij_java_spaces_within_angle_brackets = false
|
||||
ij_java_spaces_within_annotation_parentheses = false
|
||||
ij_java_spaces_within_array_initializer_braces = false
|
||||
ij_java_spaces_within_braces = false
|
||||
ij_java_spaces_within_brackets = false
|
||||
ij_java_spaces_within_cast_parentheses = false
|
||||
ij_java_spaces_within_catch_parentheses = false
|
||||
ij_java_spaces_within_for_parentheses = false
|
||||
ij_java_spaces_within_if_parentheses = false
|
||||
ij_java_spaces_within_method_call_parentheses = false
|
||||
ij_java_spaces_within_method_parentheses = false
|
||||
ij_java_spaces_within_parentheses = false
|
||||
ij_java_spaces_within_record_header = false
|
||||
ij_java_spaces_within_switch_parentheses = false
|
||||
ij_java_spaces_within_synchronized_parentheses = false
|
||||
ij_java_spaces_within_try_parentheses = false
|
||||
ij_java_spaces_within_while_parentheses = false
|
||||
ij_java_special_else_if_treatment = true
|
||||
ij_java_subclass_name_suffix = Impl
|
||||
ij_java_ternary_operation_signs_on_next_line = false
|
||||
ij_java_ternary_operation_wrap = off
|
||||
ij_java_test_name_suffix = Test
|
||||
ij_java_throws_keyword_wrap = off
|
||||
ij_java_throws_list_wrap = off
|
||||
ij_java_use_external_annotations = false
|
||||
ij_java_use_fq_class_names = false
|
||||
ij_java_use_relative_indents = false
|
||||
ij_java_use_single_class_imports = true
|
||||
ij_java_variable_annotation_wrap = off
|
||||
ij_java_visibility = public
|
||||
ij_java_while_brace_force = never
|
||||
ij_java_while_on_new_line = false
|
||||
ij_java_wrap_comments = false
|
||||
ij_java_wrap_first_method_in_call_chain = false
|
||||
ij_java_wrap_long_lines = false
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_continuation_indent_size = 4
|
||||
ij_xml_align_attributes = false
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_add_space = false
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = false
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = true
|
||||
ij_xml_text_wrap = normal
|
||||
ij_xml_use_custom_settings = true
|
||||
@@ -2,6 +2,12 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
defaultConfig {
|
||||
@@ -9,16 +15,16 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 83
|
||||
versionName "1.2.0+fork.83"
|
||||
versionCode 97
|
||||
versionName "2.0.3+fork.97"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "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", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// minifyEnabled true
|
||||
// shrinkResources true
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug{
|
||||
@@ -26,15 +32,9 @@ android {
|
||||
versionNameSuffix '-debug'
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
githubRelease{
|
||||
initWith release
|
||||
}
|
||||
playRelease{
|
||||
initWith release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
versionNameSuffix '-play'
|
||||
}
|
||||
githubRelease { initWith release }
|
||||
playRelease { initWith release }
|
||||
fdroidRelease { initWith release }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -49,14 +49,19 @@ android {
|
||||
setRoot "src/github"
|
||||
}
|
||||
}
|
||||
lintOptions{
|
||||
checkReleaseBuilds false
|
||||
namespace 'org.joinmastodon.android'
|
||||
lint {
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'androidx.annotation:annotation:1.3.0'
|
||||
api 'androidx.annotation:annotation:1.6.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||
implementation 'me.grishka.litex:recyclerview:1.2.1.1'
|
||||
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0.1'
|
||||
@@ -64,18 +69,20 @@ 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 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.9'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
implementation 'de.psdev:async-otto:1.0.3'
|
||||
implementation 'org.parceler:parceler-api:1.1.12'
|
||||
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
implementation 'com.github.UnifiedPush:android-connector:2.1.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
||||
39
mastodon/proguard-rules.pro
vendored
39
mastodon/proguard-rules.pro
vendored
@@ -30,6 +30,9 @@
|
||||
*;
|
||||
}
|
||||
|
||||
# i don't know how proguard works
|
||||
-keep class org.joinmastodon.android.** { *; }
|
||||
|
||||
# Keep all enums for debugging purposes
|
||||
-keepnames public enum * {
|
||||
*;
|
||||
@@ -46,3 +49,39 @@
|
||||
-keep interface org.parceler.Parcel
|
||||
-keep @org.parceler.Parcel class * { *; }
|
||||
-keep class **$$Parcelable { *; }
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-dontwarn sun.misc.**
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Prevent R8 from leaving Data object members always null
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.*;
|
||||
@LargeTest
|
||||
public class StoreScreenshotsGenerator{
|
||||
private static final String PHOTO_FILE="IMG_1010.jpg";
|
||||
private static final long LOAD_WAIT_TIMEOUT=20_000;
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
|
||||
@@ -84,14 +85,14 @@ public class StoreScreenshotsGenerator{
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||
MastodonApp.context.deleteDatabase(session.getID()+".db");
|
||||
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("HomeTimeline");
|
||||
|
||||
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
|
||||
activityScenarioRule.getScenario().recreate();
|
||||
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("HomeTimeline_Dark");
|
||||
|
||||
@@ -100,8 +101,8 @@ public class StoreScreenshotsGenerator{
|
||||
|
||||
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
|
||||
Thread.sleep(500);
|
||||
onView(isRoot()).perform(waitId(R.id.avatar_border, 5000)); // wait for profile to load
|
||||
onView(isRoot()).perform(waitId(R.id.more, 5000)); // wait for timeline to load
|
||||
onView(isRoot()).perform(waitId(R.id.avatar_border, LOAD_WAIT_TIMEOUT)); // wait for profile to load
|
||||
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT)); // wait for timeline to load
|
||||
Thread.sleep(500);
|
||||
takeScreenshot("Profile");
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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"
|
||||
));
|
||||
}
|
||||
|
||||
private final String[] args = new String[] { "Megalodon", "♡" };
|
||||
|
||||
private String gen(String format, CharSequence... args) {
|
||||
return UiUtils.generateFormattedString(format, args).toString();
|
||||
}
|
||||
@Test
|
||||
public void generateFormattedString() {
|
||||
assertEquals(
|
||||
"ordered substitution",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("%s reacted with %s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"1 2 3 4 5",
|
||||
gen("%s %s %s %s %s", "1", "2", "3", "4", "5")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution",
|
||||
"with ♡ was reacted by Megalodon",
|
||||
gen("with %2$s was reacted by %1$s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution, in order",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("%1$s reacted with %2$s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution, 0-based",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("%0$s reacted with %1$s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"indexed substitution, 5 items",
|
||||
"5 4 3 2 1",
|
||||
gen("%5$s %4$s %3$s %2$s %1$s", "1", "2", "3", "4", "5")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"one argument missing",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("reacted with %s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"multiple arguments missing",
|
||||
"Megalodon reacted with ♡",
|
||||
gen("reacted with", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"multiple arguments missing, numbers in expeced positions",
|
||||
"1 2 x 3 4 5",
|
||||
gen("%s x %s", "1", "2", "3", "4", "5")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"one leading and trailing space",
|
||||
"Megalodon reacted with ♡",
|
||||
gen(" reacted with ", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"multiple leading and trailing spaces",
|
||||
"Megalodon reacted with ♡",
|
||||
gen(" reacted with ", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"invalid format produces expected invalid result",
|
||||
"Megalodon reacted with % s ♡",
|
||||
gen("reacted with % s", args)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"plain string as format, all arguments get added",
|
||||
"a x b c",
|
||||
gen("x", new String[] { "a", "b", "c" })
|
||||
);
|
||||
|
||||
assertEquals("empty input produces empty output", "", gen(""));
|
||||
|
||||
// not supported:
|
||||
// assertEquals("a b a", gen("%1$s %2$s %2$s %1$s", new String[] { "a", "b", "c" }));
|
||||
// assertEquals("x", gen("%s %1$s %2$s %1$s %s", new String[] { "a", "b", "c" }));
|
||||
}
|
||||
|
||||
private AccountField makeField(String name, String value) {
|
||||
AccountField f = new AccountField();
|
||||
f.name = name;
|
||||
f.value = value;
|
||||
return f;
|
||||
}
|
||||
|
||||
private Account fakeAccount(AccountField... fields) {
|
||||
Account a = new Account();
|
||||
a.fields = Arrays.asList(fields);
|
||||
return a;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractPronouns() {
|
||||
assertEquals("they", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("name and pronouns", "https://pronouns.site"),
|
||||
makeField("pronouns", "they"),
|
||||
makeField("pronouns something", "bla bla")
|
||||
)).orElseThrow());
|
||||
|
||||
assertTrue(UiUtils.extractPronouns(MastodonApp.context, fakeAccount()).isEmpty());
|
||||
|
||||
assertEquals("it/its", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns pronouns pronouns", "hi hi hi"),
|
||||
makeField("pronouns", "it/its"),
|
||||
makeField("the pro's nouns", "professional")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("she/he", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("my name is", "jeanette shork, apparently"),
|
||||
makeField("my pronouns are", "she/he")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("they/them", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "https://pronouns.cc/pronouns/they/them")
|
||||
)).orElseThrow());
|
||||
|
||||
Context german = UiUtils.getLocalizedContext(MastodonApp.context, Locale.GERMAN);
|
||||
|
||||
assertEquals("sie/ihr", UiUtils.extractPronouns(german, fakeAccount(
|
||||
makeField("pronomen lauten", "sie/ihr"),
|
||||
makeField("pronouns are", "she/her"),
|
||||
makeField("die pronomen", "stehen oben")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("er/ihm", UiUtils.extractPronouns(german, fakeAccount(
|
||||
makeField("die pronomen", "stehen unten"),
|
||||
makeField("pronomen sind", "er/ihm"),
|
||||
makeField("pronouns are", "he/him")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "-- * (asterisk) --")
|
||||
)).orElseThrow());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import static org.joinmastodon.android.model.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.FilterContext.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class StatusFilterPredicateTest {
|
||||
|
||||
private static final LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter();
|
||||
private static final List<LegacyFilter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||
|
||||
private static final Status
|
||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.joinmastodon.android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
|
||||
|
||||
@@ -100,8 +100,8 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
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){
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||
if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
@@ -148,7 +148,8 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
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){
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber || forceUpdate){
|
||||
forceUpdate=false;
|
||||
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
@@ -323,6 +324,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
getPrefs().edit().clear().apply();
|
||||
File apk=getUpdateApkFile();
|
||||
if(apk.exists())
|
||||
apk.delete();
|
||||
state=UpdateState.NO_UPDATE;
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.joinmastodon.android">
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
@@ -29,7 +29,6 @@
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Mastodon.AutoLightDark"
|
||||
android:largeHeap="true">
|
||||
|
||||
@@ -39,6 +38,21 @@
|
||||
<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"/>
|
||||
@@ -47,7 +61,8 @@
|
||||
<data android:scheme="megalodon-android-auth" android:host="callback"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
||||
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/TransparentDialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -73,6 +88,15 @@
|
||||
<category android:name="me.grishka.fcmtest"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPushNotificationReceiver"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
|
||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
170
mastodon/src/main/assets/blocks.txt
Normal file
170
mastodon/src/main/assets/blocks.txt
Normal file
@@ -0,0 +1,170 @@
|
||||
13bells.com
|
||||
1611.social
|
||||
4aem.com
|
||||
adachi.party
|
||||
anime.website
|
||||
annihilation.social
|
||||
anon-kenkai.com
|
||||
asbestos.cafe
|
||||
bae.st
|
||||
bajax.us
|
||||
banepo.st
|
||||
baraag.net
|
||||
bassam.social
|
||||
beefyboys.win
|
||||
beepboop.ga
|
||||
berserker.town
|
||||
bikeshed.party
|
||||
boks.moe
|
||||
boymoder.biz
|
||||
brainsoap.net
|
||||
breastmilk.club
|
||||
brighteon.social
|
||||
bungle.online
|
||||
cawfee.club
|
||||
clew.lol
|
||||
clubcyberia.co
|
||||
collapsitarian.io
|
||||
comfyboy.club
|
||||
contrapointsfan.club
|
||||
cum.camp
|
||||
cum.salon
|
||||
darknight-coffee.org
|
||||
decayable.ink
|
||||
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
|
||||
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
|
||||
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
|
||||
lolicon.rocks
|
||||
lolison.top
|
||||
lovingexpressions.net
|
||||
mahodou.moe
|
||||
makemysarcophagus.com
|
||||
maladaptive.art
|
||||
marsey.moe
|
||||
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
|
||||
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
|
||||
r18.social
|
||||
rakket.app
|
||||
rapemeat.solutions
|
||||
rdrama.cc
|
||||
rebelbase.site
|
||||
retardedniggers.forsale
|
||||
rojogato.com
|
||||
ryona.agency
|
||||
schwartzwelt.xyz
|
||||
seal.cafe
|
||||
shigusegubu.club
|
||||
shitpost.cloud
|
||||
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
|
||||
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
|
||||
51
mastodon/src/main/assets/server_about_template.htm
Normal file
51
mastodon/src/main/assets/server_about_template.htm
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
body{
|
||||
background: {{colorSurface}};
|
||||
padding: 16px 16px 0 16px;
|
||||
margin: 0;
|
||||
color: {{colorOnSurface}};
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
color: {{colorPrimary}};
|
||||
}
|
||||
p, h1, h2, h3, h4, h5, h6, ul, ol{
|
||||
margin-bottom: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
h1, h2{
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
h3, h4, h5, h6{
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
b, strong{
|
||||
font-weight: 500;
|
||||
}
|
||||
ul, ol{
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
ul>li, ol>li{
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.hootsuite.nachos;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
|
||||
public class ChipConfiguration {
|
||||
|
||||
private final int mChipHorizontalSpacing;
|
||||
private final ColorStateList mChipBackground;
|
||||
private final int mChipCornerRadius;
|
||||
private final int mChipTextColor;
|
||||
private final int mChipTextSize;
|
||||
private final int mChipHeight;
|
||||
private final int mChipVerticalSpacing;
|
||||
private final int mMaxAvailableWidth;
|
||||
|
||||
/**
|
||||
* Creates a new ChipConfiguration. You can pass in {@code -1} or {@code null} for any of the parameters to indicate that parameter should be
|
||||
* ignored.
|
||||
*
|
||||
* @param chipHorizontalSpacing the amount of horizontal space (in pixels) to put between consecutive chips
|
||||
* @param chipBackground the {@link ColorStateList} to set as the background of the chips
|
||||
* @param chipCornerRadius the corner radius of the chip background, in pixels
|
||||
* @param chipTextColor the color to set as the text color of the chips
|
||||
* @param chipTextSize the font size (in pixels) to use for the text of the chips
|
||||
* @param chipHeight the height (in pixels) of each chip
|
||||
* @param chipVerticalSpacing the amount of vertical space (in pixels) to put between chips on consecutive lines
|
||||
* @param maxAvailableWidth the maximum available with for a chip (the width of a full line of text in the text view)
|
||||
*/
|
||||
ChipConfiguration(int chipHorizontalSpacing,
|
||||
ColorStateList chipBackground,
|
||||
int chipCornerRadius,
|
||||
int chipTextColor,
|
||||
int chipTextSize,
|
||||
int chipHeight,
|
||||
int chipVerticalSpacing,
|
||||
int maxAvailableWidth) {
|
||||
mChipHorizontalSpacing = chipHorizontalSpacing;
|
||||
mChipBackground = chipBackground;
|
||||
mChipCornerRadius = chipCornerRadius;
|
||||
mChipTextColor = chipTextColor;
|
||||
mChipTextSize = chipTextSize;
|
||||
mChipHeight = chipHeight;
|
||||
mChipVerticalSpacing = chipVerticalSpacing;
|
||||
mMaxAvailableWidth = maxAvailableWidth;
|
||||
}
|
||||
|
||||
public int getChipHorizontalSpacing() {
|
||||
return mChipHorizontalSpacing;
|
||||
}
|
||||
|
||||
public ColorStateList getChipBackground() {
|
||||
return mChipBackground;
|
||||
}
|
||||
|
||||
public int getChipCornerRadius() {
|
||||
return mChipCornerRadius;
|
||||
}
|
||||
|
||||
public int getChipTextColor() {
|
||||
return mChipTextColor;
|
||||
}
|
||||
|
||||
public int getChipTextSize() {
|
||||
return mChipTextSize;
|
||||
}
|
||||
|
||||
public int getChipHeight() {
|
||||
return mChipHeight;
|
||||
}
|
||||
|
||||
public int getChipVerticalSpacing() {
|
||||
return mChipVerticalSpacing;
|
||||
}
|
||||
|
||||
public int getMaxAvailableWidth() {
|
||||
return mMaxAvailableWidth;
|
||||
}
|
||||
}
|
||||
1132
mastodon/src/main/java/com/hootsuite/nachos/NachoTextView.java
Normal file
1132
mastodon/src/main/java/com/hootsuite/nachos/NachoTextView.java
Normal file
File diff suppressed because it is too large
Load Diff
29
mastodon/src/main/java/com/hootsuite/nachos/chip/Chip.java
Normal file
29
mastodon/src/main/java/com/hootsuite/nachos/chip/Chip.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public interface Chip {
|
||||
|
||||
/**
|
||||
* @return the text represented by this Chip
|
||||
*/
|
||||
CharSequence getText();
|
||||
|
||||
/**
|
||||
* @return the data associated with this Chip or null if no data is associated with it
|
||||
*/
|
||||
@Nullable
|
||||
Object getData();
|
||||
|
||||
/**
|
||||
* @return the width of the Chip or -1 if the Chip hasn't been given the chance to calculate its width
|
||||
*/
|
||||
int getWidth();
|
||||
|
||||
/**
|
||||
* Sets the UI state.
|
||||
*
|
||||
* @param stateSet one of the state constants in {@link android.view.View}
|
||||
*/
|
||||
void setState(int[] stateSet);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
|
||||
/**
|
||||
* Interface to allow the creation and configuration of chips
|
||||
*
|
||||
* @param <C> The type of {@link Chip} that the implementation will create/configure
|
||||
*/
|
||||
public interface ChipCreator<C extends Chip> {
|
||||
|
||||
/**
|
||||
* Creates a chip from the given context and text. Use this method when creating a brand new chip from a piece of text.
|
||||
*
|
||||
* @param context the {@link Context} to use to initialize the chip
|
||||
* @param text the text the Chip should represent
|
||||
* @param data the data to associate with the Chip, or null to associate no data
|
||||
* @return the created chip
|
||||
*/
|
||||
C createChip(@NonNull Context context, @NonNull CharSequence text, @Nullable Object data);
|
||||
|
||||
/**
|
||||
* Creates a chip from the given context and existing chip. Use this method when recreating a chip from an existing one.
|
||||
*
|
||||
* @param context the {@link Context} to use to initialize the chip
|
||||
* @param existingChip the chip that the created chip should be based on
|
||||
* @return the created chip
|
||||
*/
|
||||
C createChip(@NonNull Context context, @NonNull C existingChip);
|
||||
|
||||
/**
|
||||
* Applies the given {@link ChipConfiguration} to the given {@link Chip}. Use this method to customize the appearance/behavior of a chip before
|
||||
* adding it to the text.
|
||||
*
|
||||
* @param chip the chip to configure
|
||||
* @param chipConfiguration the configuration to apply to the chip
|
||||
*/
|
||||
void configureChip(@NonNull C chip, @NonNull ChipConfiguration chipConfiguration);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
public class ChipInfo {
|
||||
|
||||
private final CharSequence mText;
|
||||
private final Object mData;
|
||||
|
||||
public ChipInfo(CharSequence text, Object data) {
|
||||
this.mText = text;
|
||||
this.mData = data;
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return mData;
|
||||
}
|
||||
}
|
||||
510
mastodon/src/main/java/com/hootsuite/nachos/chip/ChipSpan.java
Normal file
510
mastodon/src/main/java/com/hootsuite/nachos/chip/ChipSpan.java
Normal file
@@ -0,0 +1,510 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.style.ImageSpan;
|
||||
|
||||
import androidx.annotation.Dimension;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
/**
|
||||
* A Span that displays text and an optional icon inside of a material design chip. The chip's dimensions, colors etc. can be extensively customized
|
||||
* through the various setter methods available in this class.
|
||||
* The basic structure of the chip is the following:
|
||||
* For chips with the icon on right:
|
||||
* <pre>
|
||||
*
|
||||
* (chip vertical spacing / 2)
|
||||
* ----------------------------------------------------------
|
||||
* | |
|
||||
* (left margin) | (padding edge) text (padding between image) icon | (right margin)
|
||||
* | |
|
||||
* ----------------------------------------------------------
|
||||
* (chip vertical spacing / 2)
|
||||
*
|
||||
* </pre>
|
||||
* For chips with the icon on the left (see {@link #setShowIconOnLeft(boolean)}):
|
||||
* <pre>
|
||||
*
|
||||
* (chip vertical spacing / 2)
|
||||
* ----------------------------------------------------------
|
||||
* | |
|
||||
* (left margin) | icon (padding between image) text (padding edge) | (right margin)
|
||||
* | |
|
||||
* ----------------------------------------------------------
|
||||
* (chip vertical spacing / 2)
|
||||
* </pre>
|
||||
*/
|
||||
public class ChipSpan extends ImageSpan implements Chip {
|
||||
|
||||
private static final float SCALE_PERCENT_OF_CHIP_HEIGHT = 0.70f;
|
||||
private static final boolean ICON_ON_LEFT_DEFAULT = true;
|
||||
|
||||
private int[] mStateSet = new int[]{};
|
||||
|
||||
private String mEllipsis;
|
||||
|
||||
private ColorStateList mDefaultBackgroundColor;
|
||||
private ColorStateList mBackgroundColor;
|
||||
private int mTextColor;
|
||||
private int mCornerRadius = -1;
|
||||
private int mIconBackgroundColor;
|
||||
|
||||
private int mTextSize = -1;
|
||||
private int mPaddingEdgePx;
|
||||
private int mPaddingBetweenImagePx;
|
||||
private int mLeftMarginPx;
|
||||
private int mRightMarginPx;
|
||||
private int mMaxAvailableWidth = -1;
|
||||
|
||||
private CharSequence mText;
|
||||
private String mTextToDraw;
|
||||
|
||||
private Drawable mIcon;
|
||||
private boolean mShowIconOnLeft = ICON_ON_LEFT_DEFAULT;
|
||||
|
||||
private int mChipVerticalSpacing = 0;
|
||||
private int mChipHeight = -1;
|
||||
private int mChipWidth = -1;
|
||||
private int mIconWidth;
|
||||
|
||||
private int mCachedSize = -1;
|
||||
|
||||
private Object mData;
|
||||
|
||||
/**
|
||||
* Constructs a new ChipSpan.
|
||||
*
|
||||
* @param context a {@link Context} that will be used to retrieve default configurations from resource files
|
||||
* @param text the text for the ChipSpan to display
|
||||
* @param icon an optional icon (can be {@code null}) for the ChipSpan to display
|
||||
*/
|
||||
public ChipSpan(@NonNull Context context, @NonNull CharSequence text, @Nullable Drawable icon, Object data) {
|
||||
super(icon);
|
||||
mIcon = icon;
|
||||
mText = text;
|
||||
mTextToDraw = mText.toString();
|
||||
|
||||
mEllipsis = context.getString(R.string.chip_ellipsis);
|
||||
|
||||
mDefaultBackgroundColor = context.getColorStateList(R.color.chip_material_background);
|
||||
mBackgroundColor = mDefaultBackgroundColor;
|
||||
|
||||
mTextColor = context.getColor(R.color.chip_default_text_color);
|
||||
mIconBackgroundColor = context.getColor(R.color.chip_default_icon_background_color);
|
||||
|
||||
Resources resources = context.getResources();
|
||||
mPaddingEdgePx = resources.getDimensionPixelSize(R.dimen.chip_default_padding_edge);
|
||||
mPaddingBetweenImagePx = resources.getDimensionPixelSize(R.dimen.chip_default_padding_between_image);
|
||||
mLeftMarginPx = resources.getDimensionPixelSize(R.dimen.chip_default_left_margin);
|
||||
mRightMarginPx = resources.getDimensionPixelSize(R.dimen.chip_default_right_margin);
|
||||
|
||||
mData = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor to recreate a ChipSpan from an existing one
|
||||
*
|
||||
* @param context a {@link Context} that will be used to retrieve default configurations from resource files
|
||||
* @param chipSpan the ChipSpan to copy
|
||||
*/
|
||||
public ChipSpan(@NonNull Context context, @NonNull ChipSpan chipSpan) {
|
||||
this(context, chipSpan.getText(), chipSpan.getDrawable(), chipSpan.getData());
|
||||
|
||||
mDefaultBackgroundColor = chipSpan.mDefaultBackgroundColor;
|
||||
mTextColor = chipSpan.mTextColor;
|
||||
mIconBackgroundColor = chipSpan.mIconBackgroundColor;
|
||||
mCornerRadius = chipSpan.mCornerRadius;
|
||||
|
||||
mTextSize = chipSpan.mTextSize;
|
||||
mPaddingEdgePx = chipSpan.mPaddingEdgePx;
|
||||
mPaddingBetweenImagePx = chipSpan.mPaddingBetweenImagePx;
|
||||
mLeftMarginPx = chipSpan.mLeftMarginPx;
|
||||
mRightMarginPx = chipSpan.mRightMarginPx;
|
||||
mMaxAvailableWidth = chipSpan.mMaxAvailableWidth;
|
||||
|
||||
mShowIconOnLeft = chipSpan.mShowIconOnLeft;
|
||||
|
||||
mChipVerticalSpacing = chipSpan.mChipVerticalSpacing;
|
||||
mChipHeight = chipSpan.mChipHeight;
|
||||
|
||||
mStateSet = chipSpan.mStateSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getData() {
|
||||
return mData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the chip. This height should not include any extra spacing (for extra vertical spacing call {@link #setChipVerticalSpacing(int)}).
|
||||
* The background of the chip will fill the full height provided here. If this method is never called, the chip will have the height of one full line
|
||||
* of text by default. If {@code -1} is passed here, the chip will revert to this default behavior.
|
||||
*
|
||||
* @param chipHeight the height to set in pixels
|
||||
*/
|
||||
public void setChipHeight(int chipHeight) {
|
||||
mChipHeight = chipHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vertical spacing to include in between chips. Half of the value set here will be placed as empty space above the chip and half the value
|
||||
* will be placed as empty space below the chip. Therefore chips on consecutive lines will have the full value as vertical space in between them.
|
||||
* This spacing is achieved by adjusting the font metrics used by the text view containing these chips; however it does not come into effect until
|
||||
* at least one chip is created. Note that vertical spacing is dependent on having a fixed chip height (set in {@link #setChipHeight(int)}). If a
|
||||
* height is not specified in that method, the value set here will be ignored.
|
||||
*
|
||||
* @param chipVerticalSpacing the vertical spacing to set in pixels
|
||||
*/
|
||||
public void setChipVerticalSpacing(int chipVerticalSpacing) {
|
||||
mChipVerticalSpacing = chipVerticalSpacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the font size for the chip's text. If this method is never called, the chip text will have the same font size as the text in the TextView
|
||||
* containing this chip by default. If {@code -1} is passed here, the chip will revert to this default behavior.
|
||||
*
|
||||
* @param size the font size to set in pixels
|
||||
*/
|
||||
public void setTextSize(int size) {
|
||||
mTextSize = size;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for the chip's text.
|
||||
*
|
||||
* @param color the color to set (as a hexadecimal number in the form 0xAARRGGBB)
|
||||
*/
|
||||
public void setTextColor(int color) {
|
||||
mTextColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets where the icon (if an icon was provided in the constructor) will appear.
|
||||
*
|
||||
* @param showIconOnLeft if true, the icon will appear on the left, otherwise the icon will appear on the right
|
||||
*/
|
||||
public void setShowIconOnLeft(boolean showIconOnLeft) {
|
||||
this.mShowIconOnLeft = showIconOnLeft;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the left margin. This margin will appear as empty space (it will not share the chip's background color) to the left of the chip.
|
||||
*
|
||||
* @param leftMarginPx the left margin to set in pixels
|
||||
*/
|
||||
public void setLeftMargin(int leftMarginPx) {
|
||||
mLeftMarginPx = leftMarginPx;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the right margin. This margin will appear as empty space (it will not share the chip's background color) to the right of the chip.
|
||||
*
|
||||
* @param rightMarginPx the right margin to set in pixels
|
||||
*/
|
||||
public void setRightMargin(int rightMarginPx) {
|
||||
this.mRightMarginPx = rightMarginPx;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background color. To configure which color in the {@link ColorStateList} is shown you can call {@link #setState(int[])}.
|
||||
* Passing {@code null} here will cause the chip to revert to it's default background.
|
||||
*
|
||||
* @param backgroundColor a {@link ColorStateList} containing backgrounds for different states.
|
||||
* @see #setState(int[])
|
||||
*/
|
||||
public void setBackgroundColor(@Nullable ColorStateList backgroundColor) {
|
||||
mBackgroundColor = backgroundColor != null ? backgroundColor : mDefaultBackgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chip background corner radius.
|
||||
*
|
||||
* @param cornerRadius The corner radius value, in pixels.
|
||||
*/
|
||||
public void setCornerRadius(@Dimension int cornerRadius) {
|
||||
mCornerRadius = cornerRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon background color. This is the color of the circle that gets drawn behind the icon passed to the
|
||||
* {@link #ChipSpan(Context, CharSequence, Drawable, Object)} constructor}
|
||||
*
|
||||
* @param iconBackgroundColor the icon background color to set (as a hexadecimal number in the form 0xAARRGGBB)
|
||||
*/
|
||||
public void setIconBackgroundColor(int iconBackgroundColor) {
|
||||
mIconBackgroundColor = iconBackgroundColor;
|
||||
}
|
||||
|
||||
public void setMaxAvailableWidth(int maxAvailableWidth) {
|
||||
mMaxAvailableWidth = maxAvailableWidth;
|
||||
invalidateCachedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UI state. This state will be reflected in the background color drawn for the chip.
|
||||
*
|
||||
* @param stateSet one of the state constants in {@link android.view.View}
|
||||
* @see #setBackgroundColor(ColorStateList)
|
||||
*/
|
||||
@Override
|
||||
public void setState(int[] stateSet) {
|
||||
this.mStateSet = stateSet != null ? stateSet : new int[]{};
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
// If we haven't actually calculated a chip width yet just return -1, otherwise return the chip width + margins
|
||||
return mChipWidth != -1 ? (mLeftMarginPx + mChipWidth + mRightMarginPx) : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
|
||||
boolean usingFontMetrics = (fm != null);
|
||||
|
||||
// Adjust the font metrics regardless of whether or not there is a cached size so that the text view can maintain its height
|
||||
if (usingFontMetrics) {
|
||||
adjustFontMetrics(paint, fm);
|
||||
}
|
||||
|
||||
if (mCachedSize == -1 && usingFontMetrics) {
|
||||
mIconWidth = (mIcon != null) ? calculateChipHeight(fm.top, fm.bottom) : 0;
|
||||
|
||||
int actualWidth = calculateActualWidth(paint);
|
||||
mCachedSize = actualWidth;
|
||||
|
||||
if (mMaxAvailableWidth != -1) {
|
||||
int maxAvailableWidthMinusMargins = mMaxAvailableWidth - mLeftMarginPx - mRightMarginPx;
|
||||
if (actualWidth > maxAvailableWidthMinusMargins) {
|
||||
mTextToDraw = mText + mEllipsis;
|
||||
|
||||
while ((calculateActualWidth(paint) > maxAvailableWidthMinusMargins) && mTextToDraw.length() > 0) {
|
||||
int lastCharacterIndex = mTextToDraw.length() - mEllipsis.length() - 1;
|
||||
if (lastCharacterIndex < 0) {
|
||||
break;
|
||||
}
|
||||
mTextToDraw = mTextToDraw.substring(0, lastCharacterIndex) + mEllipsis;
|
||||
}
|
||||
|
||||
// Avoid a negative width
|
||||
mChipWidth = Math.max(0, maxAvailableWidthMinusMargins);
|
||||
mCachedSize = mMaxAvailableWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mCachedSize;
|
||||
}
|
||||
|
||||
private int calculateActualWidth(Paint paint) {
|
||||
// Only change the text size if a text size was set
|
||||
if (mTextSize != -1) {
|
||||
paint.setTextSize(mTextSize);
|
||||
}
|
||||
|
||||
int totalPadding = mPaddingEdgePx;
|
||||
|
||||
// Find text width
|
||||
Rect bounds = new Rect();
|
||||
paint.getTextBounds(mTextToDraw, 0, mTextToDraw.length(), bounds);
|
||||
int textWidth = bounds.width();
|
||||
|
||||
if (mIcon != null) {
|
||||
totalPadding += mPaddingBetweenImagePx;
|
||||
} else {
|
||||
totalPadding += mPaddingEdgePx;
|
||||
}
|
||||
|
||||
mChipWidth = totalPadding + textWidth + mIconWidth;
|
||||
return getWidth();
|
||||
}
|
||||
|
||||
public void invalidateCachedSize() {
|
||||
mCachedSize = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the provided font metrics to make it seem like the font takes up {@code mChipHeight + mChipVerticalSpacing} pixels in height.
|
||||
* This effectively ensures that the TextView will have a height equal to {@code mChipHeight + mChipVerticalSpacing} + whatever padding it has set.
|
||||
* In {@link #draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)} the chip itself is drawn to that it is vertically centered with
|
||||
* {@code mChipVerticalSpacing / 2} pixels of space above and below it
|
||||
*
|
||||
* @param paint the paint whose font metrics should be adjusted
|
||||
* @param fm the font metrics object to populate through {@link Paint#getFontMetricsInt(Paint.FontMetricsInt)}
|
||||
*/
|
||||
private void adjustFontMetrics(Paint paint, Paint.FontMetricsInt fm) {
|
||||
// Only actually adjust font metrics if we have a chip height set
|
||||
if (mChipHeight != -1) {
|
||||
paint.getFontMetricsInt(fm);
|
||||
int textHeight = fm.descent - fm.ascent;
|
||||
// Break up the vertical spacing in half because half will go above the chip, half will go below the chip
|
||||
int halfSpacing = mChipVerticalSpacing / 2;
|
||||
|
||||
// Given that the text is centered vertically within the chip, the amount of space above or below the text (inbetween the text and chip)
|
||||
// is half their difference in height:
|
||||
int spaceBetweenChipAndText = (mChipHeight - textHeight) / 2;
|
||||
|
||||
int textTop = fm.top;
|
||||
int chipTop = fm.top - spaceBetweenChipAndText;
|
||||
|
||||
int textBottom = fm.bottom;
|
||||
int chipBottom = fm.bottom + spaceBetweenChipAndText;
|
||||
|
||||
// The text may have been taller to begin with so we take the most negative coordinate (highest up) to be the top of the content
|
||||
int topOfContent = Math.min(textTop, chipTop);
|
||||
// Same as above but we want the largest positive coordinate (lowest down) to be the bottom of the content
|
||||
int bottomOfContent = Math.max(textBottom, chipBottom);
|
||||
|
||||
// Shift the top up by halfSpacing and the bottom down by halfSpacing
|
||||
int topOfContentWithSpacing = topOfContent - halfSpacing;
|
||||
int bottomOfContentWithSpacing = bottomOfContent + halfSpacing;
|
||||
|
||||
// Change the font metrics so that the TextView thinks the font takes up the vertical space of a chip + spacing
|
||||
fm.ascent = topOfContentWithSpacing;
|
||||
fm.descent = bottomOfContentWithSpacing;
|
||||
fm.top = topOfContentWithSpacing;
|
||||
fm.bottom = bottomOfContentWithSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateChipHeight(int top, int bottom) {
|
||||
// If a chip height was set we can return that, otherwise calculate it from top and bottom
|
||||
return mChipHeight != -1 ? mChipHeight : bottom - top;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
|
||||
// Shift everything mLeftMarginPx to the left to create an empty space on the left (creating the margin)
|
||||
x += mLeftMarginPx;
|
||||
if (mChipHeight != -1) {
|
||||
// If we set a chip height, adjust to vertically center chip in the line
|
||||
// Adding (bottom - top) / 2 shifts the chip down so the top of it will be centered vertically
|
||||
// Subtracting (mChipHeight / 2) shifts the chip back up so that the center of it will be centered vertically (as desired)
|
||||
top += ((bottom - top) / 2) - (mChipHeight / 2);
|
||||
bottom = top + mChipHeight;
|
||||
}
|
||||
|
||||
// Perform actual drawing
|
||||
drawBackground(canvas, x, top, bottom, paint);
|
||||
drawText(canvas, x, top, bottom, paint, mTextToDraw);
|
||||
if (mIcon != null) {
|
||||
drawIcon(canvas, x, top, bottom, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBackground(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
int backgroundColor = mBackgroundColor.getColorForState(mStateSet, mBackgroundColor.getDefaultColor());
|
||||
paint.setColor(backgroundColor);
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
RectF rect = new RectF(x, top, x + mChipWidth, bottom);
|
||||
int cornerRadius = (mCornerRadius != -1) ? mCornerRadius : height / 2;
|
||||
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
|
||||
paint.setColor(mTextColor);
|
||||
}
|
||||
|
||||
private void drawText(Canvas canvas, float x, int top, int bottom, Paint paint, CharSequence text) {
|
||||
if (mTextSize != -1) {
|
||||
paint.setTextSize(mTextSize);
|
||||
}
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
Paint.FontMetrics fm = paint.getFontMetrics();
|
||||
|
||||
// The top value provided here is the y coordinate for the very top of the chip
|
||||
// The y coordinate we are calculating is where the baseline of the text will be drawn
|
||||
// Our objective is to have the midpoint between the top and baseline of the text be in line with the vertical center of the chip
|
||||
// First we add height / 2 which will put the baseline at the vertical center of the chip
|
||||
// Then we add half the height of the text which will lower baseline so that the midpoint is at the vertical center of the chip as desired
|
||||
float adjustedY = top + ((height / 2) + ((-fm.top - fm.bottom) / 2));
|
||||
|
||||
// The x coordinate provided here is the left-most edge of the chip
|
||||
// If there is no icon or the icon is on the right, then the text will start at the left-most edge, but indented with the edge padding, so we
|
||||
// add mPaddingEdgePx
|
||||
// If there is an icon and it's on the left, the text will start at the left-most edge, but indented by the combined width of the icon and
|
||||
// the padding between the icon and text, so we add (mIconWidth + mPaddingBetweenImagePx)
|
||||
float adjustedX = x + ((mIcon == null || !mShowIconOnLeft) ? mPaddingEdgePx : (mIconWidth + mPaddingBetweenImagePx));
|
||||
|
||||
canvas.drawText(text, 0, text.length(), adjustedX, adjustedY, paint);
|
||||
}
|
||||
|
||||
private void drawIcon(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
drawIconBackground(canvas, x, top, bottom, paint);
|
||||
drawIconBitmap(canvas, x, top, bottom, paint);
|
||||
}
|
||||
|
||||
private void drawIconBackground(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
|
||||
paint.setColor(mIconBackgroundColor);
|
||||
|
||||
// Since it's a circle the diameter is equal to the height, so the radius == diameter / 2 == height / 2
|
||||
int radius = height / 2;
|
||||
// The coordinates that get passed to drawCircle are for the center of the circle
|
||||
// x is the left edge of the chip, (x + mChipWidth) is the right edge of the chip
|
||||
// So the center of the circle is one radius distance from either the left or right edge (depending on which side the icon is being drawn on)
|
||||
float circleX = mShowIconOnLeft ? (x + radius) : (x + mChipWidth - radius);
|
||||
// The y coordinate is always just one radius distance from the top
|
||||
canvas.drawCircle(circleX, top + radius, radius, paint);
|
||||
|
||||
paint.setColor(mTextColor);
|
||||
}
|
||||
|
||||
private void drawIconBitmap(Canvas canvas, float x, int top, int bottom, Paint paint) {
|
||||
int height = calculateChipHeight(top, bottom);
|
||||
|
||||
// Create a scaled down version of the bitmap to fit within the circle (whose diameter == height)
|
||||
Bitmap iconBitmap = Bitmap.createBitmap(mIcon.getIntrinsicWidth(), mIcon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Bitmap scaledIconBitMap = scaleDown(iconBitmap, (float) height * SCALE_PERCENT_OF_CHIP_HEIGHT, true);
|
||||
iconBitmap.recycle();
|
||||
Canvas bitmapCanvas = new Canvas(scaledIconBitMap);
|
||||
mIcon.setBounds(0, 0, bitmapCanvas.getWidth(), bitmapCanvas.getHeight());
|
||||
mIcon.draw(bitmapCanvas);
|
||||
|
||||
// We are drawing a square icon inside of a circle
|
||||
// The coordinates we pass to canvas.drawBitmap have to be for the top-left corner of the bitmap
|
||||
// The bitmap should be inset by half of (circle width - bitmap width)
|
||||
// Since it's a circle, the circle's width is equal to it's height which is equal to the chip height
|
||||
float xInsetWithinCircle = (height - bitmapCanvas.getWidth()) / 2;
|
||||
|
||||
// The icon x coordinate is going to be insetWithinCircle pixels away from the left edge of the circle
|
||||
// If the icon is on the left, the left edge of the circle is just x
|
||||
// If the icon is on the right, the left edge of the circle is x + mChipWidth - height
|
||||
float iconX = mShowIconOnLeft ? (x + xInsetWithinCircle) : (x + mChipWidth - height + xInsetWithinCircle);
|
||||
|
||||
// The y coordinate works the same way (only it's always from the top edge)
|
||||
float yInsetWithinCircle = (height - bitmapCanvas.getHeight()) / 2;
|
||||
float iconY = top + yInsetWithinCircle;
|
||||
|
||||
canvas.drawBitmap(scaledIconBitMap, iconX, iconY, paint);
|
||||
}
|
||||
|
||||
private Bitmap scaleDown(Bitmap realImage, float maxImageSize, boolean filter) {
|
||||
float ratio = Math.min(maxImageSize / realImage.getWidth(), maxImageSize / realImage.getHeight());
|
||||
int width = Math.round(ratio * realImage.getWidth());
|
||||
int height = Math.round(ratio * realImage.getHeight());
|
||||
return Bitmap.createScaledBitmap(realImage, width, height, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mText.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.hootsuite.nachos.chip;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
|
||||
public class ChipSpanChipCreator implements ChipCreator<ChipSpan> {
|
||||
|
||||
@Override
|
||||
public ChipSpan createChip(@NonNull Context context, @NonNull CharSequence text, Object data) {
|
||||
return new ChipSpan(context, text, null, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChipSpan createChip(@NonNull Context context, @NonNull ChipSpan existingChip) {
|
||||
return new ChipSpan(context, existingChip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureChip(@NonNull ChipSpan chip, @NonNull ChipConfiguration chipConfiguration) {
|
||||
int chipHorizontalSpacing = chipConfiguration.getChipHorizontalSpacing();
|
||||
ColorStateList chipBackground = chipConfiguration.getChipBackground();
|
||||
int chipCornerRadius = chipConfiguration.getChipCornerRadius();
|
||||
int chipTextColor = chipConfiguration.getChipTextColor();
|
||||
int chipTextSize = chipConfiguration.getChipTextSize();
|
||||
int chipHeight = chipConfiguration.getChipHeight();
|
||||
int chipVerticalSpacing = chipConfiguration.getChipVerticalSpacing();
|
||||
int maxAvailableWidth = chipConfiguration.getMaxAvailableWidth();
|
||||
|
||||
if (chipHorizontalSpacing != -1) {
|
||||
chip.setLeftMargin(chipHorizontalSpacing / 2);
|
||||
chip.setRightMargin(chipHorizontalSpacing / 2);
|
||||
}
|
||||
if (chipBackground != null) {
|
||||
chip.setBackgroundColor(chipBackground);
|
||||
}
|
||||
if (chipCornerRadius != -1) {
|
||||
chip.setCornerRadius(chipCornerRadius);
|
||||
}
|
||||
if (chipTextColor != Color.TRANSPARENT) {
|
||||
chip.setTextColor(chipTextColor);
|
||||
}
|
||||
if (chipTextSize != -1) {
|
||||
chip.setTextSize(chipTextSize);
|
||||
}
|
||||
if (chipHeight != -1) {
|
||||
chip.setChipHeight(chipHeight);
|
||||
}
|
||||
if (chipVerticalSpacing != -1) {
|
||||
chip.setChipVerticalSpacing(chipVerticalSpacing);
|
||||
}
|
||||
if (maxAvailableWidth != -1) {
|
||||
chip.setMaxAvailableWidth(maxAvailableWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This interface is used to handle the management of characters that should trigger the creation of chips in a text view.
|
||||
*
|
||||
* @see ChipTokenizer
|
||||
*/
|
||||
public interface ChipTerminatorHandler {
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, all tokens in the whole text view will be chipified
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_ALL = 0;
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, only the current token (that in which the chip terminator character
|
||||
* was found) will be chipified. This token may extend beyond where the chip terminator character was located.
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_CURRENT_TOKEN = 1;
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, only the text from the previous chip up until the chip terminator
|
||||
* character will be chipified. This may not be an entire token.
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_TO_TERMINATOR = 2;
|
||||
|
||||
/**
|
||||
* Constant for use with {@link #setPasteBehavior(int)}. Use this if a paste should behave the same as a standard text input (the chip temrinators
|
||||
* will all behave according to their pre-determined behavior set through {@link #addChipTerminator(char, int)} or {@link #setChipTerminators(Map)}).
|
||||
*/
|
||||
int PASTE_BEHAVIOR_USE_DEFAULT = -1;
|
||||
|
||||
/**
|
||||
* Sets all the characters that will be marked as chip terminators. This will replace any previously set chip terminators.
|
||||
*
|
||||
* @param chipTerminators a map of characters to be marked as chip terminators to behaviors that describe how to respond to the characters, or null
|
||||
* to remove all chip terminators
|
||||
*/
|
||||
void setChipTerminators(@Nullable Map<Character, Integer> chipTerminators);
|
||||
|
||||
/**
|
||||
* Adds a character as a chip terminator. When the provided character is encountered in entered text, the nearby text will be chipified according
|
||||
* to the behavior provided here.
|
||||
* {@code behavior} Must be one of:
|
||||
* <ul>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_ALL}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_CURRENT_TOKEN}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_TO_TERMINATOR}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param character the character to mark as a chip terminator
|
||||
* @param behavior the behavior describing how to respond to the chip terminator
|
||||
*/
|
||||
void addChipTerminator(char character, int behavior);
|
||||
|
||||
/**
|
||||
* Customizes the way paste events are handled.
|
||||
* If one of:
|
||||
* <ul>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_ALL}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_CURRENT_TOKEN}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_TO_TERMINATOR}</li>
|
||||
* </ul>
|
||||
* is passed, all chip terminators will be handled with that behavior when a paste event occurs.
|
||||
* If {@link #PASTE_BEHAVIOR_USE_DEFAULT} is passed, whatever behavior is configured for a particular chip terminator
|
||||
* (through {@link #setChipTerminators(Map)} or {@link #addChipTerminator(char, int)} will be used for that chip terminator
|
||||
*
|
||||
* @param pasteBehavior the behavior to use on a paste event
|
||||
*/
|
||||
void setPasteBehavior(int pasteBehavior);
|
||||
|
||||
/**
|
||||
* Parses the provided text looking for characters marked as chip terminators through {@link #addChipTerminator(char, int)} and {@link #setChipTerminators(Map)}.
|
||||
* The provided {@link Editable} will be modified if chip terminators are encountered.
|
||||
*
|
||||
* @param tokenizer the {@link ChipTokenizer} to use to identify and chipify tokens in the text
|
||||
* @param text the text in which to search for chip terminators tokens to be chipped
|
||||
* @param start the index at which to begin looking for chip terminators (inclusive)
|
||||
* @param end the index at which to end looking for chip terminators (exclusive)
|
||||
* @param isPasteEvent true if this handling is for a paste event in which case the behavior set in {@link #setPasteBehavior(int)} will be used,
|
||||
* otherwise false
|
||||
* @return an non-negative integer indicating the index where the cursor (selection) should be placed once the handling is complete,
|
||||
* or a negative integer indicating that the cursor should not be moved.
|
||||
*/
|
||||
int findAndHandleChipTerminators(@NonNull ChipTokenizer tokenizer, @NonNull Editable text, int start, int end, boolean isPasteEvent);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DefaultChipTerminatorHandler implements ChipTerminatorHandler {
|
||||
|
||||
@Nullable
|
||||
private Map<Character, Integer> mChipTerminators;
|
||||
private int mPasteBehavior = BEHAVIOR_CHIPIFY_TO_TERMINATOR;
|
||||
|
||||
@Override
|
||||
public void setChipTerminators(@Nullable Map<Character, Integer> chipTerminators) {
|
||||
mChipTerminators = chipTerminators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChipTerminator(char character, int behavior) {
|
||||
if (mChipTerminators == null) {
|
||||
mChipTerminators = new HashMap<>();
|
||||
}
|
||||
|
||||
mChipTerminators.put(character, behavior);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPasteBehavior(int pasteBehavior) {
|
||||
mPasteBehavior = pasteBehavior;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findAndHandleChipTerminators(@NonNull ChipTokenizer tokenizer, @NonNull Editable text, int start, int end, boolean isPasteEvent) {
|
||||
// If we don't have a tokenizer or any chip terminators, there's nothing to look for
|
||||
if (mChipTerminators == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
TextIterator textIterator = new TextIterator(text, start, end);
|
||||
int selectionIndex = -1;
|
||||
|
||||
characterLoop:
|
||||
while (textIterator.hasNextCharacter()) {
|
||||
char theChar = textIterator.nextCharacter();
|
||||
if (isChipTerminator(theChar)) {
|
||||
int behavior = (isPasteEvent && mPasteBehavior != PASTE_BEHAVIOR_USE_DEFAULT) ? mPasteBehavior : mChipTerminators.get(theChar);
|
||||
int newSelection = -1;
|
||||
switch (behavior) {
|
||||
case BEHAVIOR_CHIPIFY_ALL:
|
||||
selectionIndex = handleChipifyAll(textIterator, tokenizer);
|
||||
break characterLoop;
|
||||
case BEHAVIOR_CHIPIFY_CURRENT_TOKEN:
|
||||
newSelection = handleChipifyCurrentToken(textIterator, tokenizer);
|
||||
break;
|
||||
case BEHAVIOR_CHIPIFY_TO_TERMINATOR:
|
||||
newSelection = handleChipifyToTerminator(textIterator, tokenizer);
|
||||
break;
|
||||
}
|
||||
|
||||
if (newSelection != -1) {
|
||||
selectionIndex = newSelection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectionIndex;
|
||||
}
|
||||
|
||||
private int handleChipifyAll(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
textIterator.deleteCharacter(true);
|
||||
tokenizer.terminateAllTokens(textIterator.getText());
|
||||
return textIterator.totalLength();
|
||||
}
|
||||
|
||||
private int handleChipifyCurrentToken(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
textIterator.deleteCharacter(true);
|
||||
Editable text = textIterator.getText();
|
||||
int index = textIterator.getIndex();
|
||||
int tokenStart = tokenizer.findTokenStart(text, index);
|
||||
int tokenEnd = tokenizer.findTokenEnd(text, index);
|
||||
if (tokenStart < tokenEnd) {
|
||||
CharSequence chippedText = tokenizer.terminateToken(text.subSequence(tokenStart, tokenEnd), null);
|
||||
textIterator.replace(tokenStart, tokenEnd, chippedText);
|
||||
return tokenStart + chippedText.length();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int handleChipifyToTerminator(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
Editable text = textIterator.getText();
|
||||
int index = textIterator.getIndex();
|
||||
if (index > 0) {
|
||||
int tokenStart = tokenizer.findTokenStart(text, index);
|
||||
if (tokenStart < index) {
|
||||
CharSequence chippedText = tokenizer.terminateToken(text.subSequence(tokenStart, index), null);
|
||||
textIterator.replace(tokenStart, index + 1, chippedText);
|
||||
} else {
|
||||
textIterator.deleteCharacter(false);
|
||||
}
|
||||
} else {
|
||||
textIterator.deleteCharacter(false);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isChipTerminator(char character) {
|
||||
return mChipTerminators != null && mChipTerminators.keySet().contains(character);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
public class TextIterator {
|
||||
|
||||
private Editable mText;
|
||||
private int mStart;
|
||||
private int mEnd;
|
||||
|
||||
private int mIndex;
|
||||
|
||||
public TextIterator(Editable text, int start, int end) {
|
||||
mText = text;
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
|
||||
mIndex = mStart - 1; // Subtract 1 so that the first call to nextCharacter() will return the first character
|
||||
}
|
||||
|
||||
public int totalLength() {
|
||||
return mText.length();
|
||||
}
|
||||
|
||||
public int windowLength() {
|
||||
return mEnd - mStart;
|
||||
}
|
||||
|
||||
public Editable getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
public boolean hasNextCharacter() {
|
||||
return (mIndex + 1) < mEnd;
|
||||
}
|
||||
|
||||
public char nextCharacter() {
|
||||
mIndex++;
|
||||
return mText.charAt(mIndex);
|
||||
}
|
||||
|
||||
public void deleteCharacter(boolean maintainIndex) {
|
||||
mText.replace(mIndex, mIndex + 1, "");
|
||||
if (!maintainIndex) {
|
||||
mIndex--;
|
||||
}
|
||||
mEnd--;
|
||||
}
|
||||
|
||||
public void replace(int replaceStart, int replaceEnd, CharSequence chippedText) {
|
||||
mText.replace(replaceStart, replaceEnd, chippedText);
|
||||
|
||||
// Update indexes
|
||||
int newLength = chippedText.length();
|
||||
int oldLength = replaceEnd - replaceStart;
|
||||
mIndex = replaceStart + newLength - 1;
|
||||
mEnd += newLength - oldLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of the {@link ChipTokenizer} interface that performs no actions and returns default values.
|
||||
* This class allows for the easy creation of a ChipTokenizer that only implements some of the methods of the interface.
|
||||
*/
|
||||
public abstract class BaseChipTokenizer implements ChipTokenizer {
|
||||
|
||||
@Override
|
||||
public void applyConfiguration(Editable text, ChipConfiguration chipConfiguration) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenStart(CharSequence charSequence, int i) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenEnd(CharSequence charSequence, int i) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Pair<Integer, Integer>> findAllTokens(CharSequence text) {
|
||||
// Do nothing
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence terminateToken(CharSequence charSequence, @Nullable Object data) {
|
||||
// Do nothing
|
||||
return charSequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAllTokens(Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipStart(Chip chip, Spanned text) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipEnd(Chip chip, Spanned text) {
|
||||
// Do nothing
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Chip[] findAllChips(int start, int end, Spanned text) {
|
||||
return new Chip[]{};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revertChipToToken(Chip chip, Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChip(Chip chip, Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChipAndPadding(Chip chip, Editable text) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An extension of {@link android.widget.MultiAutoCompleteTextView.Tokenizer Tokenizer} that provides extra support
|
||||
* for chipification.
|
||||
* <p>
|
||||
* In the context of this interface, a token is considered to be plain (non-chipped) text. Once a token is terminated it becomes or contains a chip.
|
||||
* </p>
|
||||
* <p>
|
||||
* The CharSequences passed to the ChipTokenizer methods may contain both chipped text
|
||||
* and plain text so the tokenizer must have some method of distinguishing between the two (e.g. using a delimeter character.
|
||||
* The {@link #terminateToken(CharSequence, Object)} method is where a chip can be formed and returned to replace the plain text.
|
||||
* Whatever class the implementation deems to represent a chip, must implement the {@link Chip} interface.
|
||||
* </p>
|
||||
*
|
||||
* @see SpanChipTokenizer
|
||||
*/
|
||||
public interface ChipTokenizer {
|
||||
|
||||
/**
|
||||
* Configures this ChipTokenizer to produce chips with the provided attributes. For each of these attributes, {@code -1} or {@code null} may be
|
||||
* passed to indicate that the attribute may be ignored.
|
||||
* <p>
|
||||
* This will also apply the provided {@link ChipConfiguration} to any existing chips in the provided text.
|
||||
* </p>
|
||||
*
|
||||
* @param text the text in which to search for existing chips to apply the configuration to
|
||||
* @param chipConfiguration a {@link ChipConfiguration} containing customizations for the chips produced by this class
|
||||
*/
|
||||
void applyConfiguration(Editable text, ChipConfiguration chipConfiguration);
|
||||
|
||||
/**
|
||||
* Returns the start of the token that ends at offset
|
||||
* <code>cursor</code> within <code>text</code>.
|
||||
*/
|
||||
int findTokenStart(CharSequence text, int cursor);
|
||||
|
||||
/**
|
||||
* Returns the end of the token (minus trailing punctuation)
|
||||
* that begins at offset <code>cursor</code> within <code>text</code>.
|
||||
*/
|
||||
int findTokenEnd(CharSequence text, int cursor);
|
||||
|
||||
/**
|
||||
* Searches through {@code text} for any tokens.
|
||||
*
|
||||
* @param text the text in which to search for un-terminated tokens
|
||||
* @return a list of {@link Pair}s of the form (startIndex, endIndex) containing the locations of all
|
||||
* unterminated tokens
|
||||
*/
|
||||
@NonNull
|
||||
List<Pair<Integer, Integer>> findAllTokens(CharSequence text);
|
||||
|
||||
/**
|
||||
* Returns <code>text</code>, modified, if necessary, to ensure that
|
||||
* it ends with a token terminator (for example a space or comma).
|
||||
*/
|
||||
CharSequence terminateToken(CharSequence text, @Nullable Object data);
|
||||
|
||||
/**
|
||||
* Terminates (converts from token into chip) all unterminated tokens in the provided text.
|
||||
* This method CAN alter the provided text.
|
||||
*
|
||||
* @param text the text in which to terminate all tokens
|
||||
*/
|
||||
void terminateAllTokens(Editable text);
|
||||
|
||||
/**
|
||||
* Finds the index of the first character in {@code text} that is a part of {@code chip}
|
||||
*
|
||||
* @param chip the chip whose start should be found
|
||||
* @param text the text in which to search for the start of {@code chip}
|
||||
* @return the start index of the chip
|
||||
*/
|
||||
int findChipStart(Chip chip, Spanned text);
|
||||
|
||||
/**
|
||||
* Finds the index of the character after the last character in {@code text} that is a part of {@code chip}
|
||||
*
|
||||
* @param chip the chip whose end should be found
|
||||
* @param text the text in which to search for the end of {@code chip}
|
||||
* @return the end index of the chip
|
||||
*/
|
||||
int findChipEnd(Chip chip, Spanned text);
|
||||
|
||||
/**
|
||||
* Searches through {@code text} for any chips
|
||||
*
|
||||
* @param start index to start looking for terminated tokens (inclusive)
|
||||
* @param end index to end looking for terminated tokens (exclusive)
|
||||
* @param text the text in which to search for terminated tokens
|
||||
* @return a list of objects implementing the {@link Chip} interface to represent the terminated tokens
|
||||
*/
|
||||
@NonNull
|
||||
Chip[] findAllChips(int start, int end, Spanned text);
|
||||
|
||||
/**
|
||||
* Effectively does the opposite of {@link #terminateToken(CharSequence, Object)} by reverting the provided chip back into a token.
|
||||
* This method CAN alter the provided text.
|
||||
*
|
||||
* @param chip the chip to revert into a token
|
||||
* @param text the text in which the chip resides
|
||||
*/
|
||||
void revertChipToToken(Chip chip, Editable text);
|
||||
|
||||
/**
|
||||
* Removes a chip and any text it encompasses from {@code text}. This method CAN alter the provided text.
|
||||
*
|
||||
* @param chip the chip to remove
|
||||
* @param text the text to remove the chip from
|
||||
*/
|
||||
void deleteChip(Chip chip, Editable text);
|
||||
|
||||
/**
|
||||
* Removes a chip, any text it encompasses AND any padding text (such as spaces) that may have been inserted when the chip was created in
|
||||
* {@link #terminateToken(CharSequence, Object)} or after. This method CAN alter the provided text.
|
||||
*
|
||||
* @param chip the chip to remove
|
||||
* @param text the text to remove the chip and padding from
|
||||
*/
|
||||
void deleteChipAndPadding(Chip chip, Editable text);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
import com.hootsuite.nachos.chip.ChipCreator;
|
||||
import com.hootsuite.nachos.chip.ChipSpan;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ChipTokenizer}.
|
||||
* This implementation does the following:
|
||||
* <ul>
|
||||
* <li>Surrounds each token with a space and the Unit Separator ASCII control character (31) - See the diagram below
|
||||
* <ul>
|
||||
* <li>The spaces are included so that android keyboards can distinguish the chips as different words and provide accurate
|
||||
* autocorrect suggestions</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Replaces each token with a {@link ChipSpan} containing the same text, once the token terminates</li>
|
||||
* <li>Uses the values passed to {@link #applyConfiguration(Editable, ChipConfiguration)} to configure any ChipSpans that get created</li>
|
||||
* </ul>
|
||||
* Each terminated token will therefore look like the following (this is what will be returned from {@link #terminateToken(CharSequence, Object)}):
|
||||
* <pre>
|
||||
* -----------------------------------------------------------
|
||||
* | SpannableString |
|
||||
* | ---------------------------------------------------- |
|
||||
* | | ChipSpan | |
|
||||
* | | | |
|
||||
* | | space separator text separator space | |
|
||||
* | | | |
|
||||
* | ---------------------------------------------------- |
|
||||
* -----------------------------------------------------------
|
||||
* </pre>
|
||||
*
|
||||
* @see ChipSpan
|
||||
*/
|
||||
public class SpanChipTokenizer<C extends Chip> implements ChipTokenizer {
|
||||
|
||||
/**
|
||||
* The character used to separate chips internally is the US (Unit Separator) ASCII control character.
|
||||
* This character is used because it's untypable so we have complete control over when chips are created.
|
||||
*/
|
||||
public static final char CHIP_SPAN_SEPARATOR = 31;
|
||||
public static final char AUTOCORRECT_SEPARATOR = ' ';
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@Nullable
|
||||
private ChipConfiguration mChipConfiguration;
|
||||
@NonNull
|
||||
private ChipCreator<C> mChipCreator;
|
||||
@NonNull
|
||||
private Class<C> mChipClass;
|
||||
|
||||
private Comparator<Pair<Integer, Integer>> mReverseTokenIndexesSorter = new Comparator<Pair<Integer, Integer>>() {
|
||||
@Override
|
||||
public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) {
|
||||
return rhs.first - lhs.first;
|
||||
}
|
||||
};
|
||||
|
||||
public SpanChipTokenizer(Context context, @NonNull ChipCreator<C> chipCreator, @NonNull Class<C> chipClass) {
|
||||
mContext = context;
|
||||
mChipCreator = chipCreator;
|
||||
mChipClass = chipClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyConfiguration(Editable text, ChipConfiguration chipConfiguration) {
|
||||
mChipConfiguration = chipConfiguration;
|
||||
|
||||
for (C chip : findAllChips(0, text.length(), text)) {
|
||||
// Recreate the chips with the new configuration
|
||||
int chipStart = findChipStart(chip, text);
|
||||
deleteChip(chip, text);
|
||||
text.insert(chipStart, terminateToken(mChipCreator.createChip(mContext, chip)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenStart(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
|
||||
// Work backwards until we find a CHIP_SPAN_SEPARATOR
|
||||
while (i > 0 && text.charAt(i - 1) != CHIP_SPAN_SEPARATOR) {
|
||||
i--;
|
||||
}
|
||||
// Work forwards to skip over any extra whitespace at the beginning of the token
|
||||
while (i > 0 && i < text.length() && Character.isWhitespace(text.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenEnd(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
int len = text.length();
|
||||
|
||||
// Work forwards till we find a CHIP_SPAN_SEPARATOR
|
||||
while (i < len) {
|
||||
if (text.charAt(i) == CHIP_SPAN_SEPARATOR) {
|
||||
return (i - 1); // subtract one because the CHIP_SPAN_SEPARATOR will be preceded by a space
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Pair<Integer, Integer>> findAllTokens(CharSequence text) {
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = new ArrayList<>();
|
||||
|
||||
boolean insideChip = false;
|
||||
// Iterate backwards through the text (to avoid messing up indexes)
|
||||
for (int index = text.length() - 1; index >= 0; index--) {
|
||||
char theCharacter = text.charAt(index);
|
||||
|
||||
// Every time we hit a CHIP_SPAN_SEPARATOR character we switch from being inside to outside
|
||||
// or outside to inside a chip
|
||||
// This check must happen before the whitespace check because CHIP_SPAN_SEPARATOR is considered a whitespace character
|
||||
if (theCharacter == CHIP_SPAN_SEPARATOR) {
|
||||
insideChip = !insideChip;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Completely skip over whitespace
|
||||
if (Character.isWhitespace(theCharacter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're ever outside a chip, see if the text we're in is a viable token for chipification
|
||||
if (!insideChip) {
|
||||
int tokenStart = findTokenStart(text, index);
|
||||
int tokenEnd = findTokenEnd(text, index);
|
||||
|
||||
// Can only actually be chipified if there's at least one character between them
|
||||
if (tokenEnd - tokenStart >= 1) {
|
||||
unterminatedTokens.add(new Pair<>(tokenStart, tokenEnd));
|
||||
index = tokenStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
return unterminatedTokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence terminateToken(CharSequence text, @Nullable Object data) {
|
||||
// Remove leading/trailing whitespace
|
||||
CharSequence trimmedText = text.toString().trim();
|
||||
return terminateToken(mChipCreator.createChip(mContext, trimmedText, data));
|
||||
}
|
||||
|
||||
private CharSequence terminateToken(C chip) {
|
||||
// Surround the text with CHIP_SPAN_SEPARATOR and spaces
|
||||
// The spaces allow autocorrect to correctly identify words
|
||||
String chipSeparator = Character.toString(CHIP_SPAN_SEPARATOR);
|
||||
String autoCorrectSeparator = Character.toString(AUTOCORRECT_SEPARATOR);
|
||||
CharSequence textWithSeparator = autoCorrectSeparator + chipSeparator + chip.getText() + chipSeparator + autoCorrectSeparator;
|
||||
|
||||
// Build the container object to house the ChipSpan and space
|
||||
SpannableString spannableString = new SpannableString(textWithSeparator);
|
||||
|
||||
// Attach the ChipSpan
|
||||
if (mChipConfiguration != null) {
|
||||
mChipCreator.configureChip(chip, mChipConfiguration);
|
||||
}
|
||||
spannableString.setSpan(chip, 0, textWithSeparator.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return spannableString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAllTokens(Editable text) {
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = findAllTokens(text);
|
||||
// Sort in reverse order (so index changes don't affect anything)
|
||||
Collections.sort(unterminatedTokens, mReverseTokenIndexesSorter);
|
||||
for (Pair<Integer, Integer> indexes : unterminatedTokens) {
|
||||
int start = indexes.first;
|
||||
int end = indexes.second;
|
||||
CharSequence textToChip = text.subSequence(start, end);
|
||||
CharSequence chippedText = terminateToken(textToChip, null);
|
||||
text.replace(start, end, chippedText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipStart(Chip chip, Spanned text) {
|
||||
return text.getSpanStart(chip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipEnd(Chip chip, Spanned text) {
|
||||
return text.getSpanEnd(chip);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NonNull
|
||||
@Override
|
||||
public C[] findAllChips(int start, int end, Spanned text) {
|
||||
C[] spansArray = text.getSpans(start, end, mChipClass);
|
||||
return (spansArray != null) ? spansArray : (C[]) Array.newInstance(mChipClass, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revertChipToToken(Chip chip, Editable text) {
|
||||
int chipStart = findChipStart(chip, text);
|
||||
int chipEnd = findChipEnd(chip, text);
|
||||
text.removeSpan(chip);
|
||||
text.replace(chipStart, chipEnd, chip.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChip(Chip chip, Editable text) {
|
||||
int chipStart = findChipStart(chip, text);
|
||||
int chipEnd = findChipEnd(chip, text);
|
||||
text.removeSpan(chip);
|
||||
// On the emulator for some reason the text automatically gets deleted and chipStart and chipEnd end up both being -1, so in that case we
|
||||
// don't need to call text.delete(...)
|
||||
if (chipStart != chipEnd) {
|
||||
text.delete(chipStart, chipEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChipAndPadding(Chip chip, Editable text) {
|
||||
// This implementation does not add any extra padding outside of the span so we can just delete the chip normally
|
||||
deleteChip(chip, text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.hootsuite.nachos.validator;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link NachoValidator} that deems text to be invalid if it contains
|
||||
* unterminated tokens and fixes the text by chipifying all the unterminated tokens.
|
||||
*/
|
||||
public class ChipifyingNachoValidator implements NachoValidator {
|
||||
|
||||
@Override
|
||||
public boolean isValid(@NonNull ChipTokenizer chipTokenizer, CharSequence text) {
|
||||
|
||||
// The text is considered valid if there are no unterminated tokens (everything is a chip)
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = chipTokenizer.findAllTokens(text);
|
||||
return unterminatedTokens.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence fixText(@NonNull ChipTokenizer chipTokenizer, CharSequence invalidText) {
|
||||
SpannableStringBuilder newText = new SpannableStringBuilder(invalidText);
|
||||
chipTokenizer.terminateAllTokens(newText);
|
||||
return newText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.hootsuite.nachos.validator;
|
||||
|
||||
public interface IllegalCharacterIdentifier {
|
||||
boolean isCharacterIllegal(Character c);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hootsuite.nachos.validator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
/**
|
||||
* Interface used to ensure that a given CharSequence complies to a particular format.
|
||||
*/
|
||||
public interface NachoValidator {
|
||||
|
||||
/**
|
||||
* Validates the specified text.
|
||||
*
|
||||
* @return true If the text currently in the text editor is valid.
|
||||
* @see #fixText(ChipTokenizer, CharSequence)
|
||||
*/
|
||||
boolean isValid(@NonNull ChipTokenizer chipTokenizer, CharSequence text);
|
||||
|
||||
/**
|
||||
* Corrects the specified text to make it valid.
|
||||
*
|
||||
* @param invalidText A string that doesn't pass validation: isValid(invalidText)
|
||||
* returns false
|
||||
* @return A string based on invalidText such as invoking isValid() on it returns true.
|
||||
* @see #isValid(ChipTokenizer, CharSequence)
|
||||
*/
|
||||
CharSequence fixText(@NonNull ChipTokenizer chipTokenizer, CharSequence invalidText);
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
|
||||
private static HashSet<Callback> callbacks=new HashSet<>();
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
||||
private boolean resumeAfterAudioFocusGain;
|
||||
private boolean isBuffering=true;
|
||||
|
||||
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
@@ -169,13 +169,15 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
updateNotification(false, false);
|
||||
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
int audiofocus = GlobalUserPreferences.overlayMedia ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN;
|
||||
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, audiofocus);
|
||||
|
||||
player=new MediaPlayer();
|
||||
player.setOnPreparedListener(this::onPlayerPrepared);
|
||||
player.setOnErrorListener(this::onPlayerError);
|
||||
player.setOnCompletionListener(this::onPlayerCompletion);
|
||||
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
||||
player.setOnInfoListener(this::onPlayerInfo);
|
||||
try{
|
||||
player.setDataSource(this, Uri.parse(attachment.url));
|
||||
player.prepareAsync();
|
||||
@@ -187,7 +189,9 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
private void onPlayerPrepared(MediaPlayer mp){
|
||||
Log.i(TAG, "onPlayerPrepared");
|
||||
playerReady=true;
|
||||
isBuffering=false;
|
||||
player.start();
|
||||
updateSessionState(false);
|
||||
}
|
||||
@@ -205,6 +209,21 @@ public class AudioPlayerService extends Service{
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
|
||||
switch(what){
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
||||
isBuffering=true;
|
||||
updateSessionState(false);
|
||||
}
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
||||
isBuffering=false;
|
||||
updateSessionState(false);
|
||||
}
|
||||
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onAudioFocusChanged(int change){
|
||||
switch(change){
|
||||
case AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
@@ -212,7 +231,7 @@ public class AudioPlayerService extends Service{
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
resumeAfterAudioFocusGain=true;
|
||||
resumeAfterAudioFocusGain=isPlaying();
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
@@ -232,12 +251,16 @@ public class AudioPlayerService extends Service{
|
||||
|
||||
private void updateSessionState(boolean removeNotification){
|
||||
session.setPlaybackState(new PlaybackState.Builder()
|
||||
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f)
|
||||
.setState(switch(getPlayState()){
|
||||
case PLAYING -> PlaybackState.STATE_PLAYING;
|
||||
case PAUSED -> PlaybackState.STATE_PAUSED;
|
||||
case BUFFERING -> PlaybackState.STATE_BUFFERING;
|
||||
}, player.getCurrentPosition(), 1f)
|
||||
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
||||
.build());
|
||||
updateNotification(!player.isPlaying(), removeNotification);
|
||||
for(Callback cb:callbacks)
|
||||
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition());
|
||||
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
|
||||
}
|
||||
|
||||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||
@@ -310,6 +333,12 @@ public class AudioPlayerService extends Service{
|
||||
return attachment.id;
|
||||
}
|
||||
|
||||
public PlayState getPlayState(){
|
||||
if(isBuffering)
|
||||
return PlayState.BUFFERING;
|
||||
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
|
||||
}
|
||||
|
||||
public static void registerCallback(Callback cb){
|
||||
callbacks.add(cb);
|
||||
}
|
||||
@@ -333,7 +362,13 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onPlayStateChanged(String attachmentID, boolean playing, int position);
|
||||
void onPlayStateChanged(String attachmentID, PlayState state, int position);
|
||||
void onPlaybackStopped(String attachmentID);
|
||||
}
|
||||
|
||||
public enum PlayState{
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
BUFFERING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,21 +3,25 @@ package org.joinmastodon.android;
|
||||
import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
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;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
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;
|
||||
@@ -28,18 +32,52 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
|
||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).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){
|
||||
} 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((x) -> finish());
|
||||
}));
|
||||
} else {
|
||||
openComposeFragment(accountId);
|
||||
}
|
||||
});
|
||||
sheet.show();
|
||||
} else if (sessions.size() == 1) {
|
||||
openComposeFragment(sessions.get(0).getID());
|
||||
}else{
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
|
||||
UiUtils.pickAccount(this, null, R.string.choose_account, 0,
|
||||
session -> openComposeFragment(session.getID()),
|
||||
b -> b.setOnCancelListener(d -> finish())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,113 +4,130 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
private static final String TAG="GlobalUserPreferences";
|
||||
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||
public static ThemePreference theme;
|
||||
|
||||
// MEGALODON
|
||||
public static boolean trueBlackTheme;
|
||||
public static boolean showReplies;
|
||||
public static boolean showBoosts;
|
||||
public static boolean loadNewPosts;
|
||||
public static boolean showNewPostsButton;
|
||||
public static boolean showInteractionCounts;
|
||||
public static boolean alwaysExpandContentWarnings;
|
||||
public static boolean disableMarquee;
|
||||
public static boolean toolbarMarquee;
|
||||
public static boolean disableSwipe;
|
||||
public static boolean voteButtonForSingleChoice;
|
||||
public static boolean enableDeleteNotifications;
|
||||
public static boolean translateButtonOpenedOnly;
|
||||
public static boolean uniformNotificationIcon;
|
||||
public static boolean reduceMotion;
|
||||
public static boolean keepOnlyLatestNotification;
|
||||
public static boolean disableAltTextReminder;
|
||||
public static boolean showAltIndicator;
|
||||
public static boolean showNoAltIndicator;
|
||||
public static boolean enablePreReleases;
|
||||
public static boolean prefixRepliesWithRe;
|
||||
public static boolean bottomEncoding;
|
||||
public static PrefixRepliesMode prefixReplies;
|
||||
public static boolean collapseLongPosts;
|
||||
public static boolean spectatorMode;
|
||||
public static boolean autoHideFab;
|
||||
public static boolean replyLineAboveHeader;
|
||||
public static boolean compactReblogReplyLine;
|
||||
public static boolean confirmBeforeReblog;
|
||||
public static String publishButtonText;
|
||||
public static ThemePreference theme;
|
||||
public static boolean allowRemoteLoading;
|
||||
public static boolean forwardReportDefault;
|
||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||
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();
|
||||
public static Map<String, List<String>> recentLanguages;
|
||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||
public static Set<String> accountsWithLocalOnlySupport;
|
||||
public static Set<String> accountsInGlitchMode;
|
||||
|
||||
/**
|
||||
* Pleroma
|
||||
*/
|
||||
public static String replyVisibility;
|
||||
public static boolean disableM3PillActiveIndicator;
|
||||
public static boolean showNavigationLabels;
|
||||
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
|
||||
public static boolean overlayMedia;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static <T> T fromJson(String json, Type type, T orElse) {
|
||||
if (json == null) return orElse;
|
||||
try { return gson.fromJson(json, type); }
|
||||
catch (JsonSyntaxException ignored) { return orElse; }
|
||||
public static <T> T fromJson(String json, Type type, T orElse){
|
||||
if(json==null) return orElse;
|
||||
try{
|
||||
T value=gson.fromJson(json, type);
|
||||
return value==null ? orElse : value;
|
||||
}catch(JsonSyntaxException ignored){
|
||||
return orElse;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> T enumValue(Class<T> enumType, String name) {
|
||||
try { return Enum.valueOf(enumType, name); }
|
||||
catch (NullPointerException npe) { return null; }
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
altTextReminders=prefs.getBoolean("altTextReminders", true);
|
||||
confirmUnfollow=prefs.getBoolean("confirmUnfollow", true);
|
||||
confirmBoost=prefs.getBoolean("confirmBoost", false);
|
||||
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
|
||||
|
||||
// MEGALODON
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
||||
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||
toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
|
||||
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
||||
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
||||
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
||||
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
reduceMotion=prefs.getBoolean("reduceMotion", false);
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false);
|
||||
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
|
||||
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
|
||||
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
|
||||
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
|
||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||
prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name()));
|
||||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
||||
publishButtonText=prefs.getString("publishButtonText", "");
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
|
||||
pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
|
||||
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
replyVisibility=prefs.getString("replyVisibility", null);
|
||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
||||
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
|
||||
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
|
||||
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
|
||||
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
|
||||
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
|
||||
overlayMedia=prefs.getBoolean("overlayMedia", false);
|
||||
|
||||
if (prefs.contains("prefixRepliesWithRe")) {
|
||||
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
|
||||
? PrefixRepliesMode.TO_OTHERS : PrefixRepliesMode.NEVER;
|
||||
prefs.edit()
|
||||
.putString("prefixReplies", prefixReplies.name())
|
||||
.remove("prefixRepliesWithRe")
|
||||
.apply();
|
||||
}
|
||||
|
||||
try {
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
||||
@@ -118,49 +135,101 @@ public class GlobalUserPreferences{
|
||||
// invalid color name or color was previously saved as integer
|
||||
color=ColorPreference.PINK;
|
||||
}
|
||||
|
||||
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
|
||||
}
|
||||
|
||||
public static void save(){
|
||||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putBoolean("altTextReminders", altTextReminders)
|
||||
.putBoolean("confirmUnfollow", confirmUnfollow)
|
||||
.putBoolean("confirmBoost", confirmBoost)
|
||||
.putBoolean("confirmDeletePost", confirmDeletePost)
|
||||
|
||||
// MEGALODON
|
||||
.putBoolean("loadNewPosts", loadNewPosts)
|
||||
.putBoolean("showNewPostsButton", showNewPostsButton)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putBoolean("showInteractionCounts", showInteractionCounts)
|
||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||
.putBoolean("disableMarquee", disableMarquee)
|
||||
.putBoolean("toolbarMarquee", toolbarMarquee)
|
||||
.putBoolean("disableSwipe", disableSwipe)
|
||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||
.putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly)
|
||||
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
|
||||
.putBoolean("reduceMotion", reduceMotion)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("disableAltTextReminder", disableAltTextReminder)
|
||||
.putBoolean("showAltIndicator", showAltIndicator)
|
||||
.putBoolean("showNoAltIndicator", showNoAltIndicator)
|
||||
.putBoolean("enablePreReleases", enablePreReleases)
|
||||
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
|
||||
.putString("prefixReplies", prefixReplies.name())
|
||||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||
.putBoolean("spectatorMode", spectatorMode)
|
||||
.putBoolean("autoHideFab", autoHideFab)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putBoolean("bottomEncoding", bottomEncoding)
|
||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putString("color", color.name())
|
||||
.putString("recentLanguages", gson.toJson(recentLanguages))
|
||||
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
|
||||
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
|
||||
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
|
||||
.putString("replyVisibility", replyVisibility)
|
||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||
.putBoolean("forwardReportDefault", forwardReportDefault)
|
||||
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
|
||||
.putBoolean("showNavigationLabels", showNavigationLabels)
|
||||
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
|
||||
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
|
||||
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
|
||||
.putBoolean("overlayMedia", overlayMedia)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static void migrateToUpstreamVersion61(){
|
||||
Log.d(TAG, "Migrating preferences to upstream version 61!!");
|
||||
|
||||
Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
||||
Type pinnedTimelinesType = new TypeToken<Map<String, ArrayList<TimelineDefinition>>>() {}.getType();
|
||||
Type recentLanguagesType = new TypeToken<Map<String, ArrayList<String>>>() {}.getType();
|
||||
|
||||
// migrate global preferences
|
||||
SharedPreferences prefs=getPrefs();
|
||||
altTextReminders=!prefs.getBoolean("disableAltTextReminder", false);
|
||||
confirmBoost=prefs.getBoolean("confirmBeforeReblog", false);
|
||||
toolbarMarquee=!prefs.getBoolean("disableMarquee", false);
|
||||
|
||||
save();
|
||||
|
||||
// migrate local preferences
|
||||
AccountSessionManager asm=AccountSessionManager.getInstance();
|
||||
// reset: Set<String> accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||
Map<String, ContentType> accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||
Map<String, ArrayList<TimelineDefinition>> pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
|
||||
Set<String> accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
Set<String> accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
Map<String, ArrayList<String>> recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
|
||||
|
||||
for(AccountSession session : asm.getLoggedInAccounts()){
|
||||
String accountID=session.getID();
|
||||
AccountLocalPreferences localPrefs=session.getLocalPreferences();
|
||||
localPrefs.revealCWs=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
localPrefs.recentLanguages=recentLanguages.get(accountID);
|
||||
// reset: localPrefs.contentTypesEnabled=accountsWithContentTypesEnabled.contains(accountID);
|
||||
localPrefs.defaultContentType=accountsDefaultContentTypes.getOrDefault(accountID, ContentType.PLAIN);
|
||||
localPrefs.showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
localPrefs.timelines=pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
|
||||
localPrefs.localOnlySupported=accountsWithLocalOnlySupport.contains(accountID);
|
||||
localPrefs.glitchInstance=accountsInGlitchMode.contains(accountID);
|
||||
localPrefs.publishButtonText=prefs.getString("publishButtonText", null);
|
||||
localPrefs.keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
localPrefs.showReplies=prefs.getBoolean("showReplies", true);
|
||||
localPrefs.showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
|
||||
if(session.getInstance().map(Instance::isAkkoma).orElse(false)){
|
||||
localPrefs.timelineReplyVisibility=prefs.getString("replyVisibility", null);
|
||||
}
|
||||
|
||||
localPrefs.save();
|
||||
}
|
||||
|
||||
prefs.edit().putInt("migrationLevel", 61).apply();
|
||||
}
|
||||
|
||||
public enum ColorPreference{
|
||||
MATERIAL3,
|
||||
PINK,
|
||||
@@ -169,7 +238,20 @@ public class GlobalUserPreferences{
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
YELLOW
|
||||
YELLOW;
|
||||
|
||||
public @StringRes int getName() {
|
||||
return switch(this){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
@@ -177,5 +259,16 @@ public class GlobalUserPreferences{
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
}
|
||||
|
||||
public enum AutoRevealMode {
|
||||
NEVER,
|
||||
THREADS,
|
||||
DISCUSSIONS
|
||||
}
|
||||
|
||||
public enum PrefixRepliesMode {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
TO_OTHERS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ package org.joinmastodon.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
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.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
@@ -18,14 +24,19 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
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;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity{
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
@@ -35,10 +46,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){
|
||||
@@ -52,6 +71,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);
|
||||
@@ -60,6 +80,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
} else if (intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
} else {
|
||||
showFragmentClearingBackStack(fragment);
|
||||
maybeRequestNotificationsPermission();
|
||||
@@ -75,11 +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);
|
||||
AccountSessionManager.getInstance().getAccount(accountID);
|
||||
}catch(IllegalStateException x){
|
||||
return;
|
||||
}
|
||||
@@ -97,29 +120,76 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||
}*/
|
||||
}
|
||||
|
||||
public void handleURL(Uri uri, String accountID){
|
||||
if(uri==null)
|
||||
return;
|
||||
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
|
||||
return;
|
||||
AccountSession session;
|
||||
if(accountID==null)
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
else
|
||||
session=AccountSessionManager.get(accountID);
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
||||
}
|
||||
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
||||
new GetSearchResults(q, null, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if(result.statuses!=null && !result.statuses.isEmpty()){
|
||||
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
|
||||
Nav.go(MainActivity.this, ThreadFragment.class, args);
|
||||
}else if(result.accounts!=null && !result.accounts.isEmpty()){
|
||||
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
|
||||
Nav.go(MainActivity.this, ProfileFragment.class, args);
|
||||
}else{
|
||||
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(MainActivity.this);
|
||||
}
|
||||
})
|
||||
.wrapProgress(this, progressText, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean("noTransition", true);
|
||||
UiUtils.showFragmentForNotification(this, notification, accountID, args);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -153,18 +223,40 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
|
||||
@@ -28,5 +29,8 @@ public class MastodonApp extends Application{
|
||||
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
GlobalUserPreferences.load();
|
||||
if(BuildConfig.DEBUG){
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationChannelGroup;
|
||||
@@ -12,6 +14,7 @@ import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -23,18 +26,22 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
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;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -53,6 +60,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
|
||||
private static final int SUMMARY_ID = 791;
|
||||
private static int notificationId = 0;
|
||||
private static final Map<String, Integer> notificationIdsForAccounts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
@@ -84,9 +92,12 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
|
||||
return;
|
||||
}
|
||||
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
|
||||
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
|
||||
return;
|
||||
}
|
||||
String accountID=account.getID();
|
||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||
E.post(new NotificationReceivedEvent(accountID, pn.notificationId+""));
|
||||
new GetNotificationByID(pn.notificationId+"")
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -139,9 +150,15 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyUnifiedPush(Context context, String accountID, org.joinmastodon.android.model.Notification notification) {
|
||||
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
|
||||
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
|
||||
}
|
||||
|
||||
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
Account self=session.self;
|
||||
String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
Notification.Builder builder;
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
@@ -211,7 +228,21 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
builder.setSubText(accountName);
|
||||
}
|
||||
|
||||
int id = GlobalUserPreferences.keepOnlyLatestNotification ? NOTIFICATION_ID : notificationId++;
|
||||
int id;
|
||||
if(session.getLocalPreferences().keepOnlyLatestNotification){
|
||||
if(notificationIdsForAccounts.containsKey(accountID)){
|
||||
// we overwrite the existing notification
|
||||
id=notificationIdsForAccounts.get(accountID);
|
||||
}else{
|
||||
// there's no existing notification, so we increment
|
||||
id=notificationId++;
|
||||
// and store the notification id for this account
|
||||
notificationIdsForAccounts.put(accountID, id);
|
||||
}
|
||||
}else{
|
||||
// we don't want to overwrite anything, therefore incrementing
|
||||
id=notificationId++;
|
||||
}
|
||||
|
||||
if (notification != null){
|
||||
switch (pn.notificationType){
|
||||
@@ -273,16 +304,35 @@ 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();
|
||||
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: ")){
|
||||
|
||||
if (!notification.status.spoilerText.isEmpty() &&
|
||||
(GlobalUserPreferences.prefixReplies == ALWAYS
|
||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
|
||||
&& !notification.status.spoilerText.startsWith("re: ")) {
|
||||
req.spoilerText = "re: " + notification.status.spoilerText;
|
||||
}
|
||||
|
||||
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<Status>() {
|
||||
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Status status) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.unifiedpush.android.connector.MessagingReceiver;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
||||
private static final String TAG="UnifiedPushNotificationReceiver";
|
||||
|
||||
public UnifiedPushNotificationReceiver() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) {
|
||||
// Called when a new endpoint be used for sending push messages
|
||||
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance);
|
||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (account != null)
|
||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(@NotNull Context context, @NotNull String instance) {
|
||||
// called when the registration is not possible, eg. no network
|
||||
Log.d(TAG, "onRegistrationFailed: " + instance);
|
||||
//re-register for gcm
|
||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (account != null)
|
||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregistered(@NotNull Context context, @NotNull String instance) {
|
||||
// called when this application is unregistered from receiving push messages
|
||||
Log.d(TAG, "onUnregistered: " + instance);
|
||||
//re-register for gcm
|
||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (account != null)
|
||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) {
|
||||
// Called when a new message is received. The message contains the full POST body of the push message
|
||||
AccountSession account = AccountSessionManager.getInstance().getAccount(instance);
|
||||
|
||||
//this is stupid
|
||||
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
|
||||
// thus it is not possible to decrypt them. SO we need to re-request them from the server and transform them later on
|
||||
// The official uses fcm and moves the headers to extra data, see
|
||||
// https://github.com/mastodon/webpush-fcm-relay/blob/cac95b28d5364b0204f629283141ac3fb749e0c5/webpush-fcm-relay.go#L116
|
||||
// https://github.com/tuskyapp/Tusky/pull/2303#issue-1112080540
|
||||
account.getCacheController().getNotifications(null, 1, false, false, true, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
result.items
|
||||
.stream()
|
||||
.findFirst()
|
||||
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
//professional error handling
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,23 +13,20 @@ import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -37,13 +34,15 @@ 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());
|
||||
|
||||
private final String accountID;
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@@ -59,28 +58,23 @@ public class CacheController{
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
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();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
@@ -88,11 +82,13 @@ public class CacheController{
|
||||
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
|
||||
}
|
||||
}
|
||||
new GetHomeTimeline(maxID, null, count, null)
|
||||
new GetHomeTimeline(maxID, null, count, null, AccountSessionManager.get(accountID).getLocalPreferences().timelineReplyVisibility)
|
||||
.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));
|
||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -115,7 +111,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,67 +119,103 @@ 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<CacheablePaginatedResponse<List<Notification>>> callback){
|
||||
public void updateStatus(Status status) {
|
||||
runOnDbThread((db)->{
|
||||
ContentValues statusUpdate=new ContentValues(1);
|
||||
statusUpdate.put("json", MastodonAPIController.gson.toJson(status));
|
||||
db.update("home_timeline", statusUpdate, "id = ?", new String[] { status.id });
|
||||
});
|
||||
}
|
||||
|
||||
public void updateNotification(Notification notification) {
|
||||
runOnDbThread((db)->{
|
||||
ContentValues notificationUpdate=new ContentValues(1);
|
||||
notificationUpdate.put("json", MastodonAPIController.gson.toJson(notification));
|
||||
String[] notificationArgs = new String[] { notification.id };
|
||||
db.update("notifications_all", notificationUpdate, "id = ?", notificationArgs);
|
||||
db.update("notifications_mentions", notificationUpdate, "id = ?", notificationArgs);
|
||||
db.update("notifications_posts", notificationUpdate, "id = ?", notificationArgs);
|
||||
|
||||
ContentValues statusUpdate=new ContentValues(1);
|
||||
statusUpdate.put("json", MastodonAPIController.gson.toJson(notification.status));
|
||||
db.update("home_timeline", statusUpdate, "id = ?", new String[] { notification.status.id });
|
||||
});
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
List<Filter> filters=accountSession.wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
if(!onlyMentions && !onlyPosts && loadingNotifications){
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
pendingNotificationsCallbacks.add(callback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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();
|
||||
String newMaxID;
|
||||
outer:
|
||||
do{
|
||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||
return;
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||
}
|
||||
}
|
||||
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)
|
||||
if(!onlyMentions && !onlyPosts)
|
||||
loadingNotifications=true;
|
||||
boolean isAkkoma = AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), 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 true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
callback.onSuccess(res);
|
||||
putNotifications(result, onlyMentions, onlyPosts, maxID==null);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onSuccess(res);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
callback.onError(error);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onError(error);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -201,7 +233,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 +241,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 +338,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 +363,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
|
||||
createPostsNotificationsTable(db);
|
||||
}
|
||||
if(oldVersion<4){
|
||||
addTimeColumns(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||
@@ -350,9 +390,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", "MegalodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
|
||||
String token=null;
|
||||
if(session!=null)
|
||||
@@ -113,6 +117,9 @@ public class MastodonAPIController{
|
||||
synchronized(req){
|
||||
req.okhttpCall=call;
|
||||
}
|
||||
if(req.timeout>0){
|
||||
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
||||
@@ -149,15 +156,24 @@ public class MastodonAPIController{
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(respJson, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}else{
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(reader, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}
|
||||
}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,12 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
TypeToken<T> respTypeToken;
|
||||
Call okhttpCall;
|
||||
Token token;
|
||||
boolean canceled;
|
||||
boolean canceled, isRemote;
|
||||
Map<String, String> headers;
|
||||
long timeout;
|
||||
private ProgressDialog progressDialog;
|
||||
protected boolean removeUnsupportedItems;
|
||||
@Nullable Context context;
|
||||
|
||||
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
||||
this.path=path;
|
||||
@@ -101,13 +106,28 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||
return wrapProgress(activity, message, cancelable, null);
|
||||
public MastodonAPIRequest<T> execRemote(String domain) {
|
||||
return execRemote(domain, null);
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
|
||||
progressDialog=new ProgressDialog(activity);
|
||||
progressDialog.setMessage(activity.getString(message));
|
||||
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)
|
||||
.orElseGet(() -> this.execNoAuth(domain));
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Context context, @StringRes int message, boolean cancelable){
|
||||
return wrapProgress(context, message, cancelable, null);
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Context context, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
|
||||
progressDialog=new ProgressDialog(context);
|
||||
progressDialog.setMessage(context.getString(message));
|
||||
progressDialog.setCancelable(cancelable);
|
||||
if (transform != null) transform.accept(progressDialog);
|
||||
if(cancelable){
|
||||
@@ -133,6 +153,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
protected void setTimeout(long timeout){
|
||||
this.timeout=timeout;
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v1";
|
||||
}
|
||||
@@ -164,9 +188,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 +210,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 +218,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MastodonErrorResponse extends ErrorResponse{
|
||||
@@ -22,7 +20,7 @@ public class MastodonErrorResponse extends ErrorResponse{
|
||||
|
||||
@Override
|
||||
public void bindErrorView(View view){
|
||||
TextView text=view.findViewById(R.id.error_text);
|
||||
TextView text=view.findViewById(me.grishka.appkit.R.id.error_text);
|
||||
text.setText(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
|
||||
private String accountID;
|
||||
private PrivateKey privateKey;
|
||||
private PublicKey publicKey;
|
||||
private PublicKey serverKey;
|
||||
private byte[] authKey;
|
||||
|
||||
public PushSubscriptionManager(String accountID){
|
||||
@@ -121,9 +120,22 @@ public class PushSubscriptionManager{
|
||||
return !TextUtils.isEmpty(deviceToken);
|
||||
}
|
||||
|
||||
|
||||
public void registerAccountForPush(PushSubscription subscription){
|
||||
// this function is used for registering push notifications using FCM
|
||||
// to avoid NonFreeNet in F-Droid, this registration is disabled in it
|
||||
// see https://github.com/LucasGGamerM/moshidon/issues/206 for more context
|
||||
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease"))
|
||||
return;
|
||||
|
||||
if(TextUtils.isEmpty(deviceToken))
|
||||
throw new IllegalStateException("No device push token available");
|
||||
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
||||
registerAccountForPush(subscription, endpoint);
|
||||
}
|
||||
|
||||
public void registerAccountForPush(PushSubscription subscription, String endpoint){
|
||||
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Log.d(TAG, "registerAccountForPush: started for "+accountID);
|
||||
String encodedPublicKey, encodedAuthKey, pushAccountID;
|
||||
@@ -152,20 +164,15 @@ public class PushSubscriptionManager{
|
||||
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
|
||||
return;
|
||||
}
|
||||
new RegisterForPushNotifications(deviceToken,
|
||||
new RegisterForPushNotifications(endpoint,
|
||||
encodedPublicKey,
|
||||
encodedAuthKey,
|
||||
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
|
||||
subscription==null ? PushSubscription.Policy.ALL : subscription.policy,
|
||||
pushAccountID)
|
||||
subscription==null ? PushSubscription.Policy.ALL : subscription.policy)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PushSubscription result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
result.serverKey=result.serverKey.replace('/','_');
|
||||
result.serverKey=result.serverKey.replace('+','-');
|
||||
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
|
||||
|
||||
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
|
||||
if(session==null)
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
|
||||
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
|
||||
super(method, path, (Class<Void>)null);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
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,14 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
|
||||
public GetAccountFeaturedHashtags(String id){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -21,22 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
switch(filter){
|
||||
case DEFAULT -> addQueryParameter("exclude_replies", "true");
|
||||
case INCLUDE_REPLIES -> {}
|
||||
case PINNED -> addQueryParameter("pinned", "true");
|
||||
case MEDIA -> addQueryParameter("only_media", "true");
|
||||
case NO_REBLOGS -> {
|
||||
addQueryParameter("exclude_replies", "true");
|
||||
addQueryParameter("exclude_reblogs", "true");
|
||||
}
|
||||
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
||||
case PINNED -> addQueryParameter("pinned", "true");
|
||||
}
|
||||
}
|
||||
|
||||
public enum Filter{
|
||||
DEFAULT,
|
||||
INCLUDE_REPLIES,
|
||||
PINNED,
|
||||
MEDIA,
|
||||
NO_REBLOGS,
|
||||
OWN_POSTS_AND_REPLIES
|
||||
OWN_POSTS_AND_REPLIES,
|
||||
PINNED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason){
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
|
||||
super(HttpMethod.POST, "/accounts", Token.class);
|
||||
setRequestBody(new Body(username, email, password, locale, reason));
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone));
|
||||
}
|
||||
|
||||
private static class Body{
|
||||
public String username, email, password, locale, reason;
|
||||
public String username, email, password, locale, reason, timeZone;
|
||||
public boolean agreement=true;
|
||||
|
||||
public Body(String username, String email, String password, String locale, String reason){
|
||||
public Body(String username, String email, String password, String locale, String reason, String timeZone){
|
||||
this.username=username;
|
||||
this.email=email;
|
||||
this.password=password;
|
||||
this.locale=locale;
|
||||
this.reason=reason;
|
||||
this.timeZone=timeZone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,15 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
|
||||
public SetAccountMuted(String id, boolean muted){
|
||||
public SetAccountMuted(String id, boolean muted, long duration){
|
||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
|
||||
setRequestBody(new Object());
|
||||
setRequestBody(new Request(duration));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public long duration;
|
||||
public Request(long duration){
|
||||
this.duration=duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Boolean locked, discoverable;
|
||||
public RequestSource source;
|
||||
|
||||
public Request(Boolean locked, Boolean discoverable, RequestSource source){
|
||||
this.locked=locked;
|
||||
this.discoverable=discoverable;
|
||||
this.source=source;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestSource{
|
||||
public StatusPrivacy privacy;
|
||||
public String language;
|
||||
|
||||
public RequestSource(StatusPrivacy privacy, String language){
|
||||
this.privacy=privacy;
|
||||
this.language=language;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.announcements;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class AddAnnouncementReaction extends MastodonAPIRequest<Object> {
|
||||
public AddAnnouncementReaction(String id, String emoji) {
|
||||
super(HttpMethod.PUT, "/announcements/" + id + "/reactions/" + emoji, Object.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.api.requests.announcements;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
public class DeleteAnnouncementReaction extends MastodonAPIRequest<Object> {
|
||||
public DeleteAnnouncementReaction(String id, String emoji) {
|
||||
super(HttpMethod.DELETE, "/announcements/" + id + "/reactions/" + emoji, Object.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
|
||||
public GetCatalogDefaultInstances(){
|
||||
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||
setTimeout(500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
return Uri.parse("https://api.joinmastodon.org/default-servers");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CreateFilter extends MastodonAPIRequest<Filter>{
|
||||
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
|
||||
super(HttpMethod.POST, "/filters", Filter.class);
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteFilter extends ResultlessMastodonAPIRequest{
|
||||
public DeleteFilter(String id){
|
||||
super(HttpMethod.DELETE, "/filters/"+id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
class FilterRequest{
|
||||
public String title;
|
||||
public EnumSet<FilterContext> context;
|
||||
public FilterAction filterAction;
|
||||
public Integer expiresIn;
|
||||
public List<KeywordAttribute> keywordsAttributes;
|
||||
|
||||
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
|
||||
this.title=title;
|
||||
this.context=context;
|
||||
this.filterAction=filterAction;
|
||||
this.expiresIn=expiresIn;
|
||||
this.keywordsAttributes=keywordsAttributes;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetWordFilters(){
|
||||
public class GetFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
|
||||
public GetLegacyFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
public Boolean delete;
|
||||
public String keyword;
|
||||
public Boolean wholeWord;
|
||||
|
||||
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
|
||||
this.id=id;
|
||||
this.delete=delete;
|
||||
this.keyword=keyword;
|
||||
this.wholeWord=wholeWord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class UpdateFilter extends MastodonAPIRequest<Filter>{
|
||||
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
|
||||
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
|
||||
|
||||
List<KeywordAttribute> attrs=Stream.of(
|
||||
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
|
||||
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
|
||||
).flatMap(Function.identity()).collect(Collectors.toList());
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
|
||||
public GetInstanceExtendedDescription(){
|
||||
super(HttpMethod.GET, "/instance/extended_description", Response.class);
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Instant updatedAt;
|
||||
public String content;
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class CreateList extends MastodonAPIRequest<ListTimeline> {
|
||||
public CreateList(String title, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
||||
Request req = new Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
public String title;
|
||||
public boolean exclusive;
|
||||
public ListTimeline.RepliesPolicy repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
|
||||
public UpdateList(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
||||
CreateList.Request req = new CreateList.Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class GetMarkers extends MastodonAPIRequest<Markers> {
|
||||
public GetMarkers(EnumSet<Marker.Type> timelines) {
|
||||
super(HttpMethod.GET, "/markers", Markers.class);
|
||||
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
|
||||
addQueryParameter("timeline[]", type);
|
||||
}
|
||||
public class GetMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||
public GetMarkers(){
|
||||
super(HttpMethod.GET, "/markers", TimelineMarkers.class);
|
||||
addQueryParameter("timeline[]", "home");
|
||||
addQueryParameter("timeline[]", "notifications");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
||||
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
||||
super(HttpMethod.POST, "/markers", Response.class);
|
||||
super(HttpMethod.POST, "/markers", TimelineMarkers.class);
|
||||
JsonObjectBuilder builder=new JsonObjectBuilder();
|
||||
if(lastSeenHomePostID!=null)
|
||||
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
||||
@@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Marker home, notifications;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
||||
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
|
||||
public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy, String accountID){
|
||||
public RegisterForPushNotifications(String endpoint, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
|
||||
Request r=new Request();
|
||||
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
||||
r.subscription.endpoint=endpoint;
|
||||
r.data.alerts=alerts;
|
||||
r.policy=policy;
|
||||
r.subscription.keys.p256dh=encryptionKey;
|
||||
|
||||
@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
|
||||
addQueryParameter("resolve", "true");
|
||||
}
|
||||
|
||||
public GetSearchResults limit(int limit){
|
||||
addQueryParameter("limit", String.valueOf(limit));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class AddStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public AddStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.POST, "/statuses/" + id + "/react/" + emoji, Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class DeleteStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public DeleteStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.POST, "/statuses/" + id + "/unreact/" + emoji, Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -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,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class PleromaAddStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public PleromaAddStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.PUT, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class PleromaDeleteStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public PleromaDeleteStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.DELETE, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PleromaGetStatusReactions extends MastodonAPIRequest<List<EmojiReaction>> {
|
||||
public PleromaGetStatusReactions(String id, String emoji) {
|
||||
super(HttpMethod.GET, "/pleroma/statuses/" + id + "/reactions/" + (emoji != null ? emoji : ""), new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
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, String replyVisibility) {
|
||||
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,26 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit){
|
||||
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit, List<String> containsAny, List<String> containsAll, List<String> containsNone, boolean localOnly, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){});
|
||||
if (localOnly)
|
||||
addQueryParameter("local", "true");
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(containsAny!=null)
|
||||
for (String tag : containsAny)
|
||||
addQueryParameter("any[]", tag);
|
||||
if(containsAll!=null)
|
||||
for (String tag : containsAll)
|
||||
addQueryParameter("all[]", tag);
|
||||
if(containsNone!=null)
|
||||
for (String tag : containsNone)
|
||||
addQueryParameter("none[]", tag);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ 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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID){
|
||||
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
@@ -19,7 +18,7 @@ public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) {
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID, String replyVisibility) {
|
||||
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
@@ -18,5 +18,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
@@ -20,5 +20,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.fromJson;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.enumValue;
|
||||
import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AccountLocalPreferences{
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public boolean showInteractionCounts;
|
||||
public boolean customEmojiInNames;
|
||||
public boolean revealCWs;
|
||||
public boolean hideSensitiveMedia;
|
||||
public boolean serverSideFiltersSupported;
|
||||
|
||||
// MEGALODON
|
||||
public boolean showReplies;
|
||||
public boolean showBoosts;
|
||||
public ArrayList<String> recentLanguages;
|
||||
public boolean bottomEncoding;
|
||||
public ContentType defaultContentType;
|
||||
public boolean contentTypesEnabled;
|
||||
public ArrayList<TimelineDefinition> timelines;
|
||||
public boolean localOnlySupported;
|
||||
public boolean glitchInstance;
|
||||
public String publishButtonText;
|
||||
public String timelineReplyVisibility; // akkoma-only
|
||||
public boolean keepOnlyLatestNotification;
|
||||
|
||||
public boolean emojiReactionsEnabled;
|
||||
public boolean showEmojiReactionsInLists;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
|
||||
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
|
||||
this.prefs=prefs;
|
||||
showInteractionCounts=prefs.getBoolean("interactionCounts", false);
|
||||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
revealCWs=prefs.getBoolean("revealCWs", false);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
|
||||
// MEGALODON
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new ArrayList<>());
|
||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", ContentType.PLAIN.name()));
|
||||
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", true);
|
||||
timelines=fromJson(prefs.getString("timelines", null), timelinesType, TimelineDefinition.getDefaultTimelines(session.getID()));
|
||||
localOnlySupported=prefs.getBoolean("localOnlySupported", false);
|
||||
glitchInstance=prefs.getBoolean("glitchInstance", false);
|
||||
publishButtonText=prefs.getString("publishButtonText", null);
|
||||
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
|
||||
showEmojiReactionsInLists=prefs.getBoolean("showEmojiReactionsInLists", false);
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
return prefs.getLong("notificationsPauseTime", 0L);
|
||||
}
|
||||
|
||||
public void setNotificationsPauseEndTime(long time){
|
||||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||
}
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("revealCWs", revealCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
|
||||
// MEGALODON
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putString("recentLanguages", gson.toJson(recentLanguages))
|
||||
.putBoolean("bottomEncoding", bottomEncoding)
|
||||
.putString("defaultContentType", defaultContentType==null ? null : defaultContentType.name())
|
||||
.putBoolean("contentTypesEnabled", contentTypesEnabled)
|
||||
.putString("timelines", gson.toJson(timelines))
|
||||
.putBoolean("localOnlySupported", localOnlySupported)
|
||||
.putBoolean("glitchInstance", glitchInstance)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putString("timelineReplyVisibility", timelineReplyVisibility)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
|
||||
.putBoolean("showEmojiReactionsInLists", showEmojiReactionsInLists)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,53 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class AccountSession{
|
||||
private static final String TAG="AccountSession";
|
||||
|
||||
public Token token;
|
||||
public Account self;
|
||||
public String domain;
|
||||
@@ -28,15 +60,17 @@ public class AccountSession{
|
||||
public PushSubscription pushSubscription;
|
||||
public boolean needUpdatePushSettings;
|
||||
public long filtersLastUpdated;
|
||||
public List<Filter> wordFilters=new ArrayList<>();
|
||||
public List<LegacyFilter> wordFilters=new ArrayList<>();
|
||||
public String pushAccountID;
|
||||
public Preferences preferences;
|
||||
public AccountActivationInfo activationInfo;
|
||||
public Markers markers;
|
||||
public Preferences preferences;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
|
||||
private transient CacheController cacheController;
|
||||
private transient PushSubscriptionManager pushSubscriptionManager;
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -54,10 +88,6 @@ public class AccountSession{
|
||||
return domain+"_"+self.id;
|
||||
}
|
||||
|
||||
public String getFullUsername() {
|
||||
return "@"+self.username+"@"+domain;
|
||||
}
|
||||
|
||||
public MastodonAPIController getApiController(){
|
||||
if(apiController==null)
|
||||
apiController=new MastodonAPIController(this);
|
||||
@@ -87,4 +117,197 @@ public class AccountSession{
|
||||
pushSubscriptionManager=new PushSubscriptionManager(getID());
|
||||
return pushSubscriptionManager;
|
||||
}
|
||||
|
||||
public String getFullUsername(){
|
||||
return '@'+self.username+'@'+domain;
|
||||
}
|
||||
|
||||
public void preferencesFromAccountSource(Account account) {
|
||||
if (account != null && account.source != null && preferences != null) {
|
||||
if (account.source.privacy != null)
|
||||
preferences.postingDefaultVisibility = account.source.privacy;
|
||||
if (account.source.language != null)
|
||||
preferences.postingDefaultLanguage = account.source.language;
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadPreferences(Consumer<Preferences> callback){
|
||||
new GetPreferences()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Preferences result){
|
||||
preferences=result;
|
||||
preferencesFromAccountSource(self);
|
||||
if(callback!=null)
|
||||
callback.accept(result);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public SharedPreferences getRawLocalPreferences(){
|
||||
if(prefs==null)
|
||||
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public void reloadNotificationsMarker(Consumer<String> callback){
|
||||
new GetMarkers()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(TimelineMarkers result){
|
||||
if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){
|
||||
String id=result.notifications.lastReadId;
|
||||
String lastKnown=getLastKnownNotificationsMarker();
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){
|
||||
// Marker moved back -- previous marker update must have failed.
|
||||
// Pretend it didn't happen and repeat the request.
|
||||
id=lastKnown;
|
||||
new SaveMarkers(null, id).exec(getID());
|
||||
}
|
||||
callback.accept(id);
|
||||
setNotificationsMarker(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public String getLastKnownNotificationsMarker(){
|
||||
return getRawLocalPreferences().getString("notificationsMarker", null);
|
||||
}
|
||||
|
||||
public void setNotificationsMarker(String id, boolean clearUnread){
|
||||
getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
|
||||
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
|
||||
}
|
||||
|
||||
public void logOut(Activity activity, Runnable onDone){
|
||||
new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public void savePreferencesLater(){
|
||||
preferencesNeedSaving=true;
|
||||
}
|
||||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
preferencesNeedSaving=false;
|
||||
self=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.e(TAG, "failed to save preferences: "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
}
|
||||
|
||||
public AccountLocalPreferences getLocalPreferences(){
|
||||
if(localPreferences==null)
|
||||
localPreferences=new AccountLocalPreferences(getRawLocalPreferences(), this);
|
||||
return localPreferences;
|
||||
}
|
||||
|
||||
public void filterStatuses(List<Status> statuses, FilterContext context){
|
||||
filterStatuses(statuses, context, null);
|
||||
}
|
||||
|
||||
public void filterStatuses(List<Status> statuses, FilterContext context, Account profile){
|
||||
filterStatusContainingObjects(statuses, Function.identity(), context, profile);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
|
||||
filterStatusContainingObjects(objects, extractor, context, null);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
|
||||
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
||||
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
if(s.filtered==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(FilterResult filter:s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(wordFilters==null)
|
||||
return;
|
||||
for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
getLocalPreferences().serverSideFiltersSupported=true;
|
||||
getLocalPreferences().save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(LegacyFilter filter:wordFilters){
|
||||
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
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,28 +15,24 @@ 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;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
import java.io.File;
|
||||
@@ -49,10 +45,10 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -110,6 +106,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);
|
||||
@@ -118,14 +120,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);
|
||||
@@ -146,11 +150,24 @@ public class AccountSessionManager{
|
||||
return session;
|
||||
}
|
||||
|
||||
public static AccountSession get(String id){
|
||||
return getInstance().getAccount(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(String id){
|
||||
return sessions.get(id);
|
||||
}
|
||||
|
||||
public static Optional<AccountSession> getOptional(String id) {
|
||||
return Optional.ofNullable(getInstance().tryGetAccount(id));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(Account account) {
|
||||
return sessions.get(account.getDomainFromURL() + "_" + account.id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession getLastActiveAccount(){
|
||||
if(sessions.isEmpty() || lastActiveAccountID==null)
|
||||
@@ -178,12 +195,19 @@ public class AccountSessionManager{
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
MastodonApp.context.deleteSharedPreferences(id);
|
||||
}else{
|
||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
||||
}
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
if(sessions.isEmpty())
|
||||
lastActiveAccountID=null;
|
||||
else
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
@@ -248,42 +272,37 @@ 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);
|
||||
// }
|
||||
updateSessionMarkers(session);
|
||||
if(session == activeSession || now-session.infoLastUpdated>24L*3600_000L){
|
||||
session.reloadPreferences(null);
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
if(session == activeSession || (session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L)){
|
||||
updateSessionWordFilters(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(domain.equals(activeDomain) || lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||
updateInstanceInfo(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void preferencesFromSource(AccountSession session, Account account) {
|
||||
if (account != null && account.source != null && session.preferences != null) {
|
||||
if (account.source.privacy != null)
|
||||
session.preferences.postingDefaultVisibility = account.source.privacy;
|
||||
if (account.source.language != null)
|
||||
session.preferences.postingDefaultLanguage = account.source.language;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSessionLocalInfo(AccountSession session){
|
||||
new GetOwnAccount()
|
||||
@@ -292,39 +311,7 @@ public class AccountSessionManager{
|
||||
public void onSuccess(Account result){
|
||||
session.self=result;
|
||||
session.infoLastUpdated=System.currentTimeMillis();
|
||||
preferencesFromSource(session, result);
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateSessionPreferences(AccountSession session){
|
||||
new GetPreferences().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Preferences preferences) {
|
||||
session.preferences=preferences;
|
||||
preferencesFromSource(session, session.self);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
session.preferences = new Preferences();
|
||||
preferencesFromSource(session, session.self);
|
||||
}
|
||||
}).exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateSessionWordFilters(AccountSession session){
|
||||
new GetWordFilters()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
session.preferencesFromAccountSource(result);
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@@ -336,19 +323,22 @@ public class AccountSessionManager{
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateSessionMarkers(AccountSession session) {
|
||||
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Markers markers) {
|
||||
session.markers = markers;
|
||||
writeAccountsFile();
|
||||
}
|
||||
private void updateSessionWordFilters(AccountSession session){
|
||||
new GetLegacyFilters()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<LegacyFilter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
}).exec(session.getID());
|
||||
}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
public void updateInstanceInfo(String domain){
|
||||
@@ -400,7 +390,9 @@ public class AccountSessionManager{
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
|
||||
wrapper.instance = instance;
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
@@ -411,10 +403,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);
|
||||
}
|
||||
@@ -434,7 +429,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
if(!loadedInstances){
|
||||
loadedInstances=true;
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
maybeUpdateCustomEmojis(domains, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,10 +453,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;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class AllNotificationsSeenEvent {
|
||||
}
|
||||
@@ -6,10 +6,12 @@ public class ListUpdatedCreatedEvent {
|
||||
public final String id;
|
||||
public final String title;
|
||||
public final ListTimeline.RepliesPolicy repliesPolicy;
|
||||
public final boolean exclusive;
|
||||
|
||||
public ListUpdatedCreatedEvent(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.exclusive = exclusive;
|
||||
this.repliesPolicy = repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationReceivedEvent {
|
||||
public String account, id;
|
||||
public NotificationReceivedEvent(String account, String id) {
|
||||
this.account = account;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationsMarkerUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final String marker;
|
||||
public final boolean clearUnread;
|
||||
|
||||
public NotificationsMarkerUpdatedEvent(String accountID, String marker, boolean clearUnread){
|
||||
this.accountID=accountID;
|
||||
this.marker=marker;
|
||||
this.clearUnread=clearUnread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
|
||||
public class SettingsFilterCreatedOrUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final Filter filter;
|
||||
|
||||
public SettingsFilterCreatedOrUpdatedEvent(String accountID, Filter filter){
|
||||
this.accountID=accountID;
|
||||
this.filter=filter;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user