Compare commits
465 Commits
v1.1.1-dev
...
v1.1.4+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764ab60fea | ||
|
|
9b13bdb06f | ||
|
|
9283ca8878 | ||
|
|
679ede1124 | ||
|
|
f4b1bde8c5 | ||
|
|
eff6cc3e17 | ||
|
|
03044b86b1 | ||
|
|
90ff88f02b | ||
|
|
e1f378977a | ||
|
|
a0f3d2862c | ||
|
|
0c64145368 | ||
|
|
a1474fb461 | ||
|
|
965ebc8669 | ||
|
|
3514152439 | ||
|
|
1b1dc7085e | ||
|
|
94d004724f | ||
|
|
e9f5d235cb | ||
|
|
9692afb323 | ||
|
|
3ae37a700b | ||
|
|
b166ca705e | ||
|
|
e6a67543f4 | ||
|
|
7d38f031f1 | ||
|
|
cf864e6f49 | ||
|
|
f4b1629a1d | ||
|
|
1fbb97021e | ||
|
|
229c815a59 | ||
|
|
ee8d78637d | ||
|
|
4ede842171 | ||
|
|
8306e78ce3 | ||
|
|
65d093ee9d | ||
|
|
aa48233a75 | ||
|
|
ae50e618c0 | ||
|
|
defa8b014e | ||
|
|
159d91f390 | ||
|
|
430d4ec93b | ||
|
|
ae64784daf | ||
|
|
18df7c32a4 | ||
|
|
4615612a65 | ||
|
|
6b226cdcad | ||
|
|
1776709b38 | ||
|
|
75a35cd680 | ||
|
|
18d4f2fa36 | ||
|
|
bca936f6a2 | ||
|
|
0b59379c4e | ||
|
|
58abdad62a | ||
|
|
c693414cc3 | ||
|
|
ee158e1aba | ||
|
|
40d478aaec | ||
|
|
e7fb96b3ff | ||
|
|
ef75427b45 | ||
|
|
2ff4f00774 | ||
|
|
534fd8c119 | ||
|
|
2d2cd89454 | ||
|
|
9f3f5ca7c1 | ||
|
|
c0e6f17c83 | ||
|
|
677f1cb42d | ||
|
|
1ac6a04a46 | ||
|
|
21927d2e25 | ||
|
|
c35441f5f7 | ||
|
|
3718fe6601 | ||
|
|
095f234bd5 | ||
|
|
f10c0e06db | ||
|
|
ebbd5d1fa3 | ||
|
|
c8abf26040 | ||
|
|
bc733af147 | ||
|
|
77a2fd2a60 | ||
|
|
5cfc5eb08a | ||
|
|
b832d2df26 | ||
|
|
db34dc40ba | ||
|
|
bfa48c2d3e | ||
|
|
b5e229a84d | ||
|
|
ef207f885b | ||
|
|
c4eee28335 | ||
|
|
6fe466779e | ||
|
|
e71db1b883 | ||
|
|
b6efafe99d | ||
|
|
688d466f8e | ||
|
|
3c5797932e | ||
|
|
48a5e262ce | ||
|
|
63b0365208 | ||
|
|
972c05d60b | ||
|
|
acb5778e0b | ||
|
|
7694c50358 | ||
|
|
7ffa368d10 | ||
|
|
53f8f41d88 | ||
|
|
93d57d847e | ||
|
|
83a4f5eec2 | ||
|
|
21a526dda9 | ||
|
|
836c2dba8d | ||
|
|
13ae8d2ebe | ||
|
|
98dafb4e49 | ||
|
|
85b8bae42e | ||
|
|
64416ef9ee | ||
|
|
6274ab3384 | ||
|
|
4ea5d94dc6 | ||
|
|
db4dd436b7 | ||
|
|
6906ac6c8f | ||
|
|
bc0b56a90e | ||
|
|
349d95b8ee | ||
|
|
8c8eb395de | ||
|
|
50c844aa25 | ||
|
|
85b232330b | ||
|
|
b4c305d094 | ||
|
|
be2ee3a029 | ||
|
|
596799bf2f | ||
|
|
10a405ef13 | ||
|
|
118c5d4b44 | ||
|
|
a4cb05080a | ||
|
|
699ececc42 | ||
|
|
4f8f698911 | ||
|
|
30083837a5 | ||
|
|
ea7aa6c52f | ||
|
|
244f2ed911 | ||
|
|
99a71c67e4 | ||
|
|
b3c0550a86 | ||
|
|
39a0c6d08a | ||
|
|
0ee494bcfc | ||
|
|
4233c743e2 | ||
|
|
eea00b0d53 | ||
|
|
7cef4d282c | ||
|
|
df6e085c97 | ||
|
|
70cf48355e | ||
|
|
da06c798d7 | ||
|
|
9055c094e8 | ||
|
|
5ab3ae3d59 | ||
|
|
92ebb2f339 | ||
|
|
d8c034dfa3 | ||
|
|
247516849e | ||
|
|
aad9996b6f | ||
|
|
76c1ad07a3 | ||
|
|
49599f48bb | ||
|
|
158d128316 | ||
|
|
00897e7388 | ||
|
|
1867f294c8 | ||
|
|
eaa189d6b0 | ||
|
|
87d11dcb0a | ||
|
|
ba99394890 | ||
|
|
6b8236021e | ||
|
|
68b12a579c | ||
|
|
3f0ae887b8 | ||
|
|
187f9a2246 | ||
|
|
e8566bc24b | ||
|
|
e375fc7d4d | ||
|
|
89157efce8 | ||
|
|
5e95291016 | ||
|
|
cbd6e668dc | ||
|
|
898e62490b | ||
|
|
326ab4edf1 | ||
|
|
f25a965478 | ||
|
|
f3f3bea7b3 | ||
|
|
583b0788c6 | ||
|
|
ca29cee586 | ||
|
|
ec905448b0 | ||
|
|
e8fa82d0de | ||
|
|
e381de812c | ||
|
|
1a9752d53b | ||
|
|
09cc5c5fd2 | ||
|
|
4e8d510d38 | ||
|
|
7f7f6bae80 | ||
|
|
358ff12fba | ||
|
|
8ff3ecb4d4 | ||
|
|
47df35f0fd | ||
|
|
e226851e03 | ||
|
|
7228907682 | ||
|
|
1dbabed716 | ||
|
|
4224cd037d | ||
|
|
9aa6d3f531 | ||
|
|
c9823ae9d0 | ||
|
|
1fa8a9e858 | ||
|
|
212e8893b9 | ||
|
|
367057421b | ||
|
|
c821480842 | ||
|
|
3ef7c11e3b | ||
|
|
1112756bc2 | ||
|
|
878ed43135 | ||
|
|
01b746d30a | ||
|
|
a900351729 | ||
|
|
4114a6e5d8 | ||
|
|
04dd232e38 | ||
|
|
bc26dfe856 | ||
|
|
0322f845af | ||
|
|
7daa2d63e6 | ||
|
|
3a8a41f568 | ||
|
|
65cd7d076b | ||
|
|
a1f71091fe | ||
|
|
131834af86 | ||
|
|
c0789cbdb9 | ||
|
|
7d12d866ab | ||
|
|
7c2589c35b | ||
|
|
fd2031ccf5 | ||
|
|
6a165ec9f4 | ||
|
|
fd8dad487a | ||
|
|
e9443b841a | ||
|
|
89ec6acac9 | ||
|
|
9b389b346f | ||
|
|
4a522bffc0 | ||
|
|
9b62f8b7a2 | ||
|
|
96342d67e3 | ||
|
|
95fc2bef9e | ||
|
|
4f36fbe3b9 | ||
|
|
a501d8b995 | ||
|
|
74691650b3 | ||
|
|
9325781cbc | ||
|
|
b383dd6419 | ||
|
|
fe98ecf0cc | ||
|
|
01970ab69b | ||
|
|
3aa252f681 | ||
|
|
18633291e6 | ||
|
|
b79619b6e4 | ||
|
|
dbb03ee688 | ||
|
|
00894b41d2 | ||
|
|
8868ace90d | ||
|
|
a7586eeba8 | ||
|
|
19b89c606a | ||
|
|
b6c1e7d11e | ||
|
|
ca90c89b2a | ||
|
|
170a758f5f | ||
|
|
43dcd6e7f4 | ||
|
|
063542b2f5 | ||
|
|
70e4ae4fb4 | ||
|
|
8f1a4c60df | ||
|
|
07212dba96 | ||
|
|
fb95caadbe | ||
|
|
51e86b686d | ||
|
|
6ec335087d | ||
|
|
96040e15fd | ||
|
|
12a5670441 | ||
|
|
b756fe2cdb | ||
|
|
3732a4c844 | ||
|
|
5bd59bd999 | ||
|
|
90ba5551d2 | ||
|
|
484b6891fd | ||
|
|
0435d5f0c7 | ||
|
|
864c8de8de | ||
|
|
8bcea5bfb8 | ||
|
|
beded04579 | ||
|
|
5dcd41170c | ||
|
|
8aeda56fc8 | ||
|
|
3bb921a859 | ||
|
|
38edbde645 | ||
|
|
f531a90b41 | ||
|
|
ff52c37868 | ||
|
|
8fb2b454dd | ||
|
|
265b2ad32c | ||
|
|
ba3219d9fc | ||
|
|
bcac7401ee | ||
|
|
b44e3424e3 | ||
|
|
c15f6519c0 | ||
|
|
2809b27be0 | ||
|
|
1b6e096bf9 | ||
|
|
85db37f6a7 | ||
|
|
0cee490466 | ||
|
|
a148c92da2 | ||
|
|
f073eba538 | ||
|
|
7f78431eff | ||
|
|
24c1ac042c | ||
|
|
105fe68438 | ||
|
|
46057af093 | ||
|
|
750fa4c112 | ||
|
|
6272797834 | ||
|
|
c048134ef2 | ||
|
|
6f57cd9ffe | ||
|
|
8b40643e63 | ||
|
|
76815f8194 | ||
|
|
5968dcd05b | ||
|
|
0c382bdbf6 | ||
|
|
74f03026cf | ||
|
|
8ad6bd52ef | ||
|
|
04da21edf3 | ||
|
|
ee709b6db7 | ||
|
|
b7432fe422 | ||
|
|
8a0991d533 | ||
|
|
6fffe778d3 | ||
|
|
b67b61dfe4 | ||
|
|
65d86d9238 | ||
|
|
c1e67c4f73 | ||
|
|
e0e48f87eb | ||
|
|
b2db64022f | ||
|
|
8336bfdf5c | ||
|
|
0ec14fe8fa | ||
|
|
01a2f1d95c | ||
|
|
b38bf5e431 | ||
|
|
67b3e85837 | ||
|
|
310fb7db42 | ||
|
|
9f4d330ab1 | ||
|
|
25092fbcfb | ||
|
|
705e98729d | ||
|
|
2f24977996 | ||
|
|
6c336ba89e | ||
|
|
108d16a157 | ||
|
|
e55ca6cc05 | ||
|
|
b8be1f184d | ||
|
|
aa96ec54a3 | ||
|
|
e8b43c7179 | ||
|
|
b51b4a10ee | ||
|
|
f2b9ede27c | ||
|
|
a8c7d891f1 | ||
|
|
195c4d7b6d | ||
|
|
d280dc31e8 | ||
|
|
eb0925c524 | ||
|
|
968de3664d | ||
|
|
12f7336392 | ||
|
|
3a4d13b1c6 | ||
|
|
273c841d9a | ||
|
|
d1b1cb2082 | ||
|
|
5bbe99be51 | ||
|
|
556d1e7433 | ||
|
|
293d7032ce | ||
|
|
0186b7f8da | ||
|
|
d33654c793 | ||
|
|
86d2312615 | ||
|
|
d1083c331b | ||
|
|
ed7242217a | ||
|
|
8fddaa8c82 | ||
|
|
00affe6e3e | ||
|
|
f21b647ee0 | ||
|
|
2a628a3791 | ||
|
|
ecd568503d | ||
|
|
f9d0632a85 | ||
|
|
11905513b7 | ||
|
|
9c89abf1c4 | ||
|
|
4d950e43ac | ||
|
|
99405f307d | ||
|
|
f1bfe05263 | ||
|
|
0f223159c0 | ||
|
|
ad9518e87c | ||
|
|
1c16cfb09e | ||
|
|
d4a4b10017 | ||
|
|
74ae5bd04e | ||
|
|
9638cf079f | ||
|
|
a6d161c1b4 | ||
|
|
1136e40eb4 | ||
|
|
98de3a2984 | ||
|
|
48e7071450 | ||
|
|
bcc8d55c7b | ||
|
|
8d477efc28 | ||
|
|
4ae21862a5 | ||
|
|
6e6aebef35 | ||
|
|
cd138032da | ||
|
|
864e6fb9d0 | ||
|
|
9d356b0635 | ||
|
|
6c5d720a40 | ||
|
|
d733d76ccf | ||
|
|
3483d8c3c0 | ||
|
|
0f326c1362 | ||
|
|
c6eda38400 | ||
|
|
3c59c8cc0f | ||
|
|
8f1b9ec092 | ||
|
|
5a42136395 | ||
|
|
e9b347d130 | ||
|
|
d86e203127 | ||
|
|
c80417e671 | ||
|
|
55a55fbb1c | ||
|
|
b42236999b | ||
|
|
8ca8bd765b | ||
|
|
7f4cf77283 | ||
|
|
950d413bd1 | ||
|
|
475827b1c1 | ||
|
|
1c9164e559 | ||
|
|
7311a394d8 | ||
|
|
aa09bc7ab2 | ||
|
|
267a6a75ef | ||
|
|
37660b4c73 | ||
|
|
15b4d46ea1 | ||
|
|
0b99e76b25 | ||
|
|
c851f666b3 | ||
|
|
7662c81754 | ||
|
|
5c1b583448 | ||
|
|
4071c1552d | ||
|
|
89856a81a3 | ||
|
|
005ddfb651 | ||
|
|
92d10d59c6 | ||
|
|
150f70edd8 | ||
|
|
73019eaade | ||
|
|
46bac59ff5 | ||
|
|
9b162cb63b | ||
|
|
7d216314c9 | ||
|
|
080a320e12 | ||
|
|
28787b4068 | ||
|
|
a73ea62a9c | ||
|
|
69b399e397 | ||
|
|
fc2c033e93 | ||
|
|
1d81abca5b | ||
|
|
0f3cd5d8d0 | ||
|
|
f0476f3187 | ||
|
|
b4677d14e5 | ||
|
|
a8837bd4f8 | ||
|
|
c6991a7067 | ||
|
|
0723e942f0 | ||
|
|
b08415ca8f | ||
|
|
3639c69d36 | ||
|
|
52fd300d1e | ||
|
|
37cefcaf6d | ||
|
|
68c9f7a861 | ||
|
|
8eb0b12a09 | ||
|
|
558adc6936 | ||
|
|
68ecd7bc28 | ||
|
|
5c7d4e389f | ||
|
|
31e3a8592f | ||
|
|
39655d5278 | ||
|
|
55fd74c227 | ||
|
|
b65b7c53bc | ||
|
|
68d0862008 | ||
|
|
c9e13eefa5 | ||
|
|
349fbce5af | ||
|
|
7a23c9b348 | ||
|
|
95c66654aa | ||
|
|
a8407571a4 | ||
|
|
75538deb9b | ||
|
|
601eec4607 | ||
|
|
9b87d0bece | ||
|
|
cb25632691 | ||
|
|
63957250c5 | ||
|
|
d844a77e65 | ||
|
|
32cc760272 | ||
|
|
e105764aa8 | ||
|
|
5a9a352e56 | ||
|
|
7deb2d452e | ||
|
|
c3d2df88e8 | ||
|
|
9943d19c31 | ||
|
|
51c1e115c5 | ||
|
|
f83ff93c68 | ||
|
|
7deb5d44c2 | ||
|
|
3140ae8046 | ||
|
|
772f79219b | ||
|
|
8830d67af0 | ||
|
|
89c02be41c | ||
|
|
1d092c660b | ||
|
|
a34084da5a | ||
|
|
212f5a9beb | ||
|
|
f6333de4e6 | ||
|
|
5af22f1bab | ||
|
|
02d866d7d6 | ||
|
|
fa7aa6240b | ||
|
|
bde2e398a8 | ||
|
|
8d443b2051 | ||
|
|
33d4b678ed | ||
|
|
3becad1468 | ||
|
|
fad3ba3eae | ||
|
|
cb16f95878 | ||
|
|
4e833490ff | ||
|
|
04a973f7b0 | ||
|
|
0318169b74 | ||
|
|
972fb1e241 | ||
|
|
9beb04b01d | ||
|
|
a3bea6ad24 | ||
|
|
cb38e0d367 | ||
|
|
a133a1d01f | ||
|
|
79bfc43431 | ||
|
|
72f3a51af7 | ||
|
|
ee73b487ae | ||
|
|
e580d2e890 | ||
|
|
4f6f53061f | ||
|
|
be23ec4176 | ||
|
|
186636c2ef | ||
|
|
f0396ff418 | ||
|
|
67e793b56a | ||
|
|
d84f011d27 | ||
|
|
f685d9ccdd | ||
|
|
380c742f54 | ||
|
|
ed84ea6162 | ||
|
|
e99ffc0d4c | ||
|
|
ca015db188 | ||
|
|
af3a3761f2 | ||
|
|
95085b6306 |
54
README.md
54
README.md
@@ -1,17 +1,51 @@
|
||||
# Forked Mastodon for Android
|
||||
[](https://crowdin.com/project/mastodon-for-android)
|
||||

|
||||
|
||||
This is the repository for an officially forked Android app for Mastodon.
|
||||
# Mastodos
|
||||
|
||||
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
|
||||
> 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, bookmarks and an image description viewer.
|
||||
|
||||
[](https://github.com/sk22/mastodon-android-fork/releases/latest/download/mastodos.apk)
|
||||
|
||||
## Changes
|
||||
|
||||
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
|
||||
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
|
||||
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline)
|
||||
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/tree/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
|
||||
### Features
|
||||
|
||||
* [Add “Unlisted” as a post visibility option](https://github.com/sk22/mastodon-android-fork/commits/feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
|
||||
* [Add “Federation” tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/commits/feature/add-federated-timeline) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/8))
|
||||
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/commits/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
|
||||
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodon-android-fork/commits/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
|
||||
* [Implement deleting and re-drafting](https://github.com/sk22/mastodon-android-fork/commits/feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
|
||||
* [Implement a bookmark button and list](https://github.com/sk22/mastodon-android-fork/commits/feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||
* [Add “Check for update” button in addition to integrated update checker](https://github.com/sk22/mastodon-android-fork/commits/feature/check-for-update-button)
|
||||
* [Add “Mark media as sensitive” option](https://github.com/sk22/mastodon-android-fork/commits/feature/mark-media-as-sensitive)
|
||||
* [Add settings to hide replies and reposts from the timeline](https://github.com/sk22/mastodon-android-fork/commits/feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
|
||||
* [Follow and unfollow hashtags](https://github.com/sk22/mastodon-android-fork/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
|
||||
* [Notification bell for posts](https://github.com/sk22/mastodon-android-fork/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
|
||||
* [Lists view (viewing only, for now)](https://github.com/sk22/mastodon-android-fork/commits/list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
|
||||
|
||||
### Behavior
|
||||
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/commits/feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/commits/feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/commits/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
|
||||
* [Set spoiler height independently to content height](https://github.com/sk22/mastodon-android-fork/commits/spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
|
||||
* [Custom extended footer redesign](https://github.com/sk22/mastodon-android-fork/commits/compact-extended-footer)
|
||||
|
||||
### Installation
|
||||
|
||||
To install this app on your Android device, download the [latest release from GitHub](https://github.com/sk22/mastodon-android-fork/releases/latest/download/mastodos.apk) and open it. You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/mastodon-android-fork/releases) page.
|
||||
|
||||
Mastodos makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)’s automatic update checker. Mastodos will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
|
||||
|
||||
### Branding
|
||||
|
||||
* App name “Mastodos”
|
||||
* Pink primary color
|
||||
* Custom icon: Modulate upstream icon using ImageMagick
|
||||
```bash
|
||||
mogrify -modulate 90,100,140 mastodon/src/main/res/mipmap-*/ic_launcher*.png
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
@@ -23,4 +57,4 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the [GPL-3 License](./LICENSE).
|
||||
This project is released under the [GPL-3 License](./LICENSE).
|
||||
|
||||
16
fastlane/metadata/android/cs-CZ/full_description.txt
Normal file
16
fastlane/metadata/android/cs-CZ/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jednné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, kteří mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom vysílají na Mastodon!
|
||||
|
||||
Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v bezreklamní a chronologické časové linii. Vyjádřete se pomocí vlastních emojí, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a reblogujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě.
|
||||
|
||||
Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se svými sledujícími, jen s lidmi, které zmiňujete, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a hlášení nástrojů pomáhá předcházet zneužití.
|
||||
|
||||
Další funkce:
|
||||
|
||||
• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo zcela černém režimu
|
||||
• Ankety: Požádejte sledující o jejich názor a spojte se s jejich hlasováním
|
||||
• Průzkum: Trendové hashtagy a účty jsou pryč na jedno klepnutí
|
||||
• Upozornění: Dostávejte upozornění na nové sledování, odpovědi a reblogy
|
||||
• Sdílení: Odesílání přímo do Mastodonu z libovolného seznamu sdílení v jakékoliv aplikaci
|
||||
• Roztomilost: Naším maskotem je roztomilý slon, kterého čas od času uvidíte
|
||||
|
||||
Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a my máme v plánu to udržet.
|
||||
1
fastlane/metadata/android/cs-CZ/short_description.txt
Normal file
1
fastlane/metadata/android/cs-CZ/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralizovaná sociální síť
|
||||
1
fastlane/metadata/android/cs-CZ/title.txt
Normal file
1
fastlane/metadata/android/cs-CZ/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Website ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal was dich interessiert, auf Mastodon kannst du interessierte Leute treffen, die darüber schreiben!
|
||||
|
||||
Trete einer Gemeinschaft bei und erstelle dein Profil. Finde und verfolge faszinierende Leute und lese ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bilderns, GIFs, Videos und Audio in 500-Zeichen Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten, und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bildern, GIFs, Videos und Audio in 500-Zeichen-Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen, um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||
|
||||
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge um Missbrauch vorzubeugen.
|
||||
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
|
||||
|
||||
Weitere Funktionen:
|
||||
|
||||
• Dunkler Modus: Beiträge im hellen, dunklen oder schwarzen Modus lesen
|
||||
• Umfragen: Frage deine Follower nach ihrer Meinung und zähle die Stimmen
|
||||
• Entdecken: Trending Hashtags und Accounts sind nur einen Fingertipp entfernt
|
||||
• Entdecken: trendende Hashtags und Profile sind nur einen Fingertipp entfernt
|
||||
• Benachrichtigungen: Erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
|
||||
• Teilen: Veröffentliche auf Mastodon aus jeder beliebigen anderen App
|
||||
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
||||
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant, und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
||||
|
||||
Mastodon ist eine eingetragene gemeinnützige Organisation und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen es so zu erhalten.
|
||||
Mastodon ist eine eingetragene gemeinnützige Organisation, und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen, das auch so beizubehalten.
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon es la red social descentralizada más grande de internet. En lugar de ser una sola web, es una red de millones de usuarios en comunidades independientes que pueden interactuar entre ellas de forma transparente. No importa qué es lo que hagas, podrás encontrar gente apasionada escribiendo sobre ello en Mastodon!
|
||||
|
||||
Únete a una comunidad y crea tu perfil. Encuentra y sigue a gente fascinante y lee sus publicaciones sin anuncios y de forma cronológica. Exprésate con emoticonos personalizados, imágenes, GIFs, vídeos y audio en publicaciónes de 500 caracteres. Responde a hilos e impulsa publicaciones de cualquiera para compartir contenido genial. Encuentra nuevas cuentas para seguir y los hashtags de actualidad para expandir tu red.
|
||||
Únete a una comunidad y crea tu perfil. Encuentra y sigue a gente fascinante y lee sus publicaciones sin anuncios y de forma cronológica. Exprésate con emoticonos personalizados, imágenes, GIFs, vídeos y audio en publicaciónes de 500 caracteres. Responde a hilos e rebloguea publicaciones de cualquiera para compartir contenido genial. Encuentra nuevas cuentas para seguir y los hashtags de actualidad para expandir tu red.
|
||||
|
||||
Mastodon está construída con un enfoque en la privacidad y la seguridad. Decide si tus publicaciones se comparten con tus seguidores, solo a la gente que menciones, o a todo el mundo. Las advertencias de contenido te permiten esconder publicaciones con contenido sensible o limitarlas de tu visión hasta que estés listo para interactuar con ellas. Cada comunidad tiene sus propias reglas y moderadores para mantener a salvo a sus miembros, además de herramientas robustas para bloquear y reportar contenido para prevenir el abuso.
|
||||
|
||||
@@ -9,7 +9,7 @@ Más características:
|
||||
• Modo oscuro: Lee las publicaciones en modo claro, oscuro o negro real
|
||||
• Encuestas: Pide opinión a tus seguidores y cuenta los votos
|
||||
• Explora: Hashtags y cuentas en tendencia a un solo toque
|
||||
• Notificaciones: Recibe notificaciones sobre nuevos seguidores, respuestas e impulsos
|
||||
• Notificaciones: Recibe notificaciones sobre nuevos seguidores, respuestas y reblogueos
|
||||
• Compartir: Publica directamente a Mastodon desde cualquier hoja de acción en cualquier aplicación
|
||||
• Preciosidad: Nuestra mascota es un elefante adorable, y verás que aparece de vez en cuando
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata intohimoisia ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Liity yhteisöön ja luo itsellesi tili. Löydä ja seuraa kiehtovia ihmisiä ja lue heidän julkaisunsa ilman mainoksia, kronologisella aikajanalla. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja trendaavia hashtageja laajentaaksesi verkostoasi.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon on rakennettu keskittyen yksityisyyteen ja turvallisuuteen. Päätä, jaetaanko julkaisusi seuraajille, vain mainitsemillesi ihmisille vai koko maailmalle. Sisältövaroitusten avulla, voit piilottaa julkaisut, jotka sisältävät arkaluontoista tai laukaisevaa materiaalia, kunnes olet valmis käsittelemään niitä. Jokaisella yhteisöllä on omat ohjeistonsa ja valvojansa, jotka pitävät jäsenensä turvassa, ja tehokkaat esto- ja ilmiantotyökalut auttavat torjumaan väärinkäytöksiä.
|
||||
|
||||
More features:
|
||||
Lisää ominaisuuksia:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Tumma tila: Lue julkaisut vaaleassa, tummassa tai mustan tummassa tilassa
|
||||
• Kyselyt: Kysy seuraajilta heidän mielipidettään ja laske äänet
|
||||
• Tutustu: Trendaavat hashtagit ja tilit ovat vain napsautuksen päässä
|
||||
• Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja edelleen jaoista
|
||||
• Jakaminen: Julkaise suoraan Mastodoniin minkä tahansa sovelluksen jakovalikon kautta
|
||||
• Suloisuus: Maskottimme on ihastuttava mastodontti ja näet sen ajoittain
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon on rekisteröity voittoa tavoittelematon organisaatio ja kehitystä tuetaan suoraan lahjoituksillasi. Ei mainontaa, kaupallistamista eikä riskipääomaa, ja aiomme pitää sen sellaisena.
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
Hajautettu sosiaalinen verkosto
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
마스토돈은 인터넷에서 가장 큰 분산 소셜 네트워크입니다. 단 하나의 통일된 웹사이트 대신, 수백만 명의 사용자들이, 서로 경계 없이 소통할 수 있는 독립적인 커뮤니티의 네트워크입니다. 당신이 어디에 속하든간에, 마스토돈에 열정적으로 게시물을 남기는 사람들을 만날 수 있습니다!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
커뮤니티에 가입하고 프로필을 생성하세요. 매력적인 사람들을 찾아 팔로우하고 그들의 글을 광고가 없고, 시간순으로 정렬된 타임라인에서 읽으세요. 커스텀 에모지, 그림, 움짤, 동영상, 소리와 함께 500자의 글로 당신을 표현하세요. 아무에게나 글타래에 답장을 하고 게시물을 리블로그하여 멋진 것들을 공유하세요. 새로 팔로우 할 계정이나 유행 중인 해시태그를 찾아 당신의 인맥을 넓히세요.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
마스토돈은 개인정보 보호와 안전에 중점을 두고 만들어졌습니다. 당신의 게시물을 팔로워들에게만 공개할지, 언급한 사람들에게만 공유할지, 아니면 전세계에 공유할 지 선택하세요. 열람주의는 민감하거나 남들에게 껄끄러울 수 있는 게시물을 마음의 준비가 된 사람들만 열람하도록 숨길 수 있도록 해줍니다. 각각의 커뮤니티는 구성원들을 안전하게 지키기 위한 각자의 규정과 중재자들을 가지고 있으며, 남용을 방지하기 위한 강력한 차단 도구와 신고 도구를 가지고 있습니다.
|
||||
|
||||
더 많은 기능:
|
||||
|
||||
• 다크모드: 게시물을 밝음, 어두움, 진정한 검정 모드에서 읽으세요
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• 투표: 팔로워들에게 의견을 물어보고 투표를 집계하세요
|
||||
• 탐색: 유행 중인 해시태그와 계정을 탭 한 번에 볼 수 있습니다
|
||||
• 알림: 새로운 팔로우, 답글, 리블로그에 대한 알림을 받으세요
|
||||
• 공유: 어떤 앱에서든 공유 기능으로 바로 마스토돈에 게시하세요
|
||||
• 귀여움: 우리의 마스코트는 귀여운 코끼리입니다, 때때로 이들을 볼 수 있습니다
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
마스토돈은 등록된 상호이며 여러분들의 직접적인 기부를 통해 개발되고 있습니다. 광고도, 유료화도, 벤처 캐피탈도 없습니다, 그리고 계속 그렇게 할 생각입니다.
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon to największa zdecentralizowana sieć społecznościowa w Internecie. Zamiast jednej strony internetowej, jest to sieć milionów użytkowników w niezależnych społecznościach, które mogą ze sobą wchodzić w interakcje. Niezależnie od swoich zainteresowań, momżesz poznać interesujących ludzi piszących o nich na Mastodonie!
|
||||
|
||||
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Odnajduj nowe konta do śledzenia i zyskujące popularność hashtagi, by poszerzać swoją sieć.
|
||||
|
||||
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decyduj, czy Twoje wpisy są udostępniane osobom śledzącym Cię, tylko wzmiankowanym, czy całemu światu. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Każda społeczność ma własne wytyczne i moderatorów, aby zapewniać swoim członkom bezpieczeństwo, a także solidne narzędzia blokowania i raportowania pomagające zapobiegać nadużyciom.
|
||||
|
||||
Więcej funkcji:
|
||||
|
||||
• Tryb ciemny: Czytaj wpisy w jasnym, ciemnym lub czarnym trybie
|
||||
• Ankiety: Poproś obserwujących o ich opinię i poznaj ich głosy
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Odkrywaj: Najpopularniejsze hashtagi i konta są dostępne za jednym dotknięciem
|
||||
• Powiadomienia: Otrzymuj powiadomienia o nowych obserwacjach, odpowiedziach i udostępnieniach
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Udostępnianie: Publikuj bezpośrednio na Mastodonie z menu udostępniania w dowolnej aplikacji
|
||||
• Słodycz: Nasza maskotka to uroczy słoń i zobaczysz go pojawiającego się od czasu do czasu
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon to zarejestrowana organizacja non-profit i rozwój jest wspierany bezpośrednio przez darowizny. Nie ma reklam, monetyzacji, ani kapitału inwestycyjnego i planujemy, by tak pozostało.
|
||||
@@ -1,8 +1,8 @@
|
||||
O Mastodon é a maior rede social descentralizada da Internet. Em vez de ser um único site, é uma rede de milhões de utilizadores em comunidades independentes que podem facilmente interagir uns com os outros. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
O Mastodon é a maior rede social descentralizada da Internet. Em vez de ser um único site, é uma rede de milhões de utilizadores em comunidades independentes que podem facilmente interagir uns com os outros. Independemente dos teus gostos, consegues encontrar pessoas que os partilhem no Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Junta-te a uma comunidade e cria o teu perfil. Encontra, segue gente fascinante e lê as suas publicações numa cronologia sem anúncios. Expressa-te com emojis personalizados imagens, GIFs, vídeos e áudio em publicações com até 500 caracteres. Responde a tópicos e promove publicações de qualquer pessoa para partilhares ótimas coisas. Encontra novas contas e tendências a seguir para expandires a tua rede.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
O Mastodon é construído com foco na privacidade e segurança. Decide se as tuas publicações são partilhadas com os teus seguidores, apenas com as pessoas mencionadas, ou com o mundo inteiro. Avisos de conteúdo permitem-te esconder publicações que contenham material sensível ou provocatório até estares pronto(a) para o veres. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon är det största decentraliserade sociala nätverket på internet. I stället för en enda webbplats är det ett nätverk av miljontals användare på oberoende servrar som alla kan interagera med varandra, sömlöst. Oavsett vad du är intresserad av kan du träffa passionerade personer som diskuterar ämnet på Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Gå med på en server och skapa din profil. Hitta och följ fascinerande människor och läsa deras inlägg i en annonsfri, kronologisk tidslinje. Uttryck dig med anpassade emoji, bilder, GIF:ar, videor och ljud i 500-teckensinlägg. Svara på trådar och ompostningar från vem som helst för att dela bra saker. Hitta nya konton att följa och trendande hashtaggar för att utöka ditt nätverk.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon är byggt med fokus på integritet och trygghet. Bestäm om dina inlägg delas med dina följare, bara personer du omnämner, eller hela världen. Innehållsvarningar låter dig dölja inlägg som innehåller känsligt eller triggande material tills du är redo att interagera med dem. Varje server har sina egna riktlinjer och moderatorer för att hålla sina medlemmar trygga, och robusta blockerings- och rapporteringsverktyg för att förhindra missbruk.
|
||||
|
||||
More features:
|
||||
Fler funktioner:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Mörkt läge: Läs inlägg i ljust, mörkt eller helsvart läge
|
||||
• Omröstningar: Fråga följare om deras åsikt och sammanställ deras röster
|
||||
• Utforska: Trendande hashtaggar och konton är ett tryck bort
|
||||
• Notiser: Bli meddelad om nya följare, svar och ompostningar
|
||||
• Delning: Posta direkt till Mastodon från delningsbladet i alla appar
|
||||
• Gullighet: Vår maskot är en bedårande elefant, och du kommer att se dem dyka upp då och då
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon är en registrerad ideell förening och utvecklingen stöds direkt av dina donationer. Det finns ingen reklam, ingen monetarisering, och inget riskkapital, och vi planerar att behålla det på det sättet.
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
Decentraliserat socialt nätverk
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในไทม์ไลน์ที่ไร้โฆษณาและเรียงตามลำดับเวลาล้วน ๆ แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้ควบคุมเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
|
||||
|
||||
More features:
|
||||
คุณสมบัติอื่น ๆ:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• โหมดมืด: อ่านโพสต์ในโหมดสว่าง มืด หรือโหมดมืดดำสนิท
|
||||
• การสำรวจความคิดเห็น: สำรวจความคิดเห็นของผู้ติดตามและนับจำนวนการลงคะแนน
|
||||
• สำรวจ: แตะปุ่มเดียวเพื่อดูแฮชแท็กและบัญชีที่เป็นที่นิยม
|
||||
• การแจ้งเตือน: รับการแจ้งเตือนเกี่ยวกับผู้ติดตามใหม่ การตอบกลับ และการดันโพสต์
|
||||
• การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
|
||||
• ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon là mạng xã hội liên hợp lớn nhất trên internet. Thay vì một trang web duy nhất, nó là một mạng lưới hàng triệu người dùng trong các máy chủ độc lập, tất cả đều có thể tương tác với nhau một cách liền mạch. Bất kể bạn thích gì, bạn đều có thể gặp gỡ những người đăng tút về nó trên Mastodon!
|
||||
|
||||
Tham gia một máy chủ và tạo trang hồ sơ của bạn. Tìm, theo dõi những người thú vị và đọc tút của họ theo trình tự thời gian, không có quảng cáo. Thể hiện bản thân bằng emoji, hình ảnh, GIF, video và âm thanh trong tút tối đa 500 ký tự. Trả lời tút và đăng lại tút từ bất kỳ ai để chia sẻ những điều tuyệt vời. Tìm những người dùng mới để theo dõi và các hashtag xu hướng để mở rộng mạng lưới của bạn.
|
||||
Tham gia một máy chủ và tạo trang hồ sơ của bạn. Tìm, theo dõi những người thú vị và đọc tút của họ theo trình tự thời gian, không có quảng cáo. Thể hiện bản thân bằng emoji, hình ảnh, GIF, video và âm thanh trong tút tối đa 500 ký tự. Trả lời tút và đăng lại tút từ bất kỳ ai để chia sẻ những điều tuyệt vời. Tìm những người dùng mới để theo dõi và các hashtag nổi bật để mở rộng mạng lưới của bạn.
|
||||
|
||||
Mastodon được xây dựng tập trung vào sự riêng tư và an toàn. Quyết định xem tút của bạn được chia sẻ với những người theo dõi, chỉ những người bạn nhắc đến hay cả thế giới. Nội dung ẩn cho phép bạn ẩn các tút chứa nội dung nhạy cảm hoặc chơi chữ cho đến khi bạn sẵn sàng tương tác với chúng. Mỗi máy chủ có các nguyên tắc riêng và kiểm duyệt viên riêng để giữ an toàn cho các thành viên, song song với các công cụ chặn và báo cáo mạnh mẽ giúp ngăn chặn hành vi bậy.
|
||||
|
||||
@@ -8,7 +8,7 @@ Tính năng khác:
|
||||
|
||||
• Chế độ Tối: Đọc tút ở chế độ sáng, tối hoặc đen
|
||||
• Bình chọn: Hỏi cộng đồng về ý kiến của họ và đếm lượt bình chọn
|
||||
• Khám phá: Xem hashtag xu hướng và tài khoản chỉ bằng một nhấn
|
||||
• Khám phá: Xem hashtag nổi bật và tài khoản chỉ bằng một nhấn
|
||||
• Thông báo: Nhận thông báo về người theo dõi, lượt trả lời và đăng lại mới
|
||||
• Chia sẻ: Đăng trực tiếp lên Mastodon từ bất kỳ ứng dụng nào
|
||||
• Đáng yêu: Linh vật của chúng tôi là một chú voi ma mút và bạn sẽ thấy anh ấy thỉnh thoảng xuất hiện
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon 是網際網路上最大的去中心化社交網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能在 Mastodon 上遇到充滿熱情的人們討論該話題。
|
||||
|
||||
加入社群並建立您的個人檔案。 尋找並追蹤迷人的夥伴,並在無廣告、按時間順序排列的時間軸上閱讀他們的貼文。 在 500 個字元的貼文中使用自訂表情符號、GIF、視訊與音訊來表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要追蹤的新帳號與熱門主題標籤來拓展您的網路。
|
||||
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並在無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 在 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊來表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤來拓展您的網路。
|
||||
|
||||
Mastodon 以隱私與安全為要。 決定您的貼文要與您的追蹤者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的貼文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
|
||||
Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
|
||||
|
||||
更多功能:
|
||||
|
||||
• 深色模式:以淺色、深色或純黑色模式閱讀貼文
|
||||
• 投票:詢問追蹤的意見並計票
|
||||
• 投票:詢問跟隨者們的意見並計票
|
||||
• 探索:僅需輕點一下,即可看到熱門主題標籤與帳號
|
||||
• 通知:取得關於新追蹤、回覆與轉發的通知
|
||||
• 分享:從任何應用程式中的分享表中直接發表貼文到 Mastodon 中
|
||||
• 通知:取得關於新跟隨者們、回覆與轉發的通知
|
||||
• 分享:從任何應用程式中的分享表中直接發表嘟文到 Mastodon 中
|
||||
• 可愛:我們的吉祥物是一隻可愛的大象,您會不時看到牠出現
|
||||
|
||||
Mastodon 是一家註冊的非營利組織,您的捐款會直接支援開發工作。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。
|
||||
@@ -4,14 +4,18 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
compileSdk 33
|
||||
defaultConfig {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 31
|
||||
versionCode 6
|
||||
versionName '1.1.1-dev+fork.6'
|
||||
targetSdk 33
|
||||
versionCode 35
|
||||
versionName "1.1.4+fork.35"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
|
||||
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
|
||||
"ja-rJP", "kab", "ko-rKR", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ru-rRU",
|
||||
"sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -33,6 +37,9 @@ android {
|
||||
initWith release
|
||||
versionNameSuffix "-beta"
|
||||
}
|
||||
githubRelease{
|
||||
initWith release
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -46,6 +53,16 @@ android {
|
||||
appcenterPublicBeta{
|
||||
setRoot "src/appcenter"
|
||||
}
|
||||
githubRelease{
|
||||
setRoot "src/github"
|
||||
}
|
||||
debug {
|
||||
setRoot "src/github"
|
||||
}
|
||||
}
|
||||
lintOptions{
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
mastodon/proguard-rules.pro
vendored
7
mastodon/proguard-rules.pro
vendored
@@ -46,4 +46,9 @@
|
||||
|
||||
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
# Parceler library
|
||||
-keep interface org.parceler.Parcel
|
||||
-keep @org.parceler.Parcel class * { *; }
|
||||
-keep class **$$Parcelable { *; }
|
||||
|
||||
21
mastodon/src/github/AndroidManifest.xml
Normal file
21
mastodon/src/github/AndroidManifest.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.joinmastodon.android">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
|
||||
<application>
|
||||
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>-->
|
||||
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </receiver>-->
|
||||
<provider
|
||||
android:authorities="${applicationId}.self_update_provider"
|
||||
android:name=".updater.SelfUpdateContentProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,356 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Keep
|
||||
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
private static final long CHECK_PERIOD=6*3600*1000L;
|
||||
private static final String TAG="GithubSelfUpdater";
|
||||
|
||||
private UpdateState state=UpdateState.NO_UPDATE;
|
||||
private UpdateInfo info;
|
||||
private long downloadID;
|
||||
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
|
||||
MastodonApp.context.unregisterReceiver(this);
|
||||
setState(UpdateState.DOWNLOADED);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public GithubSelfUpdaterImpl(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
|
||||
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
|
||||
info=new UpdateInfo();
|
||||
info.version=prefs.getString("version", null);
|
||||
info.size=prefs.getLong("apkSize", 0);
|
||||
downloadID=prefs.getLong("downloadID", 0);
|
||||
if(downloadID==0 || !getUpdateApkFile().exists()){
|
||||
state=UpdateState.UPDATE_AVAILABLE;
|
||||
}else{
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
|
||||
if(state==UpdateState.DOWNLOADING){
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
}
|
||||
}
|
||||
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
|
||||
// We are in a new version, running for the first time after update. Gotta clean things up.
|
||||
long id=getPrefs().getLong("downloadID", 0);
|
||||
if(id!=0){
|
||||
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
|
||||
}
|
||||
getUpdateApkFile().delete();
|
||||
getPrefs().edit()
|
||||
.remove("apkSize")
|
||||
.remove("version")
|
||||
.remove("apkURL")
|
||||
.remove("checkedByBuild")
|
||||
.remove("downloadID")
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeCheckForUpdates(){
|
||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
|
||||
if(timeSinceLastCheck>=CHECK_PERIOD){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkForUpdates() {
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
|
||||
private void actuallyCheckForUpdates(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://api.github.com/repos/sk22/mastodon-android-fork/releases/latest")
|
||||
.build();
|
||||
Call call=MastodonAPIController.getHttpClient().newCall(req);
|
||||
try(Response resp=call.execute()){
|
||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||
return;
|
||||
}
|
||||
int newMajor=Integer.parseInt(matcher.group(1)),
|
||||
newMinor=Integer.parseInt(matcher.group(2)),
|
||||
newRevision=Integer.parseInt(matcher.group(3)),
|
||||
newForkNumber=Integer.parseInt(matcher.group(4));
|
||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||
return;
|
||||
}
|
||||
int curMajor=Integer.parseInt(matcher.group(1)),
|
||||
curMinor=Integer.parseInt(matcher.group(2)),
|
||||
curRevision=Integer.parseInt(matcher.group(3)),
|
||||
curForkNumber=Integer.parseInt(matcher.group(4));
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber || BuildConfig.DEBUG){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
JsonObject asset=el.getAsJsonObject();
|
||||
if("application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
|
||||
long size=asset.get("size").getAsLong();
|
||||
String url=asset.get("browser_download_url").getAsString();
|
||||
|
||||
UpdateInfo info=new UpdateInfo();
|
||||
info.size=size;
|
||||
info.version=version;
|
||||
this.info=info;
|
||||
|
||||
getPrefs().edit()
|
||||
.putLong("apkSize", size)
|
||||
.putString("version", version)
|
||||
.putString("apkURL", url)
|
||||
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
|
||||
.remove("downloadID")
|
||||
.apply();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "actuallyCheckForUpdates", x);
|
||||
}finally{
|
||||
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(UpdateState state){
|
||||
this.state=state;
|
||||
E.post(new SelfUpdateStateChangedEvent(state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateState getState(){
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateInfo getUpdateInfo(){
|
||||
return info;
|
||||
}
|
||||
|
||||
public File getUpdateApkFile(){
|
||||
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadUpdate(){
|
||||
if(state==UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
downloadID=dm.enqueue(
|
||||
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
||||
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
||||
);
|
||||
getPrefs().edit().putLong("downloadID", downloadID).apply();
|
||||
setState(UpdateState.DOWNLOADING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installUpdate(Activity activity){
|
||||
if(state!=UpdateState.DOWNLOADED)
|
||||
throw new IllegalStateException();
|
||||
Uri uri;
|
||||
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}else{
|
||||
uri=Uri.fromFile(getUpdateApkFile());
|
||||
}
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||
activity.startActivity(intent);
|
||||
|
||||
// TODO figure out how to restart the app when updating via this new API
|
||||
/*
|
||||
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
|
||||
try{
|
||||
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
|
||||
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
|
||||
@Override
|
||||
public void onCreated(int i){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBadgingChanged(int i){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveChanged(int i, boolean b){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(int id, float progress){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(int id, boolean success){
|
||||
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
});
|
||||
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
|
||||
PackageInstaller.Session session=installer.openSession(sid);
|
||||
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
|
||||
byte[] buffer=new byte[16384];
|
||||
int read;
|
||||
while((read=in.read(buffer))>0){
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
session.commit(intent.getIntentSender());
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "installUpdate", x);
|
||||
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDownloadProgress(){
|
||||
if(state!=UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
|
||||
if(cursor.moveToFirst()){
|
||||
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
|
||||
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
|
||||
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
|
||||
return total>0 ? (float)loaded/total : 0f;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDownload(){
|
||||
if(state!=UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
dm.remove(downloadID);
|
||||
downloadID=0;
|
||||
getPrefs().edit().remove("downloadID").apply();
|
||||
setState(UpdateState.UPDATE_AVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleIntentFromInstaller(Intent intent, Activity activity){
|
||||
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
activity.startActivity(confirmIntent);
|
||||
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
|
||||
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
|
||||
Intent restartIntent=new Intent(context, MainActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setPackage(context.getPackageName());
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
|
||||
context.startActivity(restartIntent);
|
||||
}else{
|
||||
// Bypass activity starting restrictions by starting it from a notification
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
|
||||
nm.createNotificationChannel(chan);
|
||||
Notification n=new Notification.Builder(context, "selfUpdateRestart")
|
||||
.setContentTitle(context.getString(R.string.update_installed))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
|
||||
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.build();
|
||||
nm.notify(1, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class SelfUpdateContentProvider extends ContentProvider{
|
||||
@Override
|
||||
public boolean onCreate(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri){
|
||||
if(isCorrectUri(uri))
|
||||
return "application/vnd.android.package-archive";
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||
if(isCorrectUri(uri)){
|
||||
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
private boolean isCorrectUri(Uri uri){
|
||||
return "/update.apk".equals(uri.getPath());
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
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"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 46 KiB |
@@ -7,6 +7,9 @@ public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean trueBlackTheme;
|
||||
public static boolean showReplies;
|
||||
public static boolean showBoosts;
|
||||
public static boolean loadNewPosts;
|
||||
public static ThemePreference theme;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
@@ -18,6 +21,9 @@ public class GlobalUserPreferences{
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
}
|
||||
|
||||
@@ -25,6 +31,9 @@ public class GlobalUserPreferences{
|
||||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putBoolean("loadNewPosts", loadNewPosts)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.apply();
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Application;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.SplashFragment;
|
||||
@@ -16,6 +21,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@@ -56,6 +62,10 @@ public class MainActivity extends FragmentStackActivity{
|
||||
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else{
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +75,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
try{
|
||||
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
|
||||
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
|
||||
}else if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +103,11 @@ public class MainActivity extends FragmentStackActivity{
|
||||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
}
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||
}*/
|
||||
}
|
||||
|
||||
private void showFragmentForNotification(Notification notification, String accountID){
|
||||
@@ -115,4 +131,21 @@ public class MainActivity extends FragmentStackActivity{
|
||||
fragment.setArguments(args);
|
||||
showFragment(fragment);
|
||||
}
|
||||
|
||||
private void showCompose(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
ComposeFragment compose=new ComposeFragment();
|
||||
Bundle composeArgs=new Bundle();
|
||||
composeArgs.putString("account", session.getID());
|
||||
compose.setArguments(composeArgs);
|
||||
showFragment(compose);
|
||||
}
|
||||
|
||||
private void maybeRequestNotificationsPermission(){
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AvatarResizedImageRequestBody extends ResizedImageRequestBody{
|
||||
public AvatarResizedImageRequestBody(Uri uri, ProgressListener progressListener) throws IOException{
|
||||
super(uri, 0, progressListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] getTargetSize(int srcWidth, int srcHeight){
|
||||
float factor=400f/Math.min(srcWidth, srcHeight);
|
||||
return new int[]{Math.round(srcWidth*factor), Math.round(srcHeight*factor)};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needResize(int srcWidth, int srcHeight){
|
||||
return srcHeight>400 || srcWidth!=srcHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needCrop(int srcWidth, int srcHeight){
|
||||
return srcWidth!=srcHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Rect getCropBounds(int srcWidth, int srcHeight){
|
||||
Rect rect=new Rect();
|
||||
if(srcWidth>srcHeight){
|
||||
rect.set(srcWidth/2-srcHeight/2, 0, srcWidth/2-srcHeight/2+srcHeight, srcHeight);
|
||||
}else{
|
||||
rect.set(0, srcHeight/2-srcWidth/2, srcWidth, srcHeight/2-srcWidth/2+srcWidth);
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
@@ -184,7 +184,7 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
@@ -233,6 +233,12 @@ public class CacheController{
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteStatus(String id){
|
||||
runOnDbThread((db)->{
|
||||
db.delete("home_timeline", "`id`=?", new String[]{id});
|
||||
});
|
||||
}
|
||||
|
||||
public void clearRecentSearches(){
|
||||
runOnDbThread((db)->db.delete("recent_searches", null, null));
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ public class MastodonAPIController{
|
||||
if(call.isCanceled())
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed: "+e);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
req.onError(e.getLocalizedMessage(), 0);
|
||||
req.onError(e.getLocalizedMessage(), 0, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,7 +133,7 @@ public class MastodonAPIController{
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code());
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class MastodonAPIController{
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code());
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class MastodonAPIController{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||
if(error.has("details")){
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code());
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||
JsonObject errorDetails=error.getAsJsonObject("details");
|
||||
for(String key:errorDetails.keySet()){
|
||||
@@ -172,12 +172,12 @@ public class MastodonAPIController{
|
||||
err.detailedErrors=details;
|
||||
req.onError(err);
|
||||
}else{
|
||||
req.onError(error.get("error").getAsString(), response.code());
|
||||
req.onError(error.get("error").getAsString(), response.code(), null);
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
req.onError(response.code()+" "+response.message(), response.code());
|
||||
req.onError(response.code()+" "+response.message(), response.code(), x);
|
||||
}catch(Exception x){
|
||||
req.onError("Error parsing an API error", response.code());
|
||||
req.onError("Error parsing an API error", response.code(), x);
|
||||
}
|
||||
}
|
||||
}catch(Exception x){
|
||||
@@ -189,7 +189,7 @@ public class MastodonAPIController{
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage(), 0);
|
||||
req.onError(x.getLocalizedMessage(), 0, x);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -197,4 +197,8 @@ public class MastodonAPIController{
|
||||
public static void runInBackground(Runnable action){
|
||||
thread.postRunnable(action, 0);
|
||||
}
|
||||
|
||||
public static OkHttpClient getHttpClient(){
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
account.getApiController().submitRequest(this);
|
||||
}catch(Exception x){
|
||||
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
|
||||
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1));
|
||||
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1, x));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -194,8 +194,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
invokeErrorCallback(err);
|
||||
}
|
||||
|
||||
void onError(String msg, int httpStatus){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
|
||||
void onError(String msg, int httpStatus, Throwable exception){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
|
||||
}
|
||||
|
||||
void onSuccess(T resp){
|
||||
|
||||
@@ -7,8 +7,8 @@ import java.util.Map;
|
||||
public class MastodonDetailedErrorResponse extends MastodonErrorResponse{
|
||||
public Map<String, List<FieldError>> detailedErrors;
|
||||
|
||||
public MastodonDetailedErrorResponse(String error, int httpStatus){
|
||||
super(error, httpStatus);
|
||||
public MastodonDetailedErrorResponse(String error, int httpStatus, Throwable exception){
|
||||
super(error, httpStatus, exception);
|
||||
}
|
||||
|
||||
public static class FieldError{
|
||||
|
||||
@@ -12,10 +12,12 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
public class MastodonErrorResponse extends ErrorResponse{
|
||||
public final String error;
|
||||
public final int httpStatus;
|
||||
public final Throwable underlyingException;
|
||||
|
||||
public MastodonErrorResponse(String error, int httpStatus){
|
||||
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
|
||||
this.error=error;
|
||||
this.httpStatus=httpStatus;
|
||||
this.underlyingException=exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,8 +12,10 @@ import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -30,62 +32,107 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
private File tempFile;
|
||||
private Uri uri;
|
||||
private String contentType;
|
||||
private int maxSize;
|
||||
|
||||
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
|
||||
super(progressListener);
|
||||
this.uri=uri;
|
||||
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
||||
this.maxSize=maxSize;
|
||||
BitmapFactory.Options opts=new BitmapFactory.Options();
|
||||
opts.inJustDecodeBounds=true;
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
BitmapFactory.decodeStream(in, null, opts);
|
||||
if("file".equals(uri.getScheme())){
|
||||
BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
|
||||
}else{
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
BitmapFactory.decodeStream(in, null, opts);
|
||||
}
|
||||
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
||||
}
|
||||
if(opts.outWidth*opts.outHeight>maxSize){
|
||||
if(TextUtils.isEmpty(contentType))
|
||||
contentType="image/jpeg";
|
||||
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
|
||||
Bitmap bitmap;
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{
|
||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight())));
|
||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth())));
|
||||
if(Build.VERSION.SDK_INT>=28){
|
||||
ImageDecoder.Source source;
|
||||
if("file".equals(uri.getScheme())){
|
||||
source=ImageDecoder.createSource(new File(uri.getPath()));
|
||||
}else{
|
||||
source=ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri);
|
||||
}
|
||||
bitmap=ImageDecoder.decodeBitmap(source, (decoder, info, _source)->{
|
||||
int[] size=getTargetSize(info.getSize().getWidth(), info.getSize().getHeight());
|
||||
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
|
||||
decoder.setTargetSize(targetWidth, targetHeight);
|
||||
decoder.setTargetSize(size[0], size[1]);
|
||||
// Breaks images in mysterious ways
|
||||
// if(needCrop(size[0], size[1]))
|
||||
// decoder.setCrop(getCropBounds(size[0], size[1]));
|
||||
});
|
||||
if(needCrop(bitmap.getWidth(), bitmap.getHeight())){
|
||||
Rect crop=getCropBounds(bitmap.getWidth(), bitmap.getHeight());
|
||||
bitmap=Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height());
|
||||
}
|
||||
}else{
|
||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight)));
|
||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth)));
|
||||
int[] size=getTargetSize(opts.outWidth, opts.outHeight);
|
||||
int targetWidth=size[0];
|
||||
int targetHeight=size[1];
|
||||
float factor=opts.outWidth/(float)targetWidth;
|
||||
opts=new BitmapFactory.Options();
|
||||
opts.inSampleSize=(int)factor;
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
bitmap=BitmapFactory.decodeStream(in, null, opts);
|
||||
if("file".equals(uri.getScheme())){
|
||||
bitmap=BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||
}else{
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
bitmap=BitmapFactory.decodeStream(in, null, opts);
|
||||
}
|
||||
}
|
||||
if(factor%1f!=0f){
|
||||
Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
|
||||
new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, targetWidth, targetHeight), new Paint(Paint.FILTER_BITMAP_FLAG));
|
||||
boolean needCrop=needCrop(targetWidth, targetHeight);
|
||||
if(factor%1f!=0f || needCrop){
|
||||
Rect srcBounds=null;
|
||||
Rect dstBounds;
|
||||
if(needCrop){
|
||||
Rect crop=getCropBounds(targetWidth, targetHeight);
|
||||
dstBounds=new Rect(0, 0, crop.width(), crop.height());
|
||||
srcBounds=new Rect(
|
||||
Math.round(crop.left/(float)targetWidth*bitmap.getWidth()),
|
||||
Math.round(crop.top/(float)targetHeight*bitmap.getHeight()),
|
||||
Math.round(crop.right/(float)targetWidth*bitmap.getWidth()),
|
||||
Math.round(crop.bottom/(float)targetHeight*bitmap.getHeight())
|
||||
);
|
||||
}else{
|
||||
dstBounds=new Rect(0, 0, targetWidth, targetHeight);
|
||||
}
|
||||
Bitmap scaled=Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888);
|
||||
new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG));
|
||||
bitmap=scaled;
|
||||
}
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
int rotation;
|
||||
int orientation=0;
|
||||
if("file".equals(uri.getScheme())){
|
||||
ExifInterface exif=new ExifInterface(uri.getPath());
|
||||
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
ExifInterface exif=new ExifInterface(in);
|
||||
int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
rotation=switch(orientation){
|
||||
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
if(rotation!=0){
|
||||
Matrix matrix=new Matrix();
|
||||
matrix.setRotate(rotation);
|
||||
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
}
|
||||
}
|
||||
int rotation=switch(orientation){
|
||||
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
if(rotation!=0){
|
||||
Matrix matrix=new Matrix();
|
||||
matrix.setRotate(rotation);
|
||||
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||
}
|
||||
}
|
||||
|
||||
tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image");
|
||||
boolean isPNG="image/png".equals(contentType);
|
||||
tempFile=File.createTempFile("mastodon_tmp_resized", null);
|
||||
try(FileOutputStream out=new FileOutputStream(tempFile)){
|
||||
if("image/png".equals(contentType)){
|
||||
if(isPNG){
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||
}else{
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
|
||||
@@ -94,9 +141,13 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
}
|
||||
length=tempFile.length();
|
||||
}else{
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
length=cursor.getInt(0);
|
||||
if("file".equals(uri.getScheme())){
|
||||
length=new File(uri.getPath()).length();
|
||||
}else{
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
length=cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,4 +176,22 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected int[] getTargetSize(int srcWidth, int srcHeight){
|
||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)srcWidth/srcHeight)));
|
||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)srcHeight/srcWidth)));
|
||||
return new int[]{targetWidth, targetHeight};
|
||||
}
|
||||
|
||||
protected boolean needResize(int srcWidth, int srcHeight){
|
||||
return srcWidth*srcHeight>maxSize;
|
||||
}
|
||||
|
||||
protected boolean needCrop(int srcWidth, int srcHeight){
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Rect getCropBounds(int srcWidth, int srcHeight){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Looper;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
@@ -18,6 +19,7 @@ public class StatusInteractionController{
|
||||
private final String accountID;
|
||||
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
|
||||
|
||||
public StatusInteractionController(String accountID){
|
||||
this.accountID=accountID;
|
||||
@@ -61,6 +63,36 @@ public class StatusInteractionController{
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setBookmarked(Status status, boolean bookmarked){
|
||||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
|
||||
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
|
||||
if(current!=null){
|
||||
current.cancel();
|
||||
}
|
||||
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
error.showToast(MastodonApp.context);
|
||||
status.bookmarked=!bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningBookmarkRequests.put(status.id, req);
|
||||
status.bookmarked=bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setReblogged(Status status, boolean reblogged){
|
||||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
|
||||
@@ -21,18 +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");
|
||||
}
|
||||
}
|
||||
|
||||
public enum Filter{
|
||||
DEFAULT,
|
||||
INCLUDE_REPLIES,
|
||||
PINNED,
|
||||
MEDIA,
|
||||
NO_REBLOGS
|
||||
NO_REBLOGS,
|
||||
OWN_POSTS_AND_REPLIES
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
|
||||
private String maxId;
|
||||
|
||||
public GetBookmarks(String maxID, String minID, int limit){
|
||||
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
|
||||
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
|
||||
String link=httpResponse.header("link");
|
||||
// parsing link header by hand; using a library would be cleaner
|
||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
||||
if(link==null) return;
|
||||
String maxIdEq="max_id=";
|
||||
for(String s : link.split(",")) {
|
||||
if(s.contains("rel=\"next\"")) {
|
||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
||||
int end=s.indexOf('>');
|
||||
if(start<0 || start>end) return;
|
||||
this.maxId=s.substring(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMaxId() {
|
||||
return maxId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
|
||||
public class GetPreferences extends MastodonAPIRequest<Preferences> {
|
||||
public GetPreferences(){
|
||||
super(HttpMethod.GET, "/preferences", Preferences.class);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
|
||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
|
||||
if(followed)
|
||||
setRequestBody(new Request(showReblogs, null));
|
||||
setRequestBody(new Request(showReblogs, notify));
|
||||
else
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.api.AvatarResizedImageRequestBody;
|
||||
import org.joinmastodon.android.api.ContentUriRequestBody;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.ResizedImageRequestBody;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
@@ -39,21 +42,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody(){
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("display_name", displayName)
|
||||
.addFormDataPart("note", bio);
|
||||
|
||||
if(avatar!=null){
|
||||
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null));
|
||||
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new AvatarResizedImageRequestBody(avatar, null));
|
||||
}else if(avatarFile!=null){
|
||||
bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile));
|
||||
bldr.addFormDataPart("avatar", avatarFile.getName(), new AvatarResizedImageRequestBody(Uri.fromFile(avatarFile), null));
|
||||
}
|
||||
if(cover!=null){
|
||||
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null));
|
||||
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ResizedImageRequestBody(cover, 1500*500, null));
|
||||
}else if(coverFile!=null){
|
||||
bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile));
|
||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||
}
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
|
||||
public GetLists() {
|
||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String clientName="Mastodon for Android (Fork)";
|
||||
public String clientName="Mastodos";
|
||||
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
||||
public String scopes=AccountSessionManager.SCOPE;
|
||||
public String website="https://github.com/sk22/mastodon-android-fork";
|
||||
|
||||
@@ -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 EditStatus extends MastodonAPIRequest<Status>{
|
||||
public EditStatus(CreateStatus.Request req, String id){
|
||||
super(HttpMethod.PUT, "/statuses/"+id, Status.class);
|
||||
setRequestBody(req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetAttachmentByID extends MastodonAPIRequest<Attachment>{
|
||||
public GetAttachmentByID(String id){
|
||||
super(HttpMethod.GET, "/media/"+id, Attachment.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
|
||||
if(httpResponse.code()==206)
|
||||
respObj.url="";
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
||||
public GetStatusEditHistory(String id){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException{
|
||||
int i=0;
|
||||
for(Status s:respObj){
|
||||
s.uri="";
|
||||
s.id="fakeID"+i;
|
||||
s.visibility=StatusPrivacy.PUBLIC;
|
||||
s.mentions=Collections.emptyList();
|
||||
s.tags=Collections.emptyList();
|
||||
i++;
|
||||
}
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
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{
|
||||
public String id;
|
||||
public String text;
|
||||
public String spoilerText;
|
||||
}
|
||||
}
|
||||
@@ -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 SetStatusBookmarked extends MastodonAPIRequest<Status>{
|
||||
public SetStatusBookmarked(String id, boolean bookmarked){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/"+(bookmarked ? "bookmark" : "unbookmark"), Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -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 SetStatusPinned extends MastodonAPIRequest<Status>{
|
||||
public SetStatusPinned(String id, boolean pinned){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.io.IOException;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||
private Uri uri;
|
||||
@@ -40,6 +41,18 @@ public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
|
||||
if(respObj.url==null)
|
||||
respObj.url="";
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder builder=new MultipartBody.Builder()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class GetHashtag extends MastodonAPIRequest<Hashtag> {
|
||||
public GetHashtag(String name){
|
||||
super(HttpMethod.GET, "/tags/"+name, Hashtag.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class SetHashtagFollowed extends MastodonAPIRequest<Hashtag>{
|
||||
public SetHashtagFollowed(String name, boolean followed){
|
||||
super(HttpMethod.POST, "/tags/"+name+"/"+(followed ? "follow" : "unfollow"), Hashtag.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) {
|
||||
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,22 @@ package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
@@ -85,11 +92,12 @@ public class AccountSessionManager{
|
||||
domains.add(session.domain.toLowerCase());
|
||||
sessions.put(session.getID(), session);
|
||||
}
|
||||
}catch(IOException|JsonParseException x){
|
||||
}catch(Exception x){
|
||||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
|
||||
@@ -102,6 +110,7 @@ public class AccountSessionManager{
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
public synchronized void writeAccountsFile(){
|
||||
@@ -181,6 +190,7 @@ public class AccountSessionManager{
|
||||
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
|
||||
nm.deleteNotificationChannelGroup(id);
|
||||
}
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -358,7 +368,7 @@ public class AccountSessionManager{
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
instances.put(domain, emojis.instance);
|
||||
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
||||
}catch(IOException|JsonParseException x){
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "Error reading instance info file for "+domain, x);
|
||||
}
|
||||
}
|
||||
@@ -395,6 +405,29 @@ public class AccountSessionManager{
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
private void maybeUpdateShortcuts(){
|
||||
if(Build.VERSION.SDK_INT<26)
|
||||
return;
|
||||
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
|
||||
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
|
||||
// There are no shortcuts, but there are accounts. Add a compose shortcut.
|
||||
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
||||
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
||||
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
|
||||
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
|
||||
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.putExtra("compose", true))
|
||||
.build();
|
||||
sm.setDynamicShortcuts(Collections.singletonList(info));
|
||||
}else if(sessions.isEmpty()){
|
||||
// There are shortcuts, but no accounts. Disable existing shortcuts.
|
||||
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
|
||||
}else{
|
||||
sm.enableShortcuts(Collections.singletonList("compose"));
|
||||
}
|
||||
}
|
||||
|
||||
private static class SessionsStorageWrapper{
|
||||
public List<AccountSession> accounts;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
public class SelfUpdateStateChangedEvent{
|
||||
public final GithubSelfUpdater.UpdateState state;
|
||||
|
||||
public SelfUpdateStateChangedEvent(GithubSelfUpdater.UpdateState state){
|
||||
this.state=state;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusCountersUpdatedEvent{
|
||||
public String id;
|
||||
public int favorites, reblogs, replies;
|
||||
public boolean favorited, reblogged;
|
||||
public long favorites, reblogs, replies;
|
||||
public boolean favorited, reblogged, pinned;
|
||||
|
||||
public StatusCountersUpdatedEvent(Status s){
|
||||
id=s.id;
|
||||
@@ -14,5 +14,6 @@ public class StatusCountersUpdatedEvent{
|
||||
replies=s.repliesCount;
|
||||
favorited=s.favourited;
|
||||
reblogged=s.reblogged;
|
||||
pinned=s.pinned;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class StatusUnpinnedEvent {
|
||||
public final String id;
|
||||
public final String accountID;
|
||||
|
||||
public StatusUnpinnedEvent(String id, String accountID){
|
||||
this.id=id;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusUpdatedEvent{
|
||||
public Status status;
|
||||
|
||||
public StatusUpdatedEvent(Status status){
|
||||
this.status=status;
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -76,6 +78,7 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
||||
return;
|
||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||
// Keep replies to self, discard all other replies
|
||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
@@ -86,4 +89,24 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
}
|
||||
|
||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
|
||||
return;
|
||||
|
||||
Status status=getStatusByID(ev.id);
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
|
||||
if(item==null)
|
||||
return;
|
||||
int index=displayItems.indexOf(item);
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,13 +457,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
status.spoilerRevealed=true;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
|
||||
photo.setRevealed(true);
|
||||
}
|
||||
updateImagesSpoilerState(status, itemID);
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
||||
@@ -472,12 +470,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||
}
|
||||
}
|
||||
holder.rebind();
|
||||
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){
|
||||
updateImagesSpoilerState(status, holder.getItemID());
|
||||
}
|
||||
|
||||
protected void updateImagesSpoilerState(Status status, String itemID){
|
||||
ArrayList<Integer> updatedPositions=new ArrayList<>();
|
||||
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
|
||||
photo.setRevealed(status.spoilerRevealed);
|
||||
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
}
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +579,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return true;
|
||||
}
|
||||
|
||||
public ArrayList<StatusDisplayItem> getDisplayItems(){
|
||||
return displayItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class BookmarksListFragment extends StatusListFragment{
|
||||
|
||||
private String accountID;
|
||||
private Account self;
|
||||
private String lastMaxId=null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
self=session.self;
|
||||
setTitle(R.string.bookmarks);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
|
||||
currentRequest=b.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, b.getMaxId()!=null);
|
||||
lastMaxId=b.getMaxId();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,34 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetHashtag;
|
||||
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends StatusListFragment{
|
||||
private String hashtag;
|
||||
private boolean following;
|
||||
private ImageButton fab;
|
||||
private MenuItem followButton;
|
||||
|
||||
public HashtagTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -27,10 +38,61 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
hashtag=getArguments().getString("hashtag");
|
||||
updateTitle(getArguments().getString("hashtag"));
|
||||
following=getArguments().getBoolean("following", false);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
private void updateTitle(String hashtagName) {
|
||||
hashtag = hashtagName;
|
||||
setTitle('#'+hashtag);
|
||||
}
|
||||
|
||||
private void updateFollowingState(boolean newFollowing) {
|
||||
this.following = newFollowing;
|
||||
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
|
||||
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.hashtag_timeline, menu);
|
||||
followButton = menu.findItem(R.id.follow_hashtag);
|
||||
updateFollowingState(following);
|
||||
|
||||
followButton.setOnMenuItemClickListener(i -> {
|
||||
updateFollowingState(!following);
|
||||
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag i) {
|
||||
updateFollowingState(i.following);
|
||||
Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
updateFollowingState(!following);
|
||||
}
|
||||
}).exec(accountID);
|
||||
return true;
|
||||
});
|
||||
|
||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
updateTitle(hashtag.name);
|
||||
updateFollowingState(hashtag.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)
|
||||
|
||||
@@ -2,13 +2,9 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -18,20 +14,14 @@ import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.parceler.Parcels;
|
||||
@@ -41,15 +31,12 @@ import java.util.ArrayList;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
@@ -141,7 +128,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
}
|
||||
});
|
||||
}
|
||||
}else{
|
||||
}
|
||||
|
||||
return content;
|
||||
@@ -255,9 +241,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(currentTab==R.id.tab_profile)
|
||||
return profileFragment.onBackPressed();
|
||||
if (profileFragment.onBackPressed()) return true;
|
||||
if(currentTab==R.id.tab_search)
|
||||
return searchFragment.onBackPressed();
|
||||
if (searchFragment.onBackPressed()) return true;
|
||||
if (currentTab!=R.id.tab_home) {
|
||||
tabBar.selectTab(R.id.tab_home);
|
||||
onTabSelected(R.id.tab_home);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,12 @@ import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
@@ -33,6 +36,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -70,6 +74,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
loadData();
|
||||
}
|
||||
|
||||
private List<Status> filterPosts(List<Status> items) {
|
||||
return items.stream().filter(i ->
|
||||
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
|
||||
(GlobalUserPreferences.showBoosts || i.reblog == null)
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
@@ -79,7 +90,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
List<Status> filteredItems = filterPosts(result.items);
|
||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
@@ -101,6 +113,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.register(this);
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,7 +151,6 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
}
|
||||
@@ -146,6 +162,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private void loadNewPosts(){
|
||||
if (!GlobalUserPreferences.loadNewPosts) return;
|
||||
dataLoading=true;
|
||||
// The idea here is that we request the timeline such that if there are fewer than `limit` posts,
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
@@ -157,6 +174,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
result = filterPosts(result);
|
||||
if(result.isEmpty() || getActivity()==null)
|
||||
return;
|
||||
Status last=result.get(result.size()-1);
|
||||
@@ -397,4 +415,22 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
scrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.media.MediaRouter;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
|
||||
public class ListTimelineFragment extends StatusListFragment {
|
||||
private String listID;
|
||||
private String listTitle;
|
||||
private ImageButton fab;
|
||||
|
||||
public ListTimelineFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
listID=getArguments().getString("listID");
|
||||
listTitle=getArguments().getString("listTitle");
|
||||
setTitle(listTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown() {
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("prefilledText", listID+' ');
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetFabBottomInset(int inset) {
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
|
||||
private String accountId;
|
||||
|
||||
public ListTimelinesFragment() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
accountId=getArguments().getString("account");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetLists()
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result) {
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter() {
|
||||
return new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ListViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_list_timeline, list);
|
||||
title=findViewById(R.id.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openListTimeline(getActivity(), accountId, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -16,24 +12,21 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
@@ -84,9 +77,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else{
|
||||
}else if(titleItem!=null){
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
|
||||
return Arrays.asList(titleItem, card);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,91 +155,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private int bgColor=UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground);
|
||||
private int borderColor=UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted);
|
||||
private RectF rect=new RectF();
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
int pos=0;
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
pos=holder.getAbsoluteAdapterPosition();
|
||||
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
||||
if(inset){
|
||||
if(rect.isEmpty()){
|
||||
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
||||
}else{
|
||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
||||
}
|
||||
}else if(!rect.isEmpty()){
|
||||
drawInsetBackground(c);
|
||||
rect.setEmpty();
|
||||
}
|
||||
}
|
||||
if(!rect.isEmpty()){
|
||||
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
|
||||
rect.bottom=parent.getHeight()+V.dp(10);
|
||||
}
|
||||
drawInsetBackground(c);
|
||||
rect.setEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawInsetBackground(Canvas c){
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(bgColor);
|
||||
rect.left=V.dp(12);
|
||||
rect.right=list.getWidth()-V.dp(12);
|
||||
rect.inset(V.dp(4), V.dp(4));
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(borderColor);
|
||||
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
||||
boolean inset=sdi.getItem().inset;
|
||||
int pos=holder.getAbsoluteAdapterPosition();
|
||||
if(inset){
|
||||
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||
int pad;
|
||||
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
||||
pad=V.dp(16);
|
||||
else
|
||||
pad=V.dp(12);
|
||||
boolean insetLeft=true, insetRight=true;
|
||||
if(holder instanceof ImageStatusDisplayItem.Holder<?> img){
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout;
|
||||
PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile;
|
||||
// only inset those items that are on the edges of the layout
|
||||
insetLeft=tile.startCol==0;
|
||||
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
|
||||
// inset all items in the bottom row
|
||||
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
|
||||
bottomSiblingInset=false;
|
||||
}
|
||||
if(insetLeft)
|
||||
outRect.left=pad;
|
||||
if(insetRight)
|
||||
outRect.right=pad;
|
||||
if(!topSiblingInset)
|
||||
outRect.top=pad;
|
||||
if(!bottomSiblingInset)
|
||||
outRect.bottom=pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
@@ -268,4 +179,5 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -98,10 +98,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private CoverImageView cover;
|
||||
private View avatarBorder;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
|
||||
private ProgressBarButton actionButton;
|
||||
private ProgressBarButton actionButton, notifyButton;
|
||||
private ViewPager2 pager;
|
||||
private NestedRecyclerScrollView scrollView;
|
||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
|
||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
|
||||
private ProfileAboutFragment aboutFragment;
|
||||
private TabLayout tabbar;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
@@ -109,7 +109,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private float titleTransY;
|
||||
private View postsBtn, followersBtn, followingBtn;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ProgressBar actionProgress;
|
||||
private ProgressBar actionProgress, notifyProgress;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private TextView followsYouView;
|
||||
@@ -181,6 +181,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
postsLabel=content.findViewById(R.id.posts_label);
|
||||
postsBtn=content.findViewById(R.id.posts_btn);
|
||||
actionButton=content.findViewById(R.id.profile_action_btn);
|
||||
notifyButton=content.findViewById(R.id.notify_btn);
|
||||
pager=content.findViewById(R.id.pager);
|
||||
scrollView=content.findViewById(R.id.scroller);
|
||||
tabbar=content.findViewById(R.id.tabbar);
|
||||
@@ -188,6 +189,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
nameEdit=content.findViewById(R.id.name_edit);
|
||||
bioEdit=content.findViewById(R.id.bio_edit);
|
||||
actionProgress=content.findViewById(R.id.action_progress);
|
||||
notifyProgress=content.findViewById(R.id.notify_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
followsYouView=content.findViewById(R.id.follows_you);
|
||||
|
||||
@@ -209,14 +211,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[4];
|
||||
tabViews=new FrameLayout[5];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.profile_posts;
|
||||
case 1 -> R.id.profile_posts_with_replies;
|
||||
case 2 -> R.id.profile_media;
|
||||
case 3 -> R.id.profile_about;
|
||||
case 2 -> R.id.profile_pinned_posts;
|
||||
case 3 -> R.id.profile_media;
|
||||
case 4 -> R.id.profile_about;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -224,7 +227,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setOffscreenPageLimit(5);
|
||||
pager.setAdapter(new ProfilePagerAdapter());
|
||||
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
||||
|
||||
@@ -240,8 +243,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.posts;
|
||||
case 1 -> R.string.posts_and_replies;
|
||||
case 2 -> R.string.media;
|
||||
case 3 -> R.string.profile_about;
|
||||
case 2 -> R.string.pinned_posts;
|
||||
case 3 -> R.string.media;
|
||||
case 4 -> R.string.profile_about;
|
||||
default -> throw new IllegalStateException();
|
||||
});
|
||||
}
|
||||
@@ -256,6 +260,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
});
|
||||
|
||||
actionButton.setOnClickListener(this::onActionButtonClick);
|
||||
notifyButton.setOnClickListener(this::onNotifyButtonClick);
|
||||
avatar.setOnClickListener(this::onAvatarClick);
|
||||
cover.setOnClickListener(this::onCoverClick);
|
||||
refreshLayout.setOnRefreshListener(this);
|
||||
@@ -298,6 +303,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(pinnedPostsFragment.loaded)
|
||||
pinnedPostsFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
}
|
||||
@@ -322,6 +329,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(postsFragment==null){
|
||||
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
||||
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
||||
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
|
||||
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
||||
aboutFragment=new ProfileAboutFragment();
|
||||
aboutFragment.setFields(fields);
|
||||
@@ -402,22 +410,30 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
|
||||
postsFragment.onApplyWindowInsets(childInsets);
|
||||
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
|
||||
pinnedPostsFragment.onApplyWindowInsets(childInsets);
|
||||
mediaFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindHeaderView(){
|
||||
setTitle(account.displayName);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
if(isSelf){
|
||||
ssb.append('@');
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_fluent_lock_closed_20_filled, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
@@ -425,7 +441,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
username.setText('@'+account.acct);
|
||||
// noinspection SetTextI18n
|
||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||
}
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
@@ -437,13 +454,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, account.statusesCount)));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
|
||||
|
||||
UiUtils.loadCustomEmojiInTextView(name);
|
||||
UiUtils.loadCustomEmojiInTextView(bio);
|
||||
|
||||
notifyButton.setVisibility(View.GONE);
|
||||
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
}else{
|
||||
@@ -509,10 +527,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
return;
|
||||
}
|
||||
if(relationship==null)
|
||||
if(relationship==null && !isOwnProfile)
|
||||
return;
|
||||
inflater.inflate(R.menu.profile, menu);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||
if(isOwnProfile){
|
||||
for(int i=0;i<menu.size();i++){
|
||||
MenuItem item=menu.getItem(i);
|
||||
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks);
|
||||
}
|
||||
return;
|
||||
}
|
||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
@@ -529,11 +554,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
if(id==R.id.share) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
}else if(id==R.id.bookmarks) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), BookmarksListFragment.class, args);
|
||||
}else if(id==R.id.mute){
|
||||
confirmToggleMuted();
|
||||
}else if(id==R.id.block){
|
||||
@@ -551,7 +581,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
updateRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
@@ -596,9 +626,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private void updateRelationship(){
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
UiUtils.setRelationshipToActionButton(relationship, notifyButton, true);
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
notifyButton.setSelected(relationship.notifying);
|
||||
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username));
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
@@ -637,8 +672,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return switch(page){
|
||||
case 0 -> postsFragment;
|
||||
case 1 -> postsWithRepliesFragment;
|
||||
case 2 -> mediaFragment;
|
||||
case 3 -> aboutFragment;
|
||||
case 2 -> pinnedPostsFragment;
|
||||
case 3 -> mediaFragment;
|
||||
case 4 -> aboutFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
@@ -664,6 +700,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
|
||||
private void setNotifyProgressVisible(boolean visible){
|
||||
notifyButton.setTextVisible(!visible);
|
||||
notifyProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
notifyButton.setClickable(!visible);
|
||||
}
|
||||
|
||||
private void loadAccountInfoAndEnterEditMode(){
|
||||
if(editModeLoading)
|
||||
return;
|
||||
@@ -699,9 +741,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
pager.setUserInputEnabled(false);
|
||||
actionButton.setText(R.string.done);
|
||||
pager.setCurrentItem(3);
|
||||
pager.setCurrentItem(4);
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
for(int i=0;i<3;i++){
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
|
||||
tabbar.getTabAt(i).view.setEnabled(false);
|
||||
}
|
||||
@@ -742,7 +784,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
for(int i=0;i<3;i++){
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
|
||||
}
|
||||
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
|
||||
@@ -760,7 +802,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
for(int i=0;i<3;i++){
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
tabbar.getTabAt(i).view.setEnabled(true);
|
||||
}
|
||||
pager.setUserInputEnabled(true);
|
||||
@@ -832,6 +874,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return Collections.singletonList(att);
|
||||
}
|
||||
|
||||
private void onNotifyButtonClick(View v) {
|
||||
UiUtils.performToggleAccountNotifications(getActivity(), account, accountID, relationship, actionButton, this::setNotifyProgressVisible, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void onAvatarClick(View v){
|
||||
if(isInEditMode){
|
||||
startImagePicker(AVATAR_RESULT);
|
||||
@@ -937,7 +983,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return loaded ? 4 : 0;
|
||||
return loaded ? tabViews.length : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
@@ -14,15 +15,22 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
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;
|
||||
@@ -31,11 +39,13 @@ import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
@@ -47,7 +57,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -63,6 +72,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
private TextItem checkForUpdateItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -73,6 +83,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||
items.add(new UpdateItem());
|
||||
}
|
||||
}
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
@@ -87,6 +105,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.home_timeline));
|
||||
items.add(new SwitchItem(R.string.settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
GlobalUserPreferences.showReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
|
||||
GlobalUserPreferences.showBoosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
PushSubscription pushSubscription=getPushSubscription();
|
||||
@@ -97,11 +129,15 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_boring));
|
||||
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
|
||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
|
||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||
items.add(checkForUpdateItem);
|
||||
}
|
||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
@@ -131,7 +167,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
// Add 32dp gaps between sections
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
|
||||
outRect.top=V.dp(32);
|
||||
}
|
||||
});
|
||||
@@ -155,6 +191,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
||||
GlobalUserPreferences.theme=theme;
|
||||
GlobalUserPreferences.save();
|
||||
@@ -294,6 +344,30 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING;
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind();
|
||||
|
||||
UpdateItem updateItem = null;
|
||||
if(items.get(0) instanceof UpdateItem item0) {
|
||||
updateItem = item0;
|
||||
} else if (ev.state != GithubSelfUpdater.UpdateState.CHECKING
|
||||
&& ev.state != GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||
updateItem = new UpdateItem();
|
||||
items.add(0, updateItem);
|
||||
list.setAdapter(new SettingsAdapter());
|
||||
}
|
||||
|
||||
if(updateItem != null && list.findViewHolderForAdapterPosition(0) instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(updateItem);
|
||||
}
|
||||
|
||||
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
@@ -358,10 +432,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
private boolean loading;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick){
|
||||
public TextItem(@StringRes int text, Runnable onClick) {
|
||||
this(text, onClick, false);
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
this.loading=loading;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -395,6 +475,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -408,6 +496,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
case 4 -> new TextViewHolder();
|
||||
case 5 -> new HeaderViewHolder(true);
|
||||
case 6 -> new FooterViewHolder();
|
||||
case 7 -> new UpdateViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
@@ -581,14 +670,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
private final ProgressBar progress;
|
||||
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text=(TextView) itemView;
|
||||
text = itemView.findViewById(R.id.text);
|
||||
progress = itemView.findViewById(R.id.progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
text.setText(item.text);
|
||||
progress.animate().alpha(item.loading ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -609,4 +702,75 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
|
||||
private final TextView text;
|
||||
private final Button button;
|
||||
private final ImageButton cancelBtn;
|
||||
private final ProgressBar progress;
|
||||
|
||||
private ObjectAnimator rotationAnimator;
|
||||
private Runnable progressUpdater=this::updateProgress;
|
||||
|
||||
public UpdateViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_update, list);
|
||||
text=findViewById(R.id.text);
|
||||
button=findViewById(R.id.button);
|
||||
cancelBtn=findViewById(R.id.cancel_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
button.setOnClickListener(v->{
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
switch(updater.getState()){
|
||||
case UPDATE_AVAILABLE -> updater.downloadUpdate();
|
||||
case DOWNLOADED -> updater.installUpdate(getActivity());
|
||||
}
|
||||
});
|
||||
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
|
||||
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(UpdateItem item){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
}else{
|
||||
text.setText(getString(R.string.update_ready, info.version));
|
||||
button.setText(R.string.install_update);
|
||||
}
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
rotationAnimator.start();
|
||||
button.setVisibility(View.INVISIBLE);
|
||||
cancelBtn.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
updateProgress();
|
||||
}else{
|
||||
rotationAnimator.cancel();
|
||||
button.setVisibility(View.VISIBLE);
|
||||
cancelBtn.setVisibility(View.GONE);
|
||||
progress.setVisibility(View.GONE);
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
|
||||
return;
|
||||
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
progress.setProgress(value, true);
|
||||
else
|
||||
progress.setProgress(value);
|
||||
progress.postDelayed(progressUpdater, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
private String id;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
id=getArguments().getString("id");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.edit_history);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetStatusEditHistory(id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
String action="";
|
||||
if(idx==data.size()-1){
|
||||
action=getString(R.string.edit_original_post);
|
||||
}else{
|
||||
enum StatusEditChangeType{
|
||||
TEXT_CHANGED,
|
||||
SPOILER_ADDED,
|
||||
SPOILER_REMOVED,
|
||||
SPOILER_CHANGED,
|
||||
POLL_ADDED,
|
||||
POLL_REMOVED,
|
||||
POLL_CHANGED,
|
||||
MEDIA_ADDED,
|
||||
MEDIA_REMOVED,
|
||||
MEDIA_REORDERED,
|
||||
MARKED_SENSITIVE,
|
||||
MARKED_NOT_SENSITIVE
|
||||
}
|
||||
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
|
||||
Status prev=data.get(idx+1);
|
||||
|
||||
if(!Objects.equals(s.content, prev.content)){
|
||||
changes.add(StatusEditChangeType.TEXT_CHANGED);
|
||||
}
|
||||
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
|
||||
if(s.spoilerText==null){
|
||||
changes.add(StatusEditChangeType.SPOILER_REMOVED);
|
||||
}else if(prev.spoilerText==null){
|
||||
changes.add(StatusEditChangeType.SPOILER_ADDED);
|
||||
}else{
|
||||
changes.add(StatusEditChangeType.SPOILER_CHANGED);
|
||||
}
|
||||
}
|
||||
if(s.poll!=null || prev.poll!=null){
|
||||
if(s.poll==null){
|
||||
changes.add(StatusEditChangeType.POLL_REMOVED);
|
||||
}else if(prev.poll==null){
|
||||
changes.add(StatusEditChangeType.POLL_ADDED);
|
||||
}else if(!s.poll.id.equals(prev.poll.id)){
|
||||
changes.add(StatusEditChangeType.POLL_CHANGED);
|
||||
}
|
||||
}
|
||||
List<String> newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
|
||||
List<String> prevAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
|
||||
boolean addedOrRemoved=false;
|
||||
if(!newAttachmentIDs.containsAll(prevAttachmentIDs)){
|
||||
changes.add(StatusEditChangeType.MEDIA_REMOVED);
|
||||
addedOrRemoved=true;
|
||||
}
|
||||
if(!prevAttachmentIDs.containsAll(newAttachmentIDs)){
|
||||
changes.add(StatusEditChangeType.MEDIA_ADDED);
|
||||
addedOrRemoved=true;
|
||||
}
|
||||
if(!addedOrRemoved && !newAttachmentIDs.equals(prevAttachmentIDs)){
|
||||
changes.add(StatusEditChangeType.MEDIA_REORDERED);
|
||||
}
|
||||
if(s.sensitive && !prev.sensitive){
|
||||
changes.add(StatusEditChangeType.MARKED_SENSITIVE);
|
||||
}else if(prev.sensitive && !s.sensitive){
|
||||
changes.add(StatusEditChangeType.MARKED_NOT_SENSITIVE);
|
||||
}
|
||||
|
||||
if(changes.size()==1){
|
||||
action=getString(switch(changes.iterator().next()){
|
||||
case TEXT_CHANGED -> R.string.edit_text_edited;
|
||||
case SPOILER_ADDED -> R.string.edit_spoiler_added;
|
||||
case SPOILER_REMOVED -> R.string.edit_spoiler_removed;
|
||||
case SPOILER_CHANGED -> R.string.edit_spoiler_edited;
|
||||
case POLL_ADDED -> R.string.edit_poll_added;
|
||||
case POLL_REMOVED -> R.string.edit_poll_removed;
|
||||
case POLL_CHANGED -> R.string.edit_poll_edited;
|
||||
case MEDIA_ADDED -> R.string.edit_media_added;
|
||||
case MEDIA_REMOVED -> R.string.edit_media_removed;
|
||||
case MEDIA_REORDERED -> R.string.edit_media_reordered;
|
||||
case MARKED_SENSITIVE -> R.string.edit_marked_sensitive;
|
||||
case MARKED_NOT_SENSITIVE -> R.string.edit_marked_not_sensitive;
|
||||
});
|
||||
}else{
|
||||
action=getString(R.string.edit_multiple_changed);
|
||||
}
|
||||
}
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,14 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -61,6 +62,59 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){}
|
||||
|
||||
protected void onStatusUpdated(StatusUpdatedEvent ev){
|
||||
ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
|
||||
for(int i=0;i<data.size();i++){
|
||||
Status s=data.get(i);
|
||||
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
|
||||
s.reblog=ev.status;
|
||||
statusesForDisplayItems.add(s);
|
||||
}else if(s.id.equals(ev.status.id)){
|
||||
data.set(i, ev.status);
|
||||
statusesForDisplayItems.add(ev.status);
|
||||
}
|
||||
}
|
||||
for(int i=0;i<preloadedData.size();i++){
|
||||
Status s=preloadedData.get(i);
|
||||
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
|
||||
s.reblog=ev.status;
|
||||
}else if(s.id.equals(ev.status.id)){
|
||||
preloadedData.set(i, ev.status);
|
||||
}
|
||||
}
|
||||
|
||||
if(statusesForDisplayItems.isEmpty())
|
||||
return;
|
||||
|
||||
for(Status s:statusesForDisplayItems){
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.parentID.equals(s.id)){
|
||||
int start=i;
|
||||
for(;i<displayItems.size();i++){
|
||||
if(!displayItems.get(i).parentID.equals(s.id))
|
||||
break;
|
||||
}
|
||||
List<StatusDisplayItem> postItems=displayItems.subList(start, i);
|
||||
postItems.clear();
|
||||
postItems.addAll(buildDisplayItems(s));
|
||||
int oldSize=i-start, newSize=postItems.size();
|
||||
if(oldSize==newSize){
|
||||
adapter.notifyItemRangeChanged(start, newSize);
|
||||
}else if(oldSize<newSize){
|
||||
adapter.notifyItemRangeChanged(start, oldSize);
|
||||
adapter.notifyItemRangeInserted(start+oldSize, newSize-oldSize);
|
||||
}else{
|
||||
adapter.notifyItemRangeChanged(start, newSize);
|
||||
adapter.notifyItemRangeRemoved(start+newSize, oldSize-newSize);
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Status getContentStatusByID(String id){
|
||||
Status s=getStatusByID(id);
|
||||
return s==null ? null : s.getContentStatus();
|
||||
@@ -113,10 +167,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
return;
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
|
||||
if(item==null)
|
||||
int index=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
if(ev.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(index==-1)
|
||||
return;
|
||||
int index=displayItems.indexOf(item);
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||
@@ -131,6 +190,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
StatusListFragment.this.onStatusCreated(ev);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusUpdated(StatusUpdatedEvent ev){
|
||||
StatusListFragment.this.onStatusUpdated(ev);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPollUpdated(PollUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ThreadFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||
if(s==mainStatus){
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
for(StatusDisplayItem item:items){
|
||||
if(item instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
|
||||
@@ -353,7 +353,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
bindRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, account.followersCount, account.followersCount));
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_following, account.followingCount, account.followingCount));
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,7 +11,7 @@ public class StatusFavoritesListFragment extends StatusRelatedAccountListFragmen
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(getResources().getQuantityString(R.plurals.x_favorites, status.favouritesCount, status.favouritesCount));
|
||||
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,7 +11,7 @@ public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, status.reblogsCount, status.reblogsCount));
|
||||
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -220,9 +220,9 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
|
||||
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, item.account.statusesCount)));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||
relationship=relationships.get(item.account.id);
|
||||
if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -52,6 +53,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private SearchFragment searchFragment;
|
||||
private LocalTimelineFragment localTimelineFragment;
|
||||
private FederatedTimelineFragment federatedTimelineFragment;
|
||||
private ListTimelinesFragment listTimelinesFragment;
|
||||
|
||||
private String accountID;
|
||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||
@@ -73,7 +75,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[6];
|
||||
tabViews=new FrameLayout[7];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
@@ -83,6 +85,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
case 3 -> R.id.discover_posts;
|
||||
case 4 -> R.id.discover_news;
|
||||
case 5 -> R.id.discover_users;
|
||||
case 6 -> R.id.discover_lists;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -131,6 +134,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
federatedTimelineFragment=new FederatedTimelineFragment();
|
||||
federatedTimelineFragment.setArguments(args);
|
||||
|
||||
listTimelinesFragment=new ListTimelinesFragment();
|
||||
listTimelinesFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.discover_posts, postsFragment)
|
||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||
@@ -138,6 +144,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||
.add(R.id.discover_news, newsFragment)
|
||||
.add(R.id.discover_users, accountsFragment)
|
||||
.add(R.id.discover_lists, listTimelinesFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -151,12 +158,25 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
case 3 -> R.string.posts;
|
||||
case 4 -> R.string.news;
|
||||
case 5 -> R.string.for_you;
|
||||
case 6 -> R.string.list_timelines;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
}
|
||||
});
|
||||
tabLayoutMediator.attach();
|
||||
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab){}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab){}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
searchEdit=view.findViewById(R.id.search_edit);
|
||||
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
|
||||
@@ -267,6 +287,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
case 3 -> postsFragment;
|
||||
case 4 -> newsFragment;
|
||||
case 5 -> accountsFragment;
|
||||
case 6 -> listTimelinesFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
SearchResult res=getResultByID(id);
|
||||
if(res==null)
|
||||
return;
|
||||
switch(res.type){
|
||||
case ACCOUNT -> {
|
||||
Bundle args=new Bundle();
|
||||
@@ -98,7 +100,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
|
||||
case STATUS -> {
|
||||
Status status=res.status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
@@ -205,7 +207,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){
|
||||
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class GoogleMadeMeAddThisFragment extends AppKitFragment{
|
||||
private UsableRecyclerView list;
|
||||
private MergeRecyclerAdapter adapter;
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private Instance instance;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private Call currentRequest;
|
||||
private ItemsAdapter itemsAdapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
|
||||
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
|
||||
loadServerPrivacyPolicy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
|
||||
|
||||
list=view.findViewById(R.id.list);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
|
||||
TextView title=headerView.findViewById(R.id.title);
|
||||
TextView subtitle=headerView.findViewById(R.id.subtitle);
|
||||
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
|
||||
title.setText(R.string.privacy_policy_title);
|
||||
subtitle.setText(R.string.privacy_policy_subtitle);
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.setSelector(null);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildViewHolder(view) instanceof ItemViewHolder){
|
||||
outRect.left=outRect.right=V.dp(18.5f);
|
||||
outRect.top=V.dp(16);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), SignupFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadServerPrivacyPolicy(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://"+instance.uri+"/terms")
|
||||
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
|
||||
.build();
|
||||
currentRequest=MastodonAPIController.getHttpClient().newCall(req);
|
||||
currentRequest.enqueue(new Callback(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
currentRequest=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
currentRequest=null;
|
||||
try(ResponseBody body=response.body()){
|
||||
if(!response.isSuccessful())
|
||||
return;
|
||||
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
|
||||
final Item item=new Item(doc.title(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
|
||||
Activity activity=getActivity();
|
||||
if(activity!=null){
|
||||
activity.runOnUiThread(()->{
|
||||
items.add(item);
|
||||
itemsAdapter.notifyItemInserted(items.size()-1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
|
||||
private final TextView domain, title;
|
||||
private final ImageView favicon;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_privacy_policy_link, list);
|
||||
domain=findViewById(R.id.domain);
|
||||
title=findViewById(R.id.title);
|
||||
favicon=findViewById(R.id.favicon);
|
||||
itemView.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
itemView.setClipToOutline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item item){
|
||||
domain.setText(item.domain);
|
||||
title.setText(item.title);
|
||||
|
||||
ViewImageLoader.load(favicon, null, new UrlImageLoaderRequest(item.faviconUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
UiUtils.launchWebBrowser(getActivity(), item.url);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Item{
|
||||
public String title, domain, url, faviconUrl;
|
||||
|
||||
public Item(String title, String domain, String url, String faviconUrl){
|
||||
this.title=title;
|
||||
this.domain=domain;
|
||||
this.url=url;
|
||||
this.faviconUrl=faviconUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class InstanceRulesFragment extends AppKitFragment{
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), SignupFragment.class, args);
|
||||
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -200,7 +200,6 @@ public class SignupFragment extends AppKitFragment{
|
||||
@Override
|
||||
public void onSuccess(Token result){
|
||||
progressDialog.dismiss();
|
||||
progressDialog=null;
|
||||
Account fakeAccount=new Account();
|
||||
fakeAccount.acct=fakeAccount.username=username;
|
||||
fakeAccount.id="tmp"+System.currentTimeMillis();
|
||||
@@ -238,7 +237,6 @@ public class SignupFragment extends AppKitFragment{
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
progressDialog.dismiss();
|
||||
progressDialog=null;
|
||||
}
|
||||
})
|
||||
.exec(instance.uri, apiToken);
|
||||
@@ -255,9 +253,11 @@ public class SignupFragment extends AppKitFragment{
|
||||
}
|
||||
|
||||
private void showProgressDialog(){
|
||||
progressDialog=new ProgressDialog(getActivity());
|
||||
progressDialog.setMessage(getString(R.string.loading));
|
||||
progressDialog.setCancelable(false);
|
||||
if(progressDialog==null){
|
||||
progressDialog=new ProgressDialog(getActivity());
|
||||
progressDialog.setMessage(getString(R.string.loading));
|
||||
progressDialog.setCancelable(false);
|
||||
}
|
||||
progressDialog.show();
|
||||
}
|
||||
|
||||
@@ -280,7 +280,6 @@ public class SignupFragment extends AppKitFragment{
|
||||
if(submitAfterGettingToken){
|
||||
submitAfterGettingToken=false;
|
||||
progressDialog.dismiss();
|
||||
progressDialog=null;
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}
|
||||
@@ -307,7 +306,6 @@ public class SignupFragment extends AppKitFragment{
|
||||
if(submitAfterGettingToken){
|
||||
submitAfterGettingToken=false;
|
||||
progressDialog.dismiss();
|
||||
progressDialog=null;
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.NO_REBLOGS)
|
||||
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
@@ -102,6 +102,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
else
|
||||
selectedIDs.add(id);
|
||||
list.invalidate();
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -125,7 +125,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private void onUnfollowClick(){
|
||||
new SetAccountFollowed(reportAccount.id, false, false)
|
||||
new SetAccountFollowed(reportAccount.id, false, false, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
|
||||
@@ -96,15 +96,15 @@ public class Account extends BaseModel{
|
||||
/**
|
||||
* How many statuses are attached to this account.
|
||||
*/
|
||||
public int statusesCount;
|
||||
public long statusesCount;
|
||||
/**
|
||||
* The reported followers of this profile.
|
||||
*/
|
||||
public int followersCount;
|
||||
public long followersCount;
|
||||
/**
|
||||
* The reported follows of this profile.
|
||||
*/
|
||||
public int followingCount;
|
||||
public long followingCount;
|
||||
|
||||
// Optional attributes
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum ExpandMedia {
|
||||
@SerializedName("default")
|
||||
DEFAULT,
|
||||
@SerializedName("show_all")
|
||||
SHOW_ALL,
|
||||
@SerializedName("hide_all")
|
||||
HIDE_ALL;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public class Hashtag extends BaseModel{
|
||||
public String name;
|
||||
@RequiredField
|
||||
public String url;
|
||||
public boolean following;
|
||||
public List<History> history;
|
||||
|
||||
@Override
|
||||
@@ -18,6 +19,7 @@ public class Hashtag extends BaseModel{
|
||||
return "Hashtag{"+
|
||||
"name='"+name+'\''+
|
||||
", url='"+url+'\''+
|
||||
", following="+following+
|
||||
", history="+history+
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
@Parcel
|
||||
public class ListTimeline extends BaseModel {
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String title;
|
||||
@RequiredField
|
||||
public RepliesPolicy repliesPolicy;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "List{" +
|
||||
"id='" + id + '\'' +
|
||||
", title='" + title + '\'' +
|
||||
", repliesPolicy=" + repliesPolicy +
|
||||
'}';
|
||||
}
|
||||
|
||||
public enum RepliesPolicy{
|
||||
@SerializedName("followed")
|
||||
FOLLOWED,
|
||||
@SerializedName("list")
|
||||
LIST,
|
||||
@SerializedName("none")
|
||||
NONE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Preferred common behaviors to be shared across clients.
|
||||
*/
|
||||
public class Preferences extends BaseModel {
|
||||
/**
|
||||
* Default visibility for new posts
|
||||
*/
|
||||
@SerializedName("posting:default:visibility")
|
||||
public StatusPrivacy postingDefaultVisibility;
|
||||
|
||||
/**
|
||||
* Default sensitivity flag for new posts
|
||||
*/
|
||||
@SerializedName("posting:default:sensitive")
|
||||
public boolean postingDefaultSensitive;
|
||||
|
||||
/**
|
||||
* Default language for new posts
|
||||
*/
|
||||
@SerializedName("posting:default:language")
|
||||
public String postingDefaultLanguage;
|
||||
|
||||
/**
|
||||
* Whether media attachments should be automatically displayed or blurred/hidden.
|
||||
*/
|
||||
@SerializedName("reading:expand:media")
|
||||
public ExpandMedia readingExpandMedia;
|
||||
|
||||
/**
|
||||
* Whether CWs should be expanded by default.
|
||||
*/
|
||||
@SerializedName("reading:expand:spoilers")
|
||||
public boolean readingExpandSpoilers;
|
||||
}
|
||||
@@ -34,9 +34,10 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public List<Hashtag> tags;
|
||||
@RequiredField
|
||||
public List<Emoji> emojis;
|
||||
public int reblogsCount;
|
||||
public int favouritesCount;
|
||||
public int repliesCount;
|
||||
public long reblogsCount;
|
||||
public long favouritesCount;
|
||||
public long repliesCount;
|
||||
public Instant editedAt;
|
||||
|
||||
public String url;
|
||||
public String inReplyToId;
|
||||
@@ -126,6 +127,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
repliesCount=ev.replies;
|
||||
favourited=ev.favorited;
|
||||
reblogged=ev.reblogged;
|
||||
pinned=ev.pinned;
|
||||
}
|
||||
|
||||
public Status getContentStatus(){
|
||||
|
||||
@@ -4,11 +4,25 @@ import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum StatusPrivacy{
|
||||
@SerializedName("public")
|
||||
PUBLIC,
|
||||
PUBLIC(0),
|
||||
@SerializedName("unlisted")
|
||||
UNLISTED,
|
||||
UNLISTED(1),
|
||||
@SerializedName("private")
|
||||
PRIVATE,
|
||||
PRIVATE(2),
|
||||
@SerializedName("direct")
|
||||
DIRECT;
|
||||
DIRECT(3);
|
||||
|
||||
private int privacy;
|
||||
|
||||
StatusPrivacy(int privacy) {
|
||||
this.privacy = privacy;
|
||||
}
|
||||
|
||||
public boolean isLessVisibleThan(StatusPrivacy other) {
|
||||
return privacy > other.getPrivacy();
|
||||
}
|
||||
|
||||
public int getPrivacy() {
|
||||
return privacy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ public class ComposeAutocompleteViewController{
|
||||
.map(WrappedEmoji::new)
|
||||
.collect(Collectors.toList());
|
||||
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +187,7 @@ public class ComposeAutocompleteViewController{
|
||||
List<WrappedAccount> oldList=users;
|
||||
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
|
||||
imgLoader.updateImages();
|
||||
if(listIsHidden){
|
||||
listIsHidden=false;
|
||||
V.setVisibilityAnimated(list, View.VISIBLE);
|
||||
@@ -210,6 +212,7 @@ public class ComposeAutocompleteViewController{
|
||||
List<Hashtag> oldList=hashtags;
|
||||
hashtags=result.hashtags;
|
||||
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
|
||||
imgLoader.updateImages();
|
||||
if(listIsHidden){
|
||||
listIsHidden=false;
|
||||
V.setVisibilityAnimated(list, View.VISIBLE);
|
||||
|
||||
@@ -115,9 +115,9 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
|
||||
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, Math.min(999, item.account.statusesCount)));
|
||||
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||
relationship=item.parentFragment.getRelationship(item.account.id);
|
||||
if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
|
||||
@@ -92,7 +92,9 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=formatDuration(seconds);
|
||||
time.getLayoutParams().width=(int)Math.ceil(time.getPaint().measureText("-"+duration));
|
||||
// Some fonts (not Roboto) have different-width digits. 0 is supposedly the widest.
|
||||
time.getLayoutParams().width=(int)Math.ceil(Math.max(time.getPaint().measureText("-"+duration),
|
||||
time.getPaint().measureText("-"+duration.replaceAll("\\d", "0"))));
|
||||
time.setText(duration);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -8,10 +9,13 @@ import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusEditHistoryFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
|
||||
@@ -43,46 +47,59 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<ExtendedFooterStatusDisplayItem>{
|
||||
private final TextView reblogs, favorites, time;
|
||||
private final View buttonsView;
|
||||
private final TextView time;
|
||||
private final Button favorites, reblogs, editHistory, applicationName;
|
||||
private final ImageView visibility;
|
||||
private final Context context;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_extended_footer, parent);
|
||||
this.context = context;
|
||||
reblogs=findViewById(R.id.reblogs);
|
||||
favorites=findViewById(R.id.favorites);
|
||||
editHistory=findViewById(R.id.edit_history);
|
||||
applicationName=findViewById(R.id.application_name);
|
||||
visibility=findViewById(R.id.visibility);
|
||||
time=findViewById(R.id.timestamp);
|
||||
buttonsView=findViewById(R.id.button_bar);
|
||||
|
||||
reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class));
|
||||
favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class));
|
||||
editHistory.setOnClickListener(v->startEditHistoryFragment());
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(ExtendedFooterStatusDisplayItem item){
|
||||
Status s=item.status;
|
||||
if(s.favouritesCount>0){
|
||||
favorites.setVisibility(View.VISIBLE);
|
||||
favorites.setText(getFormattedPlural(R.plurals.x_favorites, s.favouritesCount));
|
||||
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
||||
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int)(s.reblogsCount%1000), s.reblogsCount));
|
||||
if(s.editedAt!=null){
|
||||
editHistory.setVisibility(View.VISIBLE);
|
||||
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));
|
||||
}else{
|
||||
favorites.setVisibility(View.GONE);
|
||||
}
|
||||
if(s.reblogsCount>0){
|
||||
reblogs.setVisibility(View.VISIBLE);
|
||||
reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, s.reblogsCount));
|
||||
}else{
|
||||
reblogs.setVisibility(View.GONE);
|
||||
}
|
||||
if(s.favouritesCount==0 && s.reblogsCount==0){
|
||||
buttonsView.setVisibility(View.GONE);
|
||||
}else{
|
||||
buttonsView.setVisibility(View.VISIBLE);
|
||||
editHistory.setVisibility(View.GONE);
|
||||
}
|
||||
String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault()));
|
||||
if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){
|
||||
time.setText(item.parentFragment.getString(R.string.timestamp_via_app, timeStr, item.status.application.name));
|
||||
}else{
|
||||
|
||||
if (item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)) {
|
||||
time.setText(item.parentFragment.getString(R.string.timestamp_via_app, timeStr, ""));
|
||||
applicationName.setText(item.status.application.name);
|
||||
if (item.status.application.website != null && item.status.application.website.toLowerCase().startsWith("https://")) {
|
||||
applicationName.setOnClickListener(e -> UiUtils.openURL(context, null, item.status.application.website));
|
||||
} else {
|
||||
applicationName.setEnabled(false);
|
||||
}
|
||||
} else {
|
||||
time.setText(timeStr);
|
||||
applicationName.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
visibility.setImageResource(switch (s.visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case DIRECT -> R.drawable.ic_at_symbol;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,5 +125,12 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
Nav.go(item.parentFragment.getActivity(), cls, args);
|
||||
}
|
||||
|
||||
private void startEditHistoryFragment(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("id", item.status.id);
|
||||
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||
private final TextView reply, boost, favorite;
|
||||
private final TextView reply, boost, favorite, bookmark;
|
||||
private final ImageView share;
|
||||
|
||||
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
||||
@@ -60,22 +60,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
reply=findViewById(R.id.reply);
|
||||
boost=findViewById(R.id.boost);
|
||||
favorite=findViewById(R.id.favorite);
|
||||
bookmark=findViewById(R.id.bookmark);
|
||||
share=findViewById(R.id.share);
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
|
||||
}
|
||||
View reply=findViewById(R.id.reply_btn);
|
||||
View boost=findViewById(R.id.boost_btn);
|
||||
View favorite=findViewById(R.id.favorite_btn);
|
||||
View share=findViewById(R.id.share_btn);
|
||||
View bookmark=findViewById(R.id.bookmark_btn);
|
||||
reply.setOnClickListener(this::onReplyClick);
|
||||
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
boost.setOnClickListener(this::onBoostClick);
|
||||
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
favorite.setOnClickListener(this::onFavoriteClick);
|
||||
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
bookmark.setOnClickListener(this::onBookmarkClick);
|
||||
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
share.setOnClickListener(this::onShareClick);
|
||||
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
}
|
||||
@@ -87,11 +92,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
bindButton(favorite, item.status.favouritesCount);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
favorite.setSelected(item.status.favourited);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|
||||
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
|
||||
}
|
||||
|
||||
private void bindButton(TextView btn, int count){
|
||||
private void bindButton(TextView btn, long count){
|
||||
if(count>0 && !item.hideCounts){
|
||||
btn.setText(DecimalFormat.getIntegerInstance().format(count));
|
||||
btn.setCompoundDrawablePadding(V.dp(8));
|
||||
@@ -120,6 +126,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
bindButton(favorite, item.status.favouritesCount);
|
||||
}
|
||||
|
||||
private void onBookmarkClick(View v){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
}
|
||||
|
||||
private void onShareClick(View v){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
@@ -134,6 +145,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
return R.string.button_reblog;
|
||||
if(id==R.id.favorite_btn)
|
||||
return R.string.button_favorite;
|
||||
if(id==R.id.bookmark_btn)
|
||||
return R.string.button_bookmark;
|
||||
if(id==R.id.share_btn)
|
||||
return R.string.button_share;
|
||||
return 0;
|
||||
|
||||
@@ -21,8 +21,10 @@ import android.widget.Toast;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -135,8 +137,36 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||
Account account=item.user;
|
||||
int id=menuItem.getItemId();
|
||||
if(id==R.id.delete){
|
||||
if(id==R.id.edit){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("editStatus", Parcels.wrap(item.status));
|
||||
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}else{
|
||||
new GetStatusSourceText(item.status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(GetStatusSourceText.Response result){
|
||||
args.putString("sourceText", result.text);
|
||||
args.putString("sourceSpoiler", result.spoilerText);
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(item.parentFragment.getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(item.parentFragment.getActivity(), R.string.loading, true)
|
||||
.exec(item.parentFragment.getAccountID());
|
||||
}
|
||||
}else if(id==R.id.delete){
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}else if(id==R.id.delete_and_redraft) {
|
||||
UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}else if(id==R.id.pin || id==R.id.unpin){
|
||||
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
|
||||
}else if(id==R.id.block){
|
||||
@@ -175,7 +205,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.user.acct);
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
if(item.status==null || item.status.editedAt==null)
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
else
|
||||
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
||||
if(item.hasVisibilityToggle){
|
||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||
@@ -249,7 +282,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
Account account=item.user;
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
|
||||
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
|
||||
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
@@ -114,7 +115,7 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
if(addFooter){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
if(status.hasGapAfter)
|
||||
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
|
||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
int i=1;
|
||||
@@ -157,7 +158,7 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
|
||||
public Holder(Context context, int layout, ViewGroup parent){
|
||||
super(context, layout, parent);
|
||||
super(context, layout, parent);
|
||||
}
|
||||
|
||||
public String getItemID(){
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
|
||||
@@ -20,7 +21,8 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence text;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), spoilerEmojiHelper;
|
||||
private CharSequence parsedSpoilerText;
|
||||
public boolean textSelectable;
|
||||
public final Status status;
|
||||
|
||||
@@ -29,6 +31,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
this.text=text;
|
||||
this.status=status;
|
||||
emojiHelper.setText(text);
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
spoilerEmojiHelper=new CustomEmojiHelper();
|
||||
spoilerEmojiHelper.setText(parsedSpoilerText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,11 +45,15 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||
return spoilerEmojiHelper.getImageCount();
|
||||
return emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||
return spoilerEmojiHelper.getImageRequest(index);
|
||||
return emojiHelper.getImageRequest(index);
|
||||
}
|
||||
|
||||
@@ -65,14 +76,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
text.setTextIsSelectable(item.textSelectable);
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
||||
spoilerTitle.setText(item.status.spoilerText);
|
||||
spoilerTitle.setText(item.parsedSpoilerText);
|
||||
if(item.status.spoilerRevealed){
|
||||
spoilerOverlay.setVisibility(View.GONE);
|
||||
text.setVisibility(View.VISIBLE);
|
||||
itemView.setClickable(false);
|
||||
}else{
|
||||
spoilerOverlay.setVisibility(View.VISIBLE);
|
||||
text.setVisibility(View.INVISIBLE);
|
||||
text.setVisibility(View.GONE);
|
||||
itemView.setClickable(true);
|
||||
}
|
||||
}else{
|
||||
@@ -84,8 +95,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
item.emojiHelper.setImageDrawable(index, image);
|
||||
getEmojiHelper().setImageDrawable(index, image);
|
||||
text.invalidate();
|
||||
spoilerTitle.invalidate();
|
||||
if(image instanceof Animatable){
|
||||
((Animatable) image).start();
|
||||
if(image instanceof MovieDrawable)
|
||||
@@ -95,8 +107,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
item.emojiHelper.setImageDrawable(index, null);
|
||||
getEmojiHelper().setImageDrawable(index, null);
|
||||
text.invalidate();
|
||||
}
|
||||
|
||||
private CustomEmojiHelper getEmojiHelper(){
|
||||
return item.spoilerEmojiHelper!=null && !item.status.spoilerRevealed ? item.spoilerEmojiHelper : item.emojiHelper;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
/**
|
||||
* A span to mark character ranges that should be deleted when copied to the clipboard.
|
||||
* Works with {@link org.joinmastodon.android.ui.views.LinkedTextView}.
|
||||
*/
|
||||
public class DeleteWhenCopiedSpan{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user