Compare commits

...

669 Commits

Author SHA1 Message Date
Grishka
a9ef1f9d47 Someone else's code throwing too many exceptions again, yay 2024-07-10 02:48:33 +03:00
Grishka
0182763b58 Fixes 2024-07-10 01:01:31 +03:00
Grishka
761cadbcc7 Prepare new release 2024-07-10 00:43:46 +03:00
Grishka
ff7b6e4564 Merge branch 'donations' 2024-07-10 00:39:43 +03:00
Grishka
db84317f7f Merge branch 'master' into donations
# Conflicts:
#	mastodon/build.gradle
2024-07-03 01:30:43 +03:00
Grishka
72df72228f Merge branch 'l10n_master' 2024-07-03 01:18:18 +03:00
Grishka
3e26bd2f52 Crash fixes 2024-07-03 01:17:47 +03:00
Grishka
f241d74e27 Bump version 2024-07-01 01:18:03 +03:00
Grishka
ff0f3b414b Rename method 2024-07-01 01:13:32 +03:00
Grishka
188da6dbf7 Merge branch 'master' into donations
# Conflicts:
#	mastodon/build.gradle
2024-07-01 00:57:35 +03:00
Grishka
f9fd62db09 Fix for a crash that resulted from a previous crash fix 2024-07-01 00:56:39 +03:00
Eugen Rochko
4795a5ff6e New translations strings.xml (Lithuanian) 2024-06-30 23:09:16 +02:00
Eugen Rochko
4da52e4797 New translations strings.xml (Ukrainian) 2024-06-30 16:42:53 +02:00
Eugen Rochko
46bc0ccbeb New translations strings.xml (Ukrainian) 2024-06-30 15:46:35 +02:00
Grishka
75f1caf022 Bump version 2024-06-29 22:13:18 +03:00
Grishka
6f6d2e1e08 Merge branch 'l10n_master' 2024-06-29 22:12:21 +03:00
Grishka
027b873f13 Merge branch 'master' into donations 2024-06-29 09:13:29 +03:00
Grishka
482b7d5e7d Don't show "this post is old" for own posts 2024-06-29 09:13:05 +03:00
Grishka
1e729d97a0 Crash fixes 2024-06-29 09:11:03 +03:00
Eugen Rochko
357e348681 New translations strings.xml (Hungarian) 2024-06-27 08:23:50 +02:00
Grishka
65c1b4def0 oops 2024-06-26 20:27:10 +03:00
Grishka
461eac8932 Support multiple authors in link cards (mastodon/mastodon#30846) 2024-06-26 20:24:32 +03:00
Grishka
9a679de9c9 Fix donations and update AppCenter SDK 2024-06-25 17:18:40 +03:00
Eugen Rochko
12d4ec2129 New translations strings.xml (Armenian) 2024-06-25 12:22:04 +02:00
Eugen Rochko
26f9737e5e New translations strings.xml (Armenian) 2024-06-25 10:47:07 +02:00
Eugen Rochko
771740ca93 New translations strings.xml (Persian) 2024-06-23 14:55:50 +02:00
Eugen Rochko
ed070118d6 New translations strings.xml (Portuguese, Brazilian) 2024-06-23 06:51:10 +02:00
Grishka
88c0985908 Merge branch 'master' into donations
# Conflicts:
#	mastodon/build.gradle
2024-06-23 04:57:02 +03:00
Grishka
e7094beab9 More crash fixes 2024-06-23 04:54:43 +03:00
Grishka
501a2715be Crash fixes
fixes #854
2024-06-23 04:45:56 +03:00
Eugen Rochko
bc2380dfb1 New translations full_description.txt (Interlingua) 2024-06-22 23:14:10 +02:00
Eugen Rochko
4b584af0a9 New translations full_description.txt (French) 2024-06-22 23:14:10 +02:00
Eugen Rochko
4bc1083939 New translations strings.xml (Interlingua) 2024-06-22 23:14:09 +02:00
Eugen Rochko
b34b5032ee New translations strings.xml (Interlingua) 2024-06-22 22:17:44 +02:00
Eugen Rochko
d10b70cc62 New translations strings.xml (Interlingua) 2024-06-22 20:47:15 +02:00
Eugen Rochko
a8ba45c14b New translations strings.xml (Dutch) 2024-06-22 20:47:14 +02:00
Grishka
a50a2446a7 Update targetSDK to 34 2024-06-22 02:48:37 +03:00
Grishka
4e2e3fb4e2 Add donation options to settings 2024-06-22 02:33:06 +03:00
Eugen Rochko
9d34f0896f New translations strings.xml (Dutch) 2024-06-21 04:46:43 +02:00
Eugen Rochko
e492aca885 New translations strings.xml (Interlingua) 2024-06-21 03:42:43 +02:00
Eugen Rochko
12208b849a New translations strings.xml (Interlingua) 2024-06-21 02:45:44 +02:00
Eugen Rochko
7f2af61244 New translations strings.xml (Interlingua) 2024-06-21 01:32:19 +02:00
Eugen Rochko
f745f95286 New translations strings.xml (Dutch) 2024-06-21 01:32:18 +02:00
Eugen Rochko
cf1c8eb4e3 New translations strings.xml (Interlingua) 2024-06-21 00:23:25 +02:00
Eugen Rochko
6d5ab6b4ba New translations strings.xml (Interlingua) 2024-06-20 23:15:17 +02:00
Eugen Rochko
ab27e36560 New translations strings.xml (Lithuanian) 2024-06-20 23:15:16 +02:00
Eugen Rochko
f368026124 New translations strings.xml (Lithuanian) 2024-06-20 22:10:31 +02:00
Eugen Rochko
bb4c68c6bd New translations strings.xml (Russian) 2024-06-20 17:49:52 +02:00
Grishka
09d68ec197 Merge branch 'master' into donations
# Conflicts:
#	mastodon/build.gradle
2024-06-19 23:12:46 +03:00
Eugen Rochko
b675c14767 New translations full_description.txt (Lithuanian) 2024-06-19 12:04:34 +02:00
Eugen Rochko
5e1b63ba21 New translations full_description.txt (Lithuanian) 2024-06-19 09:29:07 +02:00
Eugen Rochko
5a462df797 New translations strings.xml (Lithuanian) 2024-06-19 09:29:06 +02:00
Grishka
6d96ab5b7c Bump version 2024-06-19 05:14:28 +03:00
Grishka
1f051f3fd8 Fix #852 2024-06-19 05:14:02 +03:00
Grishka
e4e91dd283 Add changelog 2024-06-19 00:17:16 +03:00
Grishka
aca0773874 Merge branch 'l10n_master' 2024-06-19 00:12:34 +03:00
Grishka
2e5d73162d Merge remote-tracking branch 'origin/master' 2024-06-19 00:11:45 +03:00
Grishka
0c04701750 Bump version 2024-06-19 00:11:10 +03:00
Grishka
591ee62cc9 Crash fix 2024-06-19 00:10:12 +03:00
Eugen Rochko
1ff3ec5431 New translations strings.xml (Lithuanian) 2024-06-18 20:54:35 +02:00
Eugen Rochko
c8277624d2 New translations strings.xml (Lithuanian) 2024-06-18 19:17:48 +02:00
Eugen Rochko
a7ab35a6b0 New translations strings.xml (Lithuanian) 2024-06-18 17:53:41 +02:00
Gregory K
89d1498956 Merge pull request #850 from leoheitmannruiz/patch-1
Replace old icon used by F-Droid
2024-06-18 02:07:51 +03:00
Leo Heitmann Ruiz
cc585c9b43 Replace old icon used by F-Droid 2024-06-18 01:00:27 +02:00
Eugen Rochko
43a500c150 New translations strings.xml (Persian) 2024-06-17 18:36:26 +02:00
Grishka
5ac37a38c5 Crash fix 2024-06-17 18:19:34 +03:00
Grishka
960e54f4e6 Donation error string 2024-06-17 18:18:10 +03:00
Eugen Rochko
b107523d65 New translations strings.xml (Persian) 2024-06-17 16:46:22 +02:00
Grishka
1c6da783ad Add cancellation callback URL 2024-06-17 16:38:00 +03:00
Eugen Rochko
b2e01188ee New translations strings.xml (Lithuanian) 2024-06-17 02:57:07 +02:00
Eugen Rochko
bfd3387fa1 New translations strings.xml (Lithuanian) 2024-06-17 01:51:28 +02:00
Eugen Rochko
433161b08d New translations strings.xml (Lithuanian) 2024-06-17 00:51:22 +02:00
Eugen Rochko
b3ab900e47 New translations strings.xml (Vietnamese) 2024-06-15 16:50:34 +02:00
Grishka
bc6b3e1186 Merge branch 'master' into donations 2024-06-15 15:57:36 +03:00
Grishka
15d400c758 Make card corners nicer 2024-06-15 15:56:57 +03:00
Grishka
2023e55bfd New design for "explore -> news" 2024-06-15 15:54:25 +03:00
Grishka
d0352b86e0 Generalize link card view holder 2024-06-15 15:25:19 +03:00
Eugen Rochko
ecdc6c05b0 New translations strings.xml (Thai) 2024-06-15 09:53:23 +02:00
Eugen Rochko
c4e3566ff0 New translations strings.xml (Thai) 2024-06-15 08:12:20 +02:00
Eugen Rochko
109d103cc8 New translations strings.xml (Thai) 2024-06-15 07:12:57 +02:00
Grishka
f96237b6f1 Donations fixes + success post text from campaign 2024-06-15 00:46:42 +03:00
Grishka
d3344e3e03 Merge branch 'master' into donations 2024-06-15 00:36:09 +03:00
Grishka
115a6378d3 fediverse:creator in link cards (AND-182) 2024-06-15 00:32:56 +03:00
Grishka
264f9a933c Crash fix 2024-06-14 16:43:26 +03:00
Eugen Rochko
cc42110d49 New translations strings.xml (Japanese) 2024-06-14 15:12:09 +02:00
Eugen Rochko
5e9900378e New translations strings.xml (Slovenian) 2024-06-14 11:59:03 +02:00
Grishka
16d41fd1fc Make the illustration background transparent 2024-06-14 03:00:40 +03:00
Eugen Rochko
bacd11f4f3 New translations strings.xml (Italian) 2024-06-13 20:26:57 +02:00
Eugen Rochko
dd7aa01b3f New translations strings.xml (Italian) 2024-06-13 18:23:13 +02:00
Grishka
6270a3dfdc Donations improvements 2024-06-13 18:58:24 +03:00
Eugen Rochko
3cfed238fb New translations strings.xml (Basque) 2024-06-13 16:31:55 +02:00
Grishka
4e1bf80e12 Make WebViewFragment work with predictive back 2024-06-13 17:08:24 +03:00
Grishka
04129920eb Merge branch 'master' into donations
# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/res/values/strings.xml
2024-06-13 16:57:23 +03:00
Eugen Rochko
8725ed9d3c New translations strings.xml (Norwegian) 2024-06-12 22:57:05 +02:00
Eugen Rochko
30cfd883f5 New translations strings.xml (Icelandic) 2024-06-12 21:47:57 +02:00
Eugen Rochko
03e0b2af51 New translations strings.xml (Norwegian) 2024-06-12 21:47:56 +02:00
Eugen Rochko
da155fa5b1 New translations strings.xml (Slovenian) 2024-06-12 20:30:45 +02:00
Eugen Rochko
fe2adb28e2 New translations strings.xml (Norwegian) 2024-06-12 20:30:43 +02:00
Eugen Rochko
95a9d44fcf New translations strings.xml (Norwegian) 2024-06-12 17:51:24 +02:00
Eugen Rochko
4a712b1f26 New translations strings.xml (Norwegian) 2024-06-12 16:17:29 +02:00
Eugen Rochko
ee8ed127f6 New translations strings.xml (Belarusian) 2024-06-11 21:49:48 +02:00
Eugen Rochko
c0ba23af3c New translations strings.xml (Chinese Traditional) 2024-06-11 19:19:52 +02:00
Gregory K
c5197975c5 Merge pull request #846 from Evil2000/master
Added network security config
2024-06-11 11:39:58 +03:00
Grishka
e66751dc06 Notifications fixes 2024-06-11 11:35:23 +03:00
Eugen Rochko
8a219ab58d New translations strings.xml (Catalan) 2024-06-10 18:22:24 +02:00
Eugen Rochko
f80f240d4e New translations strings.xml (Catalan) 2024-06-10 16:32:32 +02:00
Eugen Rochko
92de40dd23 New translations strings.xml (Russian) 2024-06-10 13:00:24 +02:00
Grishka
513b29f57d Add support for predictive back navigation 2024-06-09 21:17:01 +03:00
Eugen Rochko
a8f4efa929 New translations strings.xml (Icelandic) 2024-06-09 12:31:11 +02:00
Gregory K
767e414c94 Merge pull request #847 from FineFindus/fix/honor-group-divider-upstream
fix: disable GroupDivider on Honor's MagicOS
2024-06-09 11:30:06 +03:00
FineFindus
7e1f63348a fix: disable GroupDivider on Honor's MagicOS
When enabled on MagicOS, GroupDivider are not displayed and causes other
menu items to be invisble. Similar bug to
https://github.com/mastodon/mastodon-android/pull/732.
2024-06-09 08:46:05 +02:00
Eugen Rochko
0e5c09626f New translations strings.xml (Chinese Traditional) 2024-06-09 06:30:40 +02:00
Grishka
0dabe89bcd Push notification improvements 2024-06-08 20:43:38 +03:00
Grishka
c3e48d20f3 VQA fixes part 2 2024-06-08 18:58:04 +03:00
Evil.2000
1352f884cb Added network security config 2024-06-08 15:27:04 +02:00
Eugen Rochko
f1700c9573 New translations strings.xml (Interlingua) 2024-06-08 14:10:36 +02:00
Eugen Rochko
256eb472c2 New translations strings.xml (Kabyle) 2024-06-08 14:10:34 +02:00
Eugen Rochko
5d0ebac464 New translations strings.xml (Scottish Gaelic) 2024-06-08 14:10:32 +02:00
Eugen Rochko
b2956a901a New translations strings.xml (Bosnian) 2024-06-08 14:10:31 +02:00
Eugen Rochko
bc592a97f2 New translations strings.xml (Filipino) 2024-06-08 14:10:30 +02:00
Eugen Rochko
414ad1df9c New translations strings.xml (Burmese) 2024-06-08 14:10:29 +02:00
Eugen Rochko
868e60db22 New translations strings.xml (Hindi) 2024-06-08 14:10:28 +02:00
Eugen Rochko
8d16c31e75 New translations strings.xml (Croatian) 2024-06-08 14:10:27 +02:00
Eugen Rochko
83b4a5b222 New translations strings.xml (Thai) 2024-06-08 14:10:26 +02:00
Eugen Rochko
c6483f2de2 New translations strings.xml (Persian) 2024-06-08 14:10:24 +02:00
Eugen Rochko
2884c6a8dd New translations strings.xml (Indonesian) 2024-06-08 14:10:23 +02:00
Eugen Rochko
8e01c18484 New translations strings.xml (Portuguese, Brazilian) 2024-06-08 14:10:22 +02:00
Eugen Rochko
f39b204e45 New translations strings.xml (Icelandic) 2024-06-08 14:10:21 +02:00
Eugen Rochko
121708ca55 New translations strings.xml (Galician) 2024-06-08 14:10:20 +02:00
Eugen Rochko
49c5ae3730 New translations strings.xml (Chinese Traditional) 2024-06-08 14:10:19 +02:00
Eugen Rochko
6b10ffd71a New translations strings.xml (Chinese Simplified) 2024-06-08 14:10:18 +02:00
Eugen Rochko
bcdbe6e6e8 New translations strings.xml (Swedish) 2024-06-08 14:10:17 +02:00
Eugen Rochko
1df9fbdf21 New translations strings.xml (Slovenian) 2024-06-08 14:10:16 +02:00
Eugen Rochko
15091f2a15 New translations strings.xml (Russian) 2024-06-08 14:10:15 +02:00
Eugen Rochko
34b719178e New translations strings.xml (Portuguese) 2024-06-08 14:10:13 +02:00
Eugen Rochko
790ef36b1b New translations strings.xml (Polish) 2024-06-08 14:10:12 +02:00
Eugen Rochko
1cbe0f10c4 New translations strings.xml (Norwegian) 2024-06-08 14:10:11 +02:00
Eugen Rochko
0b806bfee1 New translations strings.xml (Dutch) 2024-06-08 14:10:10 +02:00
Eugen Rochko
bd737e97f1 New translations strings.xml (Korean) 2024-06-08 14:10:09 +02:00
Eugen Rochko
1b8d21a3e0 New translations strings.xml (Georgian) 2024-06-08 14:10:08 +02:00
Eugen Rochko
4e0552e978 New translations strings.xml (Japanese) 2024-06-08 14:10:07 +02:00
Eugen Rochko
de2280dd2c New translations strings.xml (Italian) 2024-06-08 14:10:06 +02:00
Eugen Rochko
04b4f05642 New translations strings.xml (Armenian) 2024-06-08 14:10:05 +02:00
Eugen Rochko
ef05232e4e New translations strings.xml (Hungarian) 2024-06-08 14:10:03 +02:00
Eugen Rochko
21dda592b4 New translations strings.xml (Finnish) 2024-06-08 14:10:01 +02:00
Eugen Rochko
4e42a97b4b New translations strings.xml (Basque) 2024-06-08 14:10:00 +02:00
Eugen Rochko
3a029bd242 New translations strings.xml (Greek) 2024-06-08 14:09:59 +02:00
Eugen Rochko
055bbab36c New translations strings.xml (German) 2024-06-08 14:09:58 +02:00
Eugen Rochko
576d37633c New translations strings.xml (Danish) 2024-06-08 14:09:57 +02:00
Eugen Rochko
cbccc231f2 New translations strings.xml (Czech) 2024-06-08 14:09:56 +02:00
Eugen Rochko
2928301a9f New translations strings.xml (Catalan) 2024-06-08 14:09:55 +02:00
Eugen Rochko
47e105228a New translations strings.xml (Belarusian) 2024-06-08 14:09:54 +02:00
Eugen Rochko
3228deec4c New translations strings.xml (Arabic) 2024-06-08 14:09:53 +02:00
Eugen Rochko
62a4393e03 New translations strings.xml (Spanish) 2024-06-08 14:09:52 +02:00
Eugen Rochko
9fd47f4825 New translations strings.xml (French) 2024-06-08 14:09:51 +02:00
Eugen Rochko
1823919e28 New translations strings.xml (Turkish) 2024-06-08 14:09:49 +02:00
Eugen Rochko
1037d39168 New translations strings.xml (Ukrainian) 2024-06-08 14:09:48 +02:00
Eugen Rochko
7da0974ef0 New translations strings.xml (Vietnamese) 2024-06-08 14:09:47 +02:00
Eugen Rochko
aa7a307cc1 New translations strings.xml (Lithuanian) 2024-06-08 14:09:46 +02:00
Grishka
bf44f7ef13 VQA fixes part 1 2024-06-08 14:49:39 +03:00
Grishka
1b4afe7ba9 Fix #845 2024-06-05 04:28:50 +03:00
Eugen Rochko
832d371b2e New translations strings.xml (Turkish) 2024-06-04 22:14:20 +02:00
Eugen Rochko
15984eabdf New translations strings.xml (Turkish) 2024-06-04 21:10:09 +02:00
Eugen Rochko
1f72ff3ed7 New translations strings.xml (Ukrainian) 2024-05-31 16:32:27 +02:00
Eugen Rochko
9456a0045a New translations full_description.txt (Lithuanian) 2024-05-30 23:22:30 +02:00
Eugen Rochko
0035a41fe9 New translations strings.xml (Lithuanian) 2024-05-30 23:22:29 +02:00
Eugen Rochko
8c3fe2ff52 New translations full_description.txt (Lithuanian) 2024-05-30 21:27:24 +02:00
Eugen Rochko
7678d7b809 New translations strings.xml (Vietnamese) 2024-05-29 03:16:24 +02:00
Eugen Rochko
9be424fa77 New translations strings.xml (Basque) 2024-05-24 14:12:07 +02:00
Eugen Rochko
e65dfe16ac New translations strings.xml (Belarusian) 2024-05-22 13:59:26 +02:00
Eugen Rochko
84c555e3bc New translations strings.xml (Lithuanian) 2024-05-22 13:59:25 +02:00
Eugen Rochko
c4d7018531 New translations strings.xml (Belarusian) 2024-05-22 11:56:30 +02:00
Grishka
1dd40a77de Merge branch 'master' into donations 2024-05-22 12:22:06 +03:00
Grishka
b2c797fb46 Fix #842 2024-05-21 22:23:15 +03:00
Grishka
c94e1f939a Add staging environment toggle 2024-05-21 20:17:46 +03:00
Grishka
2dae00800e Merge branch 'master' into donations
# Conflicts:
#	mastodon/src/main/res/values/strings.xml
2024-05-21 20:08:43 +03:00
Eugen Rochko
b888f71d2a New translations strings.xml (Icelandic) 2024-05-21 13:28:06 +02:00
Eugen Rochko
6ffdbb7b71 New translations strings.xml (Italian) 2024-05-17 13:35:40 -07:00
Eugen Rochko
7cb40f1556 New translations strings.xml (Slovenian) 2024-05-17 11:48:15 -07:00
Eugen Rochko
975999df96 New translations strings.xml (Thai) 2024-05-15 06:42:48 -07:00
Eugen Rochko
a6f1943e05 New translations strings.xml (French) 2024-05-14 11:09:34 -07:00
Eugen Rochko
91ff898ac7 New translations full_description.txt (Interlingua) 2024-05-14 08:56:24 -07:00
Eugen Rochko
5c705b0f22 New translations strings.xml (Interlingua) 2024-05-14 08:56:23 -07:00
Eugen Rochko
77a07ffbb1 New translations full_description.txt (Interlingua) 2024-05-14 07:12:55 -07:00
Eugen Rochko
559ec3ecb3 New translations strings.xml (Interlingua) 2024-05-14 07:12:54 -07:00
Eugen Rochko
17bdd54f63 New translations strings.xml (Interlingua) 2024-05-14 05:35:09 -07:00
Eugen Rochko
cfdf6ac27e New translations strings.xml (Chinese Traditional) 2024-05-13 18:50:27 -07:00
Eugen Rochko
4fbf9aa1f8 New translations strings.xml (Interlingua) 2024-05-13 13:21:14 -07:00
Eugen Rochko
d66463c7c6 New translations strings.xml (Interlingua) 2024-05-13 12:03:25 -07:00
Eugen Rochko
e2d5d4c30b New translations strings.xml (Interlingua) 2024-05-13 10:58:20 -07:00
Eugen Rochko
5db90188b2 New translations strings.xml (Interlingua) 2024-05-13 08:58:05 -07:00
Eugen Rochko
cfe6332005 New translations strings.xml (Interlingua) 2024-05-13 07:16:57 -07:00
Eugen Rochko
d1a706860f New translations strings.xml (Japanese) 2024-05-12 21:25:55 -07:00
Gregory K
29ab502d2e Merge pull request #833 from NorbiPeti/master
Display more user-friendly error messages
2024-05-13 01:49:34 +03:00
NorbiPeti
629e65edba Update error messages and remove the unknown error text 2024-05-13 00:08:18 +02:00
Eugen Rochko
f4c2c5b7f3 New translations strings.xml (Interlingua) 2024-05-12 23:09:22 +02:00
Eugen Rochko
2b334e973a New translations strings.xml (Interlingua) 2024-05-12 22:11:59 +02:00
Eugen Rochko
3212d3ce04 New translations strings.xml (Interlingua) 2024-05-12 18:50:21 +02:00
Eugen Rochko
094acf86c3 New translations strings.xml (Interlingua) 2024-05-12 16:31:36 +02:00
Eugen Rochko
51e91efb12 New translations strings.xml (Interlingua) 2024-05-12 11:56:54 +02:00
Eugen Rochko
fe48a9ece2 New translations strings.xml (Interlingua) 2024-05-12 10:51:38 +02:00
Eugen Rochko
97152ffcdf New translations strings.xml (Interlingua) 2024-05-12 09:43:43 +02:00
Eugen Rochko
25f8649ee5 New translations strings.xml (Interlingua) 2024-05-12 08:46:18 +02:00
Eugen Rochko
359b9d04be New translations strings.xml (Interlingua) 2024-05-12 07:50:07 +02:00
Eugen Rochko
359392d77d New translations strings.xml (Interlingua) 2024-05-10 23:13:43 +02:00
Eugen Rochko
755b56a8db New translations strings.xml (Interlingua) 2024-05-10 22:14:16 +02:00
Eugen Rochko
9a99223d19 New translations strings.xml (Interlingua) 2024-05-10 19:12:48 +02:00
Eugen Rochko
b77ae93290 New translations strings.xml (Interlingua) 2024-05-10 17:52:39 +02:00
Eugen Rochko
688b6b3f82 New translations strings.xml (Vietnamese) 2024-05-10 16:31:42 +02:00
Eugen Rochko
ff8d2e7010 New translations strings.xml (Portuguese, Brazilian) 2024-05-10 10:34:45 +02:00
Eugen Rochko
a29672d758 New translations strings.xml (Lithuanian) 2024-05-09 18:49:09 +02:00
Eugen Rochko
7b2c1c1f32 New translations strings.xml (Interlingua) 2024-05-09 00:13:03 +02:00
Eugen Rochko
7f4a51d1cd New translations strings.xml (Interlingua) 2024-05-08 15:05:44 +02:00
Eugen Rochko
7486bf4c06 New translations strings.xml (Interlingua) 2024-05-08 12:57:14 +02:00
Grishka
97e16b9f73 Use image_matrix_limit from instance configuration if available 2024-05-07 23:04:43 +03:00
Eugen Rochko
16b3352e65 New translations short_description.txt (Interlingua) 2024-05-07 21:50:35 +02:00
Eugen Rochko
4936127655 New translations full_description.txt (Interlingua) 2024-05-07 21:50:34 +02:00
Eugen Rochko
aa7453855e New translations strings.xml (Interlingua) 2024-05-07 21:50:33 +02:00
Eugen Rochko
c789c71768 New translations strings.xml (Interlingua) 2024-05-07 20:40:03 +02:00
Eugen Rochko
37de5b79a8 New translations strings.xml (Icelandic) 2024-05-07 20:40:02 +02:00
Eugen Rochko
11b24a1821 New translations strings.xml (Interlingua) 2024-05-07 19:31:24 +02:00
Eugen Rochko
5963b5ab1e New translations strings.xml (Hungarian) 2024-05-07 11:47:00 +02:00
Eugen Rochko
b4f738cd98 New translations strings.xml (Interlingua) 2024-05-07 09:25:41 +02:00
Eugen Rochko
d44107d64d New translations strings.xml (Ukrainian) 2024-05-07 09:25:40 +02:00
Eugen Rochko
ad2006c853 New translations strings.xml (Interlingua) 2024-05-07 07:56:58 +02:00
Eugen Rochko
296c3bbcc8 New translations strings.xml (Basque) 2024-05-06 18:14:50 +02:00
Eugen Rochko
c349293e24 New translations title.txt (Interlingua) 2024-05-06 14:36:16 +02:00
Eugen Rochko
c624bb69b6 New translations short_description.txt (Interlingua) 2024-05-06 14:36:15 +02:00
Eugen Rochko
db58e8f214 New translations full_description.txt (Interlingua) 2024-05-06 14:36:14 +02:00
Eugen Rochko
4379f5cd12 New translations strings.xml (Interlingua) 2024-05-06 14:36:12 +02:00
Grishka
bc78c61009 Fix #835 2024-05-06 12:56:56 +03:00
Eugen Rochko
51b2fc7dc5 New translations strings.xml (Ukrainian) 2024-05-05 11:49:11 +02:00
Eugen Rochko
2bd2dbe624 New translations strings.xml (German) 2024-05-04 20:50:05 +02:00
Grishka
4a1b1e19e8 Fix #834 2024-05-04 21:20:20 +03:00
Eugen Rochko
2456f07128 New translations strings.xml (German) 2024-05-04 19:50:03 +02:00
Eugen Rochko
e517db3002 New translations strings.xml (Slovenian) 2024-05-03 22:28:31 +02:00
NorbiPeti
c7b8cc72fc Display more user-friendly error messages
Instead of displaying the Java
exception, this change displays a more user-friendly message for some
common network-related issues.
Fixes mastodon/mastodon-android#667
2024-05-02 19:02:03 +02:00
Eugen Rochko
06e832848b New translations strings.xml (Italian) 2024-05-02 18:33:26 +02:00
Eugen Rochko
7848bef09b New translations strings.xml (Lithuanian) 2024-05-02 16:59:23 +02:00
Eugen Rochko
943a7fcff3 New translations strings.xml (Japanese) 2024-05-02 10:54:21 +02:00
Eugen Rochko
c67194606f New translations strings.xml (Japanese) 2024-05-02 09:15:07 +02:00
Eugen Rochko
2c823b9eb5 New translations strings.xml (Chinese Traditional) 2024-05-02 06:29:52 +02:00
Eugen Rochko
238abc20b5 New translations strings.xml (Thai) 2024-05-01 21:16:34 +02:00
Eugen Rochko
657e1b9c36 New translations strings.xml (Thai) 2024-05-01 16:50:36 +02:00
Eugen Rochko
7164bca3c8 New translations strings.xml (Portuguese, Brazilian) 2024-05-01 16:50:34 +02:00
Eugen Rochko
7b7e0e1f8d New translations strings.xml (Icelandic) 2024-05-01 16:50:32 +02:00
Eugen Rochko
4058a844b7 New translations strings.xml (Chinese Traditional) 2024-05-01 16:50:31 +02:00
Eugen Rochko
2f8b2e4069 New translations strings.xml (Ukrainian) 2024-05-01 16:50:29 +02:00
Eugen Rochko
21323fd396 New translations strings.xml (Slovenian) 2024-05-01 16:50:27 +02:00
Eugen Rochko
35efc6a1bc New translations strings.xml (Russian) 2024-05-01 16:50:26 +02:00
Eugen Rochko
8bbef30a1d New translations strings.xml (Dutch) 2024-05-01 16:50:23 +02:00
Eugen Rochko
70c61dcbf5 New translations strings.xml (Italian) 2024-05-01 16:50:20 +02:00
Eugen Rochko
6375850da6 New translations strings.xml (Hungarian) 2024-05-01 16:50:19 +02:00
Eugen Rochko
8a18e2f167 New translations strings.xml (Basque) 2024-05-01 16:50:15 +02:00
Eugen Rochko
61dd972ee5 New translations strings.xml (Lithuanian) 2024-05-01 16:50:07 +02:00
Eugen Rochko
30efd8c45b New translations strings.xml (Vietnamese) 2024-05-01 16:50:06 +02:00
Eugen Rochko
6b31a3e2d6 New translations strings.xml (Persian) 2024-05-01 16:50:04 +02:00
Grishka
0e868f0be0 English proper nouns are a mess 2024-05-01 17:39:56 +03:00
Grishka
67847d90da Fix #832 2024-05-01 17:38:22 +03:00
Eugen Rochko
730804409a New translations strings.xml (Portuguese, Brazilian) 2024-05-01 01:27:29 +02:00
Eugen Rochko
567347e66a New translations strings.xml (Lithuanian) 2024-04-29 11:43:21 +02:00
Eugen Rochko
b3f033e30b New translations strings.xml (Lithuanian) 2024-04-29 09:25:49 +02:00
Eugen Rochko
c44dbc82c8 New translations strings.xml (Ukrainian) 2024-04-28 13:35:46 +02:00
Eugen Rochko
00cffd24fe New translations strings.xml (Ukrainian) 2024-04-28 12:30:39 +02:00
Eugen Rochko
51469e1198 New translations strings.xml (Ukrainian) 2024-04-28 11:34:29 +02:00
Eugen Rochko
78f75d4cc1 New translations strings.xml (Ukrainian) 2024-04-26 13:14:28 +02:00
Eugen Rochko
9fbc4ad851 New translations full_description.txt (Hungarian) 2024-04-25 22:29:55 +02:00
Eugen Rochko
bcc62ed222 New translations strings.xml (Hungarian) 2024-04-25 22:29:54 +02:00
Eugen Rochko
9cc8769d45 New translations strings.xml (Ukrainian) 2024-04-25 21:21:43 +02:00
Eugen Rochko
9ed3e2f897 New translations strings.xml (Basque) 2024-04-25 21:21:42 +02:00
Eugen Rochko
9bf399070a New translations strings.xml (Dutch) 2024-04-25 19:41:44 +02:00
Eugen Rochko
dd72723198 New translations strings.xml (Ukrainian) 2024-04-25 18:26:36 +02:00
Eugen Rochko
3f4567fa5f New translations strings.xml (Ukrainian) 2024-04-25 17:13:33 +02:00
Eugen Rochko
065d6398e6 New translations strings.xml (Ukrainian) 2024-04-24 17:33:24 +02:00
Eugen Rochko
68e3e54ae0 New translations strings.xml (Icelandic) 2024-04-24 15:28:53 +02:00
Eugen Rochko
cae5622d7c New translations strings.xml (Portuguese, Brazilian) 2024-04-24 06:52:31 +02:00
Eugen Rochko
5e8230291e New translations strings.xml (Portuguese, Brazilian) 2024-04-24 05:51:08 +02:00
Eugen Rochko
e2c9115d63 New translations strings.xml (Portuguese, Brazilian) 2024-04-24 01:15:41 +02:00
Eugen Rochko
cae67ad3a9 New translations strings.xml (Portuguese, Brazilian) 2024-04-24 00:14:03 +02:00
Eugen Rochko
1f7939c90d New translations strings.xml (Slovenian) 2024-04-23 19:39:36 +02:00
Eugen Rochko
b6b2d873a0 New translations strings.xml (Italian) 2024-04-23 17:58:18 +02:00
Eugen Rochko
f1393ee02e New translations strings.xml (Italian) 2024-04-23 16:14:12 +02:00
Eugen Rochko
316593e32c New translations strings.xml (Thai) 2024-04-22 21:31:03 +02:00
Eugen Rochko
78af4bfb59 New translations strings.xml (Dutch) 2024-04-22 15:34:16 +02:00
Grishka
ea53200e23 Merge branch 'master' into donations 2024-04-21 18:11:32 +03:00
Gregory K
b0c77d42c0 Merge pull request #829 from Endeavour233/avoid-leak-response-body
Opti: avoid leaking response body when request is cancelled
2024-04-21 18:11:02 +03:00
Eugen Rochko
2ec8ddbebe New translations strings.xml (Vietnamese) 2024-04-21 15:51:06 +02:00
shimura233
bfc0076429 opti: avoid leaking response body when it's not used. 2024-04-21 20:29:15 +08:00
Eugen Rochko
1b1a1e4449 New translations strings.xml (Chinese Traditional) 2024-04-21 02:35:03 +02:00
Eugen Rochko
5fa6ad31a8 New translations strings.xml (Persian) 2024-04-20 18:53:16 +02:00
Eugen Rochko
21e59d9a68 New translations strings.xml (Persian) 2024-04-20 16:53:13 +02:00
Eugen Rochko
5739d8a5ae New translations strings.xml (Greek) 2024-04-20 12:28:40 +02:00
Eugen Rochko
1f7ad8e535 New translations strings.xml (Scottish Gaelic) 2024-04-20 06:44:35 +02:00
Eugen Rochko
ced0112561 New translations strings.xml (Filipino) 2024-04-20 06:44:33 +02:00
Eugen Rochko
e0f11440d9 New translations strings.xml (Hindi) 2024-04-20 06:44:31 +02:00
Eugen Rochko
75d92cdbbe New translations strings.xml (Thai) 2024-04-20 06:44:30 +02:00
Eugen Rochko
2c6ecb7d42 New translations strings.xml (Indonesian) 2024-04-20 06:44:28 +02:00
Eugen Rochko
e8391c2463 New translations strings.xml (Portuguese, Brazilian) 2024-04-20 06:44:27 +02:00
Eugen Rochko
c0d631b3c3 New translations strings.xml (Icelandic) 2024-04-20 06:44:26 +02:00
Eugen Rochko
89806139a8 New translations strings.xml (Galician) 2024-04-20 06:44:25 +02:00
Eugen Rochko
210b0b3be2 New translations strings.xml (Chinese Traditional) 2024-04-20 06:44:24 +02:00
Eugen Rochko
49d4421635 New translations strings.xml (Chinese Simplified) 2024-04-20 06:44:23 +02:00
Eugen Rochko
bf3cae3da7 New translations strings.xml (Ukrainian) 2024-04-20 06:44:23 +02:00
Eugen Rochko
e93730a5dc New translations strings.xml (Turkish) 2024-04-20 06:44:22 +02:00
Eugen Rochko
7e544749d5 New translations strings.xml (Swedish) 2024-04-20 06:44:21 +02:00
Eugen Rochko
1e23d2f510 New translations strings.xml (Slovenian) 2024-04-20 06:44:19 +02:00
Eugen Rochko
5bcda74066 New translations strings.xml (Russian) 2024-04-20 06:44:18 +02:00
Eugen Rochko
b599b4d6cc New translations strings.xml (Portuguese) 2024-04-20 06:44:17 +02:00
Eugen Rochko
3f3610102f New translations strings.xml (Polish) 2024-04-20 06:44:16 +02:00
Eugen Rochko
e4e6ebf91c New translations strings.xml (Norwegian) 2024-04-20 06:44:15 +02:00
Eugen Rochko
589d011d4c New translations strings.xml (Dutch) 2024-04-20 06:44:14 +02:00
Eugen Rochko
325d40c1bf New translations strings.xml (Korean) 2024-04-20 06:44:13 +02:00
Eugen Rochko
f2dc402869 New translations strings.xml (Japanese) 2024-04-20 06:44:12 +02:00
Eugen Rochko
c19a0be8f8 New translations strings.xml (Italian) 2024-04-20 06:44:11 +02:00
Eugen Rochko
445fea5237 New translations strings.xml (Hungarian) 2024-04-20 06:44:10 +02:00
Eugen Rochko
d1b38b926b New translations strings.xml (Finnish) 2024-04-20 06:44:08 +02:00
Eugen Rochko
fcd128d8d9 New translations strings.xml (Basque) 2024-04-20 06:44:07 +02:00
Eugen Rochko
0025f812b8 New translations strings.xml (Greek) 2024-04-20 06:44:06 +02:00
Eugen Rochko
d7c7e05ec1 New translations strings.xml (German) 2024-04-20 06:44:05 +02:00
Eugen Rochko
c932bafc12 New translations strings.xml (Danish) 2024-04-20 06:44:04 +02:00
Eugen Rochko
a0376b344a New translations strings.xml (Czech) 2024-04-20 06:44:03 +02:00
Eugen Rochko
1cbc117878 New translations strings.xml (Catalan) 2024-04-20 06:44:02 +02:00
Eugen Rochko
ea8c1abec9 New translations strings.xml (Belarusian) 2024-04-20 06:44:01 +02:00
Eugen Rochko
a410eb424f New translations strings.xml (Arabic) 2024-04-20 06:44:00 +02:00
Eugen Rochko
8624de5705 New translations strings.xml (Spanish) 2024-04-20 06:43:59 +02:00
Eugen Rochko
358d5820f6 New translations strings.xml (French) 2024-04-20 06:43:58 +02:00
Eugen Rochko
7c06aecf31 New translations strings.xml (Lithuanian) 2024-04-20 06:43:56 +02:00
Eugen Rochko
5d7544e58c New translations strings.xml (Vietnamese) 2024-04-20 06:43:55 +02:00
Eugen Rochko
17423efb96 New translations strings.xml (Armenian) 2024-04-20 06:43:55 +02:00
Eugen Rochko
a28793edd2 New translations strings.xml (Persian) 2024-04-20 06:43:53 +02:00
Grishka
79087e3c86 Merge branch 'master' into donations
# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/res/values/strings.xml
2024-04-20 07:16:11 +03:00
Grishka
50760471d5 Add context menus to post footer buttons (AND-161) 2024-04-20 07:12:51 +03:00
Grishka
a6df8cb62d Pressed state for alt badge (AND-157) 2024-04-20 06:57:14 +03:00
Grishka
873711939d Post header tap area thing (AND-160) 2024-04-20 06:51:30 +03:00
Grishka
2bd13eb3ba Label unlabeled things for accessibility 2024-04-20 06:34:21 +03:00
Grishka
d423f17342 Animate privacy button changes (AND-158) 2024-04-20 06:27:47 +03:00
Eugen Rochko
a02c99452d New translations strings.xml (Kabyle) 2024-04-20 04:07:19 +02:00
Eugen Rochko
fd854912fc New translations strings.xml (Scottish Gaelic) 2024-04-20 04:07:17 +02:00
Eugen Rochko
5999991209 New translations strings.xml (Bosnian) 2024-04-20 04:07:15 +02:00
Eugen Rochko
50ff46465f New translations strings.xml (Filipino) 2024-04-20 04:07:15 +02:00
Eugen Rochko
c1040907d9 New translations strings.xml (Hindi) 2024-04-20 04:07:13 +02:00
Eugen Rochko
2cae19a75d New translations strings.xml (Croatian) 2024-04-20 04:07:12 +02:00
Eugen Rochko
c302473798 New translations strings.xml (Thai) 2024-04-20 04:07:11 +02:00
Eugen Rochko
4107636f00 New translations strings.xml (Bengali) 2024-04-20 04:07:10 +02:00
Eugen Rochko
ee07ea9426 New translations strings.xml (Indonesian) 2024-04-20 04:07:09 +02:00
Eugen Rochko
c806f20f2c New translations strings.xml (Portuguese, Brazilian) 2024-04-20 04:07:08 +02:00
Eugen Rochko
98b5121a72 New translations strings.xml (Icelandic) 2024-04-20 04:07:07 +02:00
Eugen Rochko
fb842f72c8 New translations strings.xml (Galician) 2024-04-20 04:07:06 +02:00
Eugen Rochko
982a043342 New translations strings.xml (Chinese Traditional) 2024-04-20 04:07:05 +02:00
Eugen Rochko
09e1baf19f New translations strings.xml (Chinese Simplified) 2024-04-20 04:07:04 +02:00
Eugen Rochko
fd2dbcf120 New translations strings.xml (Ukrainian) 2024-04-20 04:07:03 +02:00
Eugen Rochko
9ff0d76c3a New translations strings.xml (Turkish) 2024-04-20 04:07:02 +02:00
Eugen Rochko
48f0186151 New translations strings.xml (Swedish) 2024-04-20 04:07:01 +02:00
Eugen Rochko
45a945e983 New translations strings.xml (Slovenian) 2024-04-20 04:07:00 +02:00
Eugen Rochko
971c07a7c2 New translations strings.xml (Russian) 2024-04-20 04:06:59 +02:00
Eugen Rochko
eeb85ae3db New translations strings.xml (Portuguese) 2024-04-20 04:06:58 +02:00
Eugen Rochko
fe13376a56 New translations strings.xml (Polish) 2024-04-20 04:06:57 +02:00
Eugen Rochko
0d55c1fc37 New translations strings.xml (Norwegian) 2024-04-20 04:06:56 +02:00
Eugen Rochko
d5ebe761d5 New translations strings.xml (Dutch) 2024-04-20 04:06:55 +02:00
Eugen Rochko
9774b2a215 New translations strings.xml (Korean) 2024-04-20 04:06:55 +02:00
Eugen Rochko
ba442511b7 New translations strings.xml (Georgian) 2024-04-20 04:06:54 +02:00
Eugen Rochko
e831a01a28 New translations strings.xml (Japanese) 2024-04-20 04:06:53 +02:00
Eugen Rochko
884ab72ac6 New translations strings.xml (Italian) 2024-04-20 04:06:52 +02:00
Eugen Rochko
6edb483a5b New translations strings.xml (Hungarian) 2024-04-20 04:06:51 +02:00
Eugen Rochko
d1c788c639 New translations strings.xml (Finnish) 2024-04-20 04:06:49 +02:00
Eugen Rochko
b17680d897 New translations strings.xml (Basque) 2024-04-20 04:06:48 +02:00
Eugen Rochko
707d9d2332 New translations strings.xml (Greek) 2024-04-20 04:06:47 +02:00
Eugen Rochko
9c67d68d05 New translations strings.xml (German) 2024-04-20 04:06:46 +02:00
Eugen Rochko
a37ae88d3b New translations strings.xml (Danish) 2024-04-20 04:06:45 +02:00
Eugen Rochko
b8489b0379 New translations strings.xml (Czech) 2024-04-20 04:06:44 +02:00
Eugen Rochko
87e2bdd39d New translations strings.xml (Catalan) 2024-04-20 04:06:43 +02:00
Eugen Rochko
52b95ef5f9 New translations strings.xml (Belarusian) 2024-04-20 04:06:42 +02:00
Eugen Rochko
81375f501e New translations strings.xml (Arabic) 2024-04-20 04:06:41 +02:00
Eugen Rochko
d37d977c79 New translations strings.xml (Spanish) 2024-04-20 04:06:40 +02:00
Eugen Rochko
654967b6f0 New translations strings.xml (French) 2024-04-20 04:06:39 +02:00
Eugen Rochko
8ad48a04a0 New translations strings.xml (Lithuanian) 2024-04-20 04:06:37 +02:00
Eugen Rochko
a4c04bb279 New translations strings.xml (Vietnamese) 2024-04-20 04:06:36 +02:00
Eugen Rochko
d4e0797586 New translations strings.xml (Armenian) 2024-04-20 04:06:35 +02:00
Eugen Rochko
8c6009cd63 New translations strings.xml (Persian) 2024-04-20 04:06:34 +02:00
Grishka
09be5b3f97 Add descriptions to post visibility options 2024-04-20 04:48:57 +03:00
Grishka
31af0251ea Update endpoint 2024-04-18 02:59:45 +03:00
Grishka
d0d899c73d Check account age for donation eligibility 2024-04-18 02:58:42 +03:00
Grishka
b5aa1a4598 Cache donation campaign responses 2024-04-18 02:55:07 +03:00
Grishka
b9956950b6 Add arrows to currency selector 2024-04-18 02:43:19 +03:00
Eugen Rochko
dee5e3b365 New translations strings.xml (Portuguese, Brazilian) 2024-04-17 15:41:23 +02:00
Eugen Rochko
8224801024 New translations strings.xml (Portuguese, Brazilian) 2024-04-17 13:56:32 +02:00
Eugen Rochko
f5377dc276 New translations full_description.txt (Chinese Simplified) 2024-04-17 04:39:57 +02:00
Eugen Rochko
94f6b49487 New translations strings.xml (Chinese Simplified) 2024-04-17 04:39:56 +02:00
Eugen Rochko
acaacf063e New translations strings.xml (Chinese Simplified) 2024-04-17 03:28:50 +02:00
Eugen Rochko
881705fa9c New translations strings.xml (Vietnamese) 2024-04-16 18:56:49 +02:00
Eugen Rochko
5440674498 New translations strings.xml (Italian) 2024-04-16 13:26:04 +02:00
Eugen Rochko
c1770c428d New translations strings.xml (Spanish) 2024-04-16 11:28:23 +02:00
Eugen Rochko
db3cb118e1 New translations strings.xml (Spanish) 2024-04-16 09:16:09 +02:00
Eugen Rochko
085cce211f New translations strings.xml (Spanish) 2024-04-16 06:33:09 +02:00
Eugen Rochko
671318a810 New translations strings.xml (Spanish) 2024-04-16 05:24:02 +02:00
Eugen Rochko
8accae1105 New translations strings.xml (Spanish) 2024-04-16 04:08:26 +02:00
Grishka
104896ae1b Custom layout for suggested amount buttons 2024-04-15 20:05:12 +03:00
Grishka
977fc2483f Update "masterial" colors to correct ones 2024-04-15 17:46:03 +03:00
Grishka
658177538b Fix dark theme 2024-04-15 17:02:11 +03:00
Grishka
b2d49c3143 Initial implementation of donations 2024-04-15 16:36:59 +03:00
Eugen Rochko
cbf36eb999 New translations strings.xml (Dutch) 2024-04-15 14:33:18 +02:00
Eugen Rochko
26e25268f7 New translations strings.xml (Polish) 2024-04-15 13:15:50 +02:00
Eugen Rochko
2d93e98dc7 New translations strings.xml (Dutch) 2024-04-15 13:15:49 +02:00
Eugen Rochko
7764ff22dc New translations strings.xml (Polish) 2024-04-15 11:28:30 +02:00
Eugen Rochko
a5e0b96af5 New translations strings.xml (Icelandic) 2024-04-13 15:01:39 +02:00
Eugen Rochko
f30d58446f New translations strings.xml (Japanese) 2024-04-13 06:01:34 +02:00
Eugen Rochko
a4041d3bcd New translations strings.xml (Portuguese, Brazilian) 2024-04-13 04:32:21 +02:00
Eugen Rochko
e72b7f3d29 New translations strings.xml (Thai) 2024-04-12 22:51:36 +02:00
Eugen Rochko
2bfe5cdfc9 New translations strings.xml (Belarusian) 2024-04-12 20:24:11 +02:00
Grishka
1124bc48c2 Update CI Ruby to 3.3.0 and add Gemfile.lock 2024-04-12 18:31:43 +03:00
Grishka
69b7484a4a Merge commit '4dd5a80ef27bd4abf1eaa272d1e3c67b7d9a3a13' 2024-04-12 17:59:55 +03:00
Grishka
19939e457b Prepare for release 2024-04-12 17:57:14 +03:00
Eugen Rochko
92603217e7 New translations strings.xml (Chinese Traditional) 2024-04-11 22:59:52 +02:00
Grishka
3e256d41d2 Fix alert text size 2024-04-11 22:36:55 +03:00
Eugen Rochko
b6a9c1b879 New translations strings.xml (Kabyle) 2024-04-11 16:46:26 +02:00
Eugen Rochko
bf839b8d52 New translations strings.xml (Occitan) 2024-04-11 16:46:25 +02:00
Eugen Rochko
a13c98e6c9 New translations strings.xml (Scottish Gaelic) 2024-04-11 16:46:24 +02:00
Eugen Rochko
65806d2491 New translations strings.xml (Bosnian) 2024-04-11 16:46:22 +02:00
Eugen Rochko
0450701548 New translations strings.xml (Filipino) 2024-04-11 16:46:21 +02:00
Eugen Rochko
9f1b329a46 New translations strings.xml (Burmese) 2024-04-11 16:46:20 +02:00
Eugen Rochko
22a71dd2d2 New translations strings.xml (Hindi) 2024-04-11 16:46:19 +02:00
Eugen Rochko
595690aea9 New translations strings.xml (Croatian) 2024-04-11 16:46:18 +02:00
Eugen Rochko
deae7d100c New translations strings.xml (Thai) 2024-04-11 16:46:17 +02:00
Eugen Rochko
0397fcb9be New translations strings.xml (Bengali) 2024-04-11 16:46:15 +02:00
Eugen Rochko
6df27566d2 New translations strings.xml (Indonesian) 2024-04-11 16:46:14 +02:00
Eugen Rochko
0b919e3815 New translations strings.xml (Portuguese, Brazilian) 2024-04-11 16:46:13 +02:00
Eugen Rochko
1d5f07a658 New translations strings.xml (Icelandic) 2024-04-11 16:46:12 +02:00
Eugen Rochko
571a09364c New translations strings.xml (Galician) 2024-04-11 16:46:11 +02:00
Eugen Rochko
92f94d7a82 New translations strings.xml (Chinese Traditional) 2024-04-11 16:46:10 +02:00
Eugen Rochko
87cb7dbc39 New translations strings.xml (Chinese Simplified) 2024-04-11 16:46:09 +02:00
Eugen Rochko
2c3dd3960d New translations strings.xml (Ukrainian) 2024-04-11 16:46:08 +02:00
Eugen Rochko
d1a16a3202 New translations strings.xml (Turkish) 2024-04-11 16:46:07 +02:00
Eugen Rochko
3bfb665a76 New translations strings.xml (Swedish) 2024-04-11 16:46:06 +02:00
Eugen Rochko
1ebf6eb8f9 New translations strings.xml (Slovenian) 2024-04-11 16:46:05 +02:00
Eugen Rochko
1af6ce2ce6 New translations strings.xml (Russian) 2024-04-11 16:46:04 +02:00
Eugen Rochko
f0040620c8 New translations strings.xml (Portuguese) 2024-04-11 16:46:03 +02:00
Eugen Rochko
41e91371d5 New translations strings.xml (Polish) 2024-04-11 16:46:02 +02:00
Eugen Rochko
c2a00f88d9 New translations strings.xml (Norwegian) 2024-04-11 16:46:01 +02:00
Eugen Rochko
4a0ac11883 New translations strings.xml (Dutch) 2024-04-11 16:46:00 +02:00
Eugen Rochko
a50082f738 New translations strings.xml (Korean) 2024-04-11 16:45:59 +02:00
Eugen Rochko
19dc448a08 New translations strings.xml (Japanese) 2024-04-11 16:45:57 +02:00
Eugen Rochko
ad06dc14b0 New translations strings.xml (Italian) 2024-04-11 16:45:56 +02:00
Eugen Rochko
c1aa2775a5 New translations strings.xml (Hungarian) 2024-04-11 16:45:55 +02:00
Eugen Rochko
ec58a7e369 New translations strings.xml (Hebrew) 2024-04-11 16:45:54 +02:00
Eugen Rochko
973ffb0fe5 New translations strings.xml (Finnish) 2024-04-11 16:45:52 +02:00
Eugen Rochko
5e4c993525 New translations strings.xml (Basque) 2024-04-11 16:45:51 +02:00
Eugen Rochko
3ffc427cd6 New translations strings.xml (Greek) 2024-04-11 16:45:50 +02:00
Eugen Rochko
24b065ba03 New translations strings.xml (German) 2024-04-11 16:45:49 +02:00
Eugen Rochko
47bd910727 New translations strings.xml (Danish) 2024-04-11 16:45:48 +02:00
Eugen Rochko
a02f654b14 New translations strings.xml (Czech) 2024-04-11 16:45:47 +02:00
Eugen Rochko
d1c6afcebd New translations strings.xml (Catalan) 2024-04-11 16:45:46 +02:00
Eugen Rochko
75d64c3c31 New translations strings.xml (Belarusian) 2024-04-11 16:45:44 +02:00
Eugen Rochko
97b16e879f New translations strings.xml (Arabic) 2024-04-11 16:45:43 +02:00
Eugen Rochko
00db01d163 New translations strings.xml (Spanish) 2024-04-11 16:45:42 +02:00
Eugen Rochko
56610538ce New translations strings.xml (French) 2024-04-11 16:45:41 +02:00
Eugen Rochko
d555f5d3e4 New translations strings.xml (Romanian) 2024-04-11 16:45:40 +02:00
Eugen Rochko
89d5b8fb69 New translations strings.xml (Lithuanian) 2024-04-11 16:45:39 +02:00
Eugen Rochko
69e36b0f71 New translations strings.xml (Vietnamese) 2024-04-11 16:45:38 +02:00
Eugen Rochko
9b5ef3ad33 New translations strings.xml (Armenian) 2024-04-11 16:45:37 +02:00
Eugen Rochko
ec5dd81018 New translations strings.xml (Persian) 2024-04-11 16:45:36 +02:00
Grishka
72f546ed15 update string 2024-04-11 17:42:18 +03:00
Gregory K
0b48414715 Merge pull request #824 from FineFindus/fix/featured-hastag-crash
fix: include account when opening FeaturedHashtags
2024-04-10 17:25:56 +03:00
FineFindus
ca67c1eaca fix: include account when opening FeaturedHashtags
Closes https://github.com/mastodon/mastodon-android/issues/803.
2024-04-10 16:16:11 +02:00
Eugen Rochko
4dd5a80ef2 New translations strings.xml (Persian) 2024-04-09 12:29:17 +02:00
Eugen Rochko
c9f211807a New translations strings.xml (Lithuanian) 2024-04-09 05:53:52 +02:00
Eugen Rochko
440c15d9fa New translations full_description.txt (Lithuanian) 2024-04-07 22:44:40 +02:00
Eugen Rochko
86a443c39f New translations strings.xml (Lithuanian) 2024-04-07 22:44:39 +02:00
Eugen Rochko
a8d86db57f New translations strings.xml (Lithuanian) 2024-04-07 21:46:44 +02:00
Eugen Rochko
8359b48285 New translations strings.xml (Persian) 2024-04-04 13:08:30 -04:00
Grishka
36f4770cae Media layout: improve the case of two horizontal attachments 2024-04-02 05:09:55 +03:00
Eugen Rochko
932cf91800 New translations strings.xml (Vietnamese) 2024-03-29 22:27:06 -04:00
Eugen Rochko
5cf02e66b7 New translations strings.xml (Armenian) 2024-03-29 03:51:21 -04:00
Grishka
b7251972a8 Show the profile header view if we know the username 2024-03-28 23:00:03 +03:00
Eugen Rochko
006a423d5c New translations strings.xml (Greek) 2024-03-27 16:29:16 -04:00
Eugen Rochko
374b1edc81 New translations strings.xml (Basque) 2024-03-27 13:16:30 -04:00
Eugen Rochko
0894549687 New translations strings.xml (Basque) 2024-03-27 11:50:23 -04:00
Eugen Rochko
cc5963cc34 New translations strings.xml (Persian) 2024-03-26 14:19:20 +01:00
Eugen Rochko
fffe77501d New translations strings.xml (Greek) 2024-03-26 12:17:29 +01:00
Eugen Rochko
3443c2ae82 New translations strings.xml (Slovenian) 2024-03-25 13:25:24 +01:00
Eugen Rochko
01324af544 New translations strings.xml (Icelandic) 2024-03-25 11:23:46 +01:00
Eugen Rochko
d56b1fe89b New translations strings.xml (Icelandic) 2024-03-25 09:14:16 +01:00
Eugen Rochko
92457d54df New translations strings.xml (Icelandic) 2024-03-24 22:36:45 +01:00
Eugen Rochko
4d2e30ff85 New translations strings.xml (French) 2024-03-24 17:29:05 +01:00
Eugen Rochko
1d0c279956 New translations strings.xml (Greek) 2024-03-24 16:28:56 +01:00
Eugen Rochko
0202ca5b23 New translations strings.xml (Hungarian) 2024-03-23 21:34:53 +01:00
Eugen Rochko
5e8ebeadc3 New translations strings.xml (Thai) 2024-03-23 19:14:41 +01:00
Eugen Rochko
8bfb0c45a8 New translations strings.xml (Thai) 2024-03-23 18:05:28 +01:00
Eugen Rochko
2c1ecf32ad New translations strings.xml (Thai) 2024-03-23 17:07:59 +01:00
Eugen Rochko
9cc4bd722d New translations full_description.txt (Indonesian) 2024-03-23 08:38:04 +01:00
Eugen Rochko
d5f1e091b8 New translations strings.xml (Indonesian) 2024-03-23 08:38:03 +01:00
Eugen Rochko
439f3b44cb New translations strings.xml (Indonesian) 2024-03-23 07:33:06 +01:00
Eugen Rochko
e07bd39e95 New translations strings.xml (Chinese Simplified) 2024-03-23 05:48:47 +01:00
Eugen Rochko
11fe2ba2a4 New translations strings.xml (Hungarian) 2024-03-22 20:50:32 +01:00
Eugen Rochko
2b0e507f45 New translations strings.xml (Dutch) 2024-03-22 18:48:03 +01:00
Eugen Rochko
84bad0aa6c New translations strings.xml (Dutch) 2024-03-22 17:50:30 +01:00
Eugen Rochko
b41cda84e4 New translations strings.xml (Hungarian) 2024-03-22 14:19:35 +01:00
Eugen Rochko
0ae6fb2833 New translations strings.xml (Hungarian) 2024-03-22 11:26:06 +01:00
Eugen Rochko
21b6a1f4ef New translations strings.xml (Hungarian) 2024-03-22 09:52:08 +01:00
Eugen Rochko
69f9da4be4 New translations strings.xml (Indonesian) 2024-03-22 03:15:21 +01:00
Eugen Rochko
032db0921d New translations strings.xml (Italian) 2024-03-22 02:20:19 +01:00
Eugen Rochko
eef4601ce2 New translations strings.xml (Chinese Traditional) 2024-03-22 01:20:13 +01:00
Eugen Rochko
71701048f5 New translations strings.xml (Italian) 2024-03-22 01:20:12 +01:00
Eugen Rochko
2361bb7682 New translations strings.xml (Chinese Traditional) 2024-03-21 22:55:40 +01:00
Eugen Rochko
0b1b8c5c5a New translations strings.xml (Slovenian) 2024-03-21 22:55:36 +01:00
Eugen Rochko
fb885b0e00 New translations strings.xml (Hungarian) 2024-03-21 22:55:18 +01:00
Grishka
a2dec4f7cf Notification requests tweaks 2024-03-22 00:49:42 +03:00
Eugen Rochko
521797d070 New translations strings.xml (Hungarian) 2024-03-21 20:51:30 +01:00
Eugen Rochko
d2922dc226 New translations strings.xml (Hungarian) 2024-03-21 19:20:59 +01:00
Eugen Rochko
ecc2b675d5 New translations strings.xml (Persian) 2024-03-21 14:09:21 +01:00
Eugen Rochko
5a49b650b0 New translations strings.xml (Chinese Traditional) 2024-03-21 02:31:23 +01:00
Eugen Rochko
5fd57caabf New translations strings.xml (Slovenian) 2024-03-21 00:57:01 +01:00
Eugen Rochko
bad6afc543 New translations strings.xml (Slovenian) 2024-03-20 23:53:49 +01:00
Eugen Rochko
ffaa036115 New translations strings.xml (Urdu (India)) 2024-03-20 21:23:27 +01:00
Eugen Rochko
65133e969e New translations strings.xml (Kabyle) 2024-03-20 21:23:26 +01:00
Eugen Rochko
267ee4e03e New translations strings.xml (Igbo) 2024-03-20 21:23:25 +01:00
Eugen Rochko
db972ae421 New translations strings.xml (Occitan) 2024-03-20 21:23:24 +01:00
Eugen Rochko
ea93dd5b2d New translations strings.xml (Scottish Gaelic) 2024-03-20 21:23:23 +01:00
Eugen Rochko
c4d738844e New translations strings.xml (Sinhala) 2024-03-20 21:23:22 +01:00
Eugen Rochko
f124d2cabc New translations strings.xml (Bosnian) 2024-03-20 21:23:21 +01:00
Eugen Rochko
022038878b New translations strings.xml (Filipino) 2024-03-20 21:23:20 +01:00
Eugen Rochko
c6a846c602 New translations strings.xml (Burmese) 2024-03-20 21:23:18 +01:00
Eugen Rochko
31986a1ce5 New translations strings.xml (Croatian) 2024-03-20 21:23:17 +01:00
Eugen Rochko
04eea3b6e4 New translations strings.xml (Thai) 2024-03-20 21:23:16 +01:00
Eugen Rochko
09a5482df5 New translations strings.xml (Bengali) 2024-03-20 21:23:15 +01:00
Eugen Rochko
ba8e5a03ea New translations strings.xml (Indonesian) 2024-03-20 21:23:14 +01:00
Eugen Rochko
ddb3c34078 New translations strings.xml (Portuguese, Brazilian) 2024-03-20 21:23:13 +01:00
Eugen Rochko
8d4daa5d00 New translations strings.xml (Icelandic) 2024-03-20 21:23:12 +01:00
Eugen Rochko
65e1787987 New translations strings.xml (Galician) 2024-03-20 21:23:11 +01:00
Eugen Rochko
202f41b34b New translations strings.xml (Vietnamese) 2024-03-20 21:23:09 +01:00
Eugen Rochko
10497f358e New translations strings.xml (Chinese Traditional) 2024-03-20 21:23:08 +01:00
Eugen Rochko
ffe0dafbdc New translations strings.xml (Chinese Simplified) 2024-03-20 21:23:06 +01:00
Eugen Rochko
5828be28e8 New translations strings.xml (Ukrainian) 2024-03-20 21:23:05 +01:00
Eugen Rochko
b4e0605016 New translations strings.xml (Turkish) 2024-03-20 21:23:04 +01:00
Eugen Rochko
da7686b9b3 New translations strings.xml (Swedish) 2024-03-20 21:23:03 +01:00
Eugen Rochko
651d5ae56a New translations strings.xml (Slovenian) 2024-03-20 21:23:02 +01:00
Eugen Rochko
570d8ce7eb New translations strings.xml (Portuguese) 2024-03-20 21:23:01 +01:00
Eugen Rochko
a9491e22e4 New translations strings.xml (Norwegian) 2024-03-20 21:23:00 +01:00
Eugen Rochko
1ba31afa23 New translations strings.xml (Lithuanian) 2024-03-20 21:22:58 +01:00
Eugen Rochko
8f30d0d468 New translations strings.xml (Korean) 2024-03-20 21:22:57 +01:00
Eugen Rochko
4a8cea262b New translations strings.xml (Georgian) 2024-03-20 21:22:56 +01:00
Eugen Rochko
e25574ce9a New translations strings.xml (Japanese) 2024-03-20 21:22:55 +01:00
Eugen Rochko
9773c1cb98 New translations strings.xml (Italian) 2024-03-20 21:22:54 +01:00
Eugen Rochko
6d3eafe9e8 New translations strings.xml (Armenian) 2024-03-20 21:22:53 +01:00
Eugen Rochko
19acab9d18 New translations strings.xml (Hebrew) 2024-03-20 21:22:52 +01:00
Eugen Rochko
bfcda1d73c New translations strings.xml (Irish) 2024-03-20 21:22:51 +01:00
Eugen Rochko
600fc7939e New translations strings.xml (Finnish) 2024-03-20 21:22:50 +01:00
Eugen Rochko
1189aaae4f New translations strings.xml (Basque) 2024-03-20 21:22:49 +01:00
Eugen Rochko
e9c8e8d764 New translations strings.xml (German) 2024-03-20 21:22:48 +01:00
Eugen Rochko
3e90620fcc New translations strings.xml (Danish) 2024-03-20 21:22:47 +01:00
Eugen Rochko
62a364a110 New translations strings.xml (Czech) 2024-03-20 21:22:47 +01:00
Eugen Rochko
fbbbe99bf4 New translations strings.xml (Arabic) 2024-03-20 21:22:45 +01:00
Eugen Rochko
d078ccc78c New translations strings.xml (Spanish) 2024-03-20 21:22:44 +01:00
Eugen Rochko
cbacb6568e New translations strings.xml (French) 2024-03-20 21:22:43 +01:00
Eugen Rochko
65f220b570 New translations strings.xml (Romanian) 2024-03-20 21:22:42 +01:00
Eugen Rochko
22d83d831d New translations strings.xml (Dutch) 2024-03-20 21:22:41 +01:00
Eugen Rochko
ac564a67ca New translations strings.xml (Persian) 2024-03-20 21:22:40 +01:00
Eugen Rochko
d27a8dc29c New translations strings.xml (Catalan) 2024-03-20 21:22:39 +01:00
Eugen Rochko
568dfe911e New translations strings.xml (Belarusian) 2024-03-20 21:22:38 +01:00
Eugen Rochko
64a647ca84 New translations strings.xml (Hindi) 2024-03-20 21:22:37 +01:00
Eugen Rochko
6b3c3ac9b0 New translations strings.xml (Greek) 2024-03-20 21:22:36 +01:00
Eugen Rochko
6438df92c6 New translations strings.xml (Russian) 2024-03-20 21:22:35 +01:00
Eugen Rochko
5f98fdfafc New translations strings.xml (Polish) 2024-03-20 21:22:34 +01:00
Eugen Rochko
4a1c8aadf8 New translations strings.xml (Hungarian) 2024-03-20 21:22:33 +01:00
Grishka
441567f9d2 Notification requests (AND-154) 2024-03-20 23:18:04 +03:00
Eugen Rochko
381fd434ad New translations strings.xml (Japanese) 2024-03-20 18:45:42 +01:00
Eugen Rochko
89f713899b New translations strings.xml (Japanese) 2024-03-20 17:16:49 +01:00
Eugen Rochko
a82c61791e New translations strings.xml (Thai) 2024-03-19 19:16:37 +01:00
Eugen Rochko
734c1ddab6 New translations strings.xml (Dutch) 2024-03-19 19:16:35 +01:00
Eugen Rochko
1d0b31e9de New translations strings.xml (Chinese Traditional) 2024-03-19 17:56:57 +01:00
Eugen Rochko
1441036475 New translations strings.xml (French) 2024-03-19 13:19:22 +01:00
Eugen Rochko
7a84352723 New translations strings.xml (Hungarian) 2024-03-19 13:19:21 +01:00
Eugen Rochko
6f153f3879 New translations strings.xml (Icelandic) 2024-03-19 11:06:29 +01:00
Grishka
f888091e22 Add unlisted visibility option as "quiet public"
closes #189, closes #103, closes #37
2024-03-18 20:34:23 +03:00
Grishka
e59cf2afca Make alt text selectable 2024-03-18 20:25:28 +03:00
Eugen Rochko
7516b8e662 New translations strings.xml (Persian) 2024-03-18 11:01:25 +01:00
Eugen Rochko
bb4a480250 New translations strings.xml (Persian) 2024-03-17 18:19:22 +01:00
Eugen Rochko
c6df18c456 New translations strings.xml (Persian) 2024-03-17 17:09:46 +01:00
Eugen Rochko
cb945998d3 New translations strings.xml (Persian) 2024-03-17 16:13:47 +01:00
Grishka
5622c93bd9 Fix fragment stack breaking after opening a notification 2024-03-17 04:24:12 +03:00
Eugen Rochko
7552227da0 New translations strings.xml (Dutch) 2024-03-16 19:18:36 +01:00
Eugen Rochko
1904fce32d New translations strings.xml (Greek) 2024-03-16 19:18:35 +01:00
Eugen Rochko
f1e5e572f4 New translations strings.xml (Kabyle) 2024-03-16 13:44:30 +01:00
Eugen Rochko
d8f83170be New translations strings.xml (Dutch) 2024-03-16 13:44:30 +01:00
Eugen Rochko
95c135b270 New translations strings.xml (Dutch) 2024-03-16 12:48:38 +01:00
Eugen Rochko
8408daf070 New translations strings.xml (Dutch) 2024-03-16 00:08:45 +01:00
Eugen Rochko
bd8eb6a034 New translations strings.xml (Slovenian) 2024-03-15 23:03:06 +01:00
Eugen Rochko
9c4d0ef85e New translations strings.xml (Dutch) 2024-03-15 23:03:05 +01:00
Eugen Rochko
1ee441314f New translations strings.xml (Slovenian) 2024-03-15 22:04:35 +01:00
Eugen Rochko
d5d1e51bbc New translations strings.xml (Dutch) 2024-03-15 19:48:48 +01:00
Eugen Rochko
84aa99ba88 New translations strings.xml (Dutch) 2024-03-15 18:23:19 +01:00
Eugen Rochko
23e02d2c24 New translations strings.xml (Dutch) 2024-03-15 17:10:51 +01:00
Eugen Rochko
3ea66c6c4c New translations strings.xml (Belarusian) 2024-03-15 11:12:27 +01:00
Eugen Rochko
50cf737db6 New translations strings.xml (Vietnamese) 2024-03-13 16:46:20 +01:00
Grishka
bf55b5a802 Swap poll options around 2024-03-13 17:13:25 +03:00
Grishka
49bf04c6c6 Tweak line height for posts
#791
2024-03-13 17:05:57 +03:00
Eugen Rochko
f5d64f3882 New translations strings.xml (Slovenian) 2024-03-12 12:31:29 +01:00
Eugen Rochko
48d7de53c0 New translations strings.xml (Georgian) 2024-03-12 10:00:34 +01:00
Eugen Rochko
d53bace4ce New translations strings.xml (Thai) 2024-03-11 20:09:04 +01:00
Eugen Rochko
02c800496c New translations strings.xml (Icelandic) 2024-03-11 18:48:27 +01:00
Eugen Rochko
0ce39946cb New translations strings.xml (Chinese Traditional) 2024-03-11 18:48:26 +01:00
Eugen Rochko
52b573d20f New translations strings.xml (Russian) 2024-03-11 16:19:01 +01:00
Grishka
5be6faa07c New posts button tweaks 2024-03-11 18:09:29 +03:00
Eugen Rochko
ee05e818d9 New translations strings.xml (Italian) 2024-03-11 13:45:02 +01:00
Eugen Rochko
29d5e4fa13 New translations strings.xml (Urdu (India)) 2024-03-11 12:06:58 +01:00
Eugen Rochko
9bd830b368 New translations strings.xml (Kabyle) 2024-03-11 12:06:56 +01:00
Eugen Rochko
7d70f816d1 New translations strings.xml (Igbo) 2024-03-11 12:06:55 +01:00
Eugen Rochko
0d0cf04b57 New translations strings.xml (Occitan) 2024-03-11 12:06:54 +01:00
Eugen Rochko
4a6e514b81 New translations strings.xml (Scottish Gaelic) 2024-03-11 12:06:53 +01:00
Eugen Rochko
b74f9092e7 New translations strings.xml (Sinhala) 2024-03-11 12:06:52 +01:00
Eugen Rochko
a061347d76 New translations strings.xml (Bosnian) 2024-03-11 12:06:50 +01:00
Eugen Rochko
71586b1100 New translations strings.xml (Filipino) 2024-03-11 12:06:49 +01:00
Eugen Rochko
809aa6afd2 New translations strings.xml (Burmese) 2024-03-11 12:06:48 +01:00
Eugen Rochko
0067a036ae New translations strings.xml (Croatian) 2024-03-11 12:06:47 +01:00
Eugen Rochko
794d3329fe New translations strings.xml (Thai) 2024-03-11 12:06:46 +01:00
Eugen Rochko
f96ed6c56f New translations strings.xml (Bengali) 2024-03-11 12:06:45 +01:00
Eugen Rochko
92d44eebe6 New translations strings.xml (Indonesian) 2024-03-11 12:06:44 +01:00
Eugen Rochko
93fe734636 New translations strings.xml (Portuguese, Brazilian) 2024-03-11 12:06:43 +01:00
Eugen Rochko
71a5c132f4 New translations strings.xml (Icelandic) 2024-03-11 12:06:42 +01:00
Eugen Rochko
15a514aca5 New translations strings.xml (Galician) 2024-03-11 12:06:41 +01:00
Eugen Rochko
71f74ced7d New translations strings.xml (Vietnamese) 2024-03-11 12:06:40 +01:00
Eugen Rochko
5c86c911c1 New translations strings.xml (Chinese Traditional) 2024-03-11 12:06:38 +01:00
Eugen Rochko
7aebc44062 New translations strings.xml (Chinese Simplified) 2024-03-11 12:06:37 +01:00
Eugen Rochko
6f6e1f1009 New translations strings.xml (Ukrainian) 2024-03-11 12:06:36 +01:00
Eugen Rochko
2e7f17b823 New translations strings.xml (Turkish) 2024-03-11 12:06:35 +01:00
Eugen Rochko
5194dae9a6 New translations strings.xml (Swedish) 2024-03-11 12:06:34 +01:00
Eugen Rochko
2db03669ce New translations strings.xml (Slovenian) 2024-03-11 12:06:33 +01:00
Eugen Rochko
a6c4f83973 New translations strings.xml (Portuguese) 2024-03-11 12:06:32 +01:00
Eugen Rochko
f625cea183 New translations strings.xml (Norwegian) 2024-03-11 12:06:31 +01:00
Eugen Rochko
d6ee9db6ff New translations strings.xml (Lithuanian) 2024-03-11 12:06:30 +01:00
Eugen Rochko
9ea94ce177 New translations strings.xml (Korean) 2024-03-11 12:06:29 +01:00
Eugen Rochko
8102163c5b New translations strings.xml (Georgian) 2024-03-11 12:06:27 +01:00
Eugen Rochko
1034c16bfb New translations strings.xml (Japanese) 2024-03-11 12:06:26 +01:00
Eugen Rochko
956f9547e7 New translations strings.xml (Italian) 2024-03-11 12:06:25 +01:00
Eugen Rochko
95fb241da1 New translations strings.xml (Armenian) 2024-03-11 12:06:24 +01:00
Eugen Rochko
7b06af4c8b New translations strings.xml (Hebrew) 2024-03-11 12:06:23 +01:00
Eugen Rochko
1ca0ff53c8 New translations strings.xml (Irish) 2024-03-11 12:06:22 +01:00
Eugen Rochko
1e0bdf44c2 New translations strings.xml (Finnish) 2024-03-11 12:06:21 +01:00
Eugen Rochko
c611d6386a New translations strings.xml (Basque) 2024-03-11 12:06:20 +01:00
Eugen Rochko
b326dc3bc2 New translations strings.xml (German) 2024-03-11 12:06:18 +01:00
Eugen Rochko
7f6e21450a New translations strings.xml (Danish) 2024-03-11 12:06:17 +01:00
Eugen Rochko
ee911a15c6 New translations strings.xml (Czech) 2024-03-11 12:06:16 +01:00
Eugen Rochko
aa0e05f085 New translations strings.xml (Arabic) 2024-03-11 12:06:15 +01:00
Eugen Rochko
f1fe078cf2 New translations strings.xml (Spanish) 2024-03-11 12:06:14 +01:00
Eugen Rochko
e53dcf27ec New translations strings.xml (French) 2024-03-11 12:06:13 +01:00
Eugen Rochko
6e9ce8d5a5 New translations strings.xml (Romanian) 2024-03-11 12:06:12 +01:00
Eugen Rochko
686d88557b New translations strings.xml (Dutch) 2024-03-11 12:06:11 +01:00
Eugen Rochko
b07f14d01b New translations strings.xml (Persian) 2024-03-11 12:06:09 +01:00
Eugen Rochko
c200a72031 New translations strings.xml (Catalan) 2024-03-11 12:06:08 +01:00
Eugen Rochko
2a8ff3e50a New translations strings.xml (Belarusian) 2024-03-11 12:06:07 +01:00
Eugen Rochko
06cde138c1 New translations strings.xml (Hindi) 2024-03-11 12:06:06 +01:00
Eugen Rochko
9fae62f289 New translations strings.xml (Greek) 2024-03-11 12:06:04 +01:00
Eugen Rochko
a1333929e9 New translations strings.xml (Russian) 2024-03-11 12:06:03 +01:00
Eugen Rochko
6a05fafe04 New translations strings.xml (Polish) 2024-03-11 12:06:02 +01:00
Eugen Rochko
13c6fc60f8 New translations strings.xml (Hungarian) 2024-03-11 12:06:01 +01:00
Grishka
ff7948ad83 Add conversation muting 2024-03-11 13:31:32 +03:00
Grishka
3972ab207c New post notifications (AND-151) 2024-03-11 13:18:45 +03:00
Gregory K
33a8f1dab4 Merge pull request #795 from Arthur-GYT/per-app-language
Enable auto generated per-app language file
2024-03-11 12:06:02 +03:00
Eugen Rochko
27f261ae4a New translations strings.xml (Armenian) 2024-03-10 20:52:20 +01:00
Eugen Rochko
ef59331dd3 New translations strings.xml (Armenian) 2024-03-10 19:14:55 +01:00
Eugen Rochko
b019731249 New translations strings.xml (French) 2024-03-10 10:28:55 +01:00
Arthur-GYT
47aa7fc191 Enable auto generated per-app language file 2024-03-09 19:07:04 +01:00
Eugen Rochko
9356b26dfd New translations strings.xml (Scottish Gaelic) 2024-03-09 11:27:32 +01:00
Eugen Rochko
41b626ddbd New translations strings.xml (Polish) 2024-03-08 18:16:47 +01:00
Eugen Rochko
f76b41581b New translations strings.xml (Polish) 2024-03-08 17:10:58 +01:00
Eugen Rochko
14f08b7759 New translations strings.xml (French) 2024-03-08 15:45:22 +01:00
Eugen Rochko
8c1cec09d6 New translations strings.xml (Russian) 2024-03-08 13:20:37 +01:00
Eugen Rochko
15c10cb14c New translations strings.xml (Armenian) 2024-03-07 22:56:57 +01:00
Eugen Rochko
3b2f68a400 New translations strings.xml (Armenian) 2024-03-07 21:17:50 +01:00
Eugen Rochko
2a9e4e0b82 New translations strings.xml (Turkish) 2024-03-07 17:30:18 +01:00
241 changed files with 9029 additions and 1772 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.2
ruby-version: 3.3.0
bundler-cache: true
- name: Set up Android SDK

218
Gemfile.lock Normal file
View File

@@ -0,0 +1,218 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.913.0)
aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.78.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.4.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.5)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-22
ruby
DEPENDENCIES
fastlane
BUNDLED WITH
2.5.4

View File

@@ -0,0 +1,2 @@
- Updated look for link previews and the News tab
- Bug fixes and improvements

View File

@@ -0,0 +1 @@
- Added option to donate to Mastodon in-app when connected to our flagship server. This option will not appear when using a 3rd party server to avoid confusion. Please check if the 3rd party server you're using accepts donations through their website.

View File

@@ -0,0 +1,8 @@
- Adjust filters in the Notifications tab to silence unwanted alerts*
- Opt into push notifications when a user posts by tapping the bell 🔔 in the top corner of a user's profile.
- Mute overly active conversation notifications via the More button ⋮ on your posts
- When writing a post, choose Quiet public 🌙 visibility to avoid appearing in feeds or searches
- Improved post legibility with adjusted line height
- Bug fixes
*Your server must support filtered notifications to see this option.

View File

@@ -0,0 +1 @@
110.txt

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,4 +1,4 @@
Mastodon est le plus grand réseau social décentralisé sur Internet. Au lieu dun site Web unique, cest un réseau de millions dutilisateurs dans des communautés indépendantes qui peuvent tous interagir les uns avec les autres, de manière transparente. Peu importe ce que vous êtes, vous pouvez rencontrer des gens passionnés qui publient à ce sujet sur Mastodon !
Mastodon est le plus grand réseau social décentralisé sur Internet. Au lieu dun site Web unique, cest un réseau de millions dutilisateurs dans des communautés indépendantes qui peuvent tous interagir les uns avec les autres, de manière transparente. Peu importe ce que vous aimez, vous pouvez rencontrer des gens passionnés qui en parlent sur Mastodon !
Rejoignez une communauté et créez votre profil. Trouvez et suivez des personnes fascinantes et lisez leurs messages chronologiquement et sans publicité. Exprimez-vous avec des émojis personnalisés, des images, des GIFs, des vidéos et de laudio dans des messages de 500 caractères. Répondez aux sujets de discussions et aux reblogues de nimporte qui pour partager des choses géniales. Trouvez de nouveaux comptes à suivre et des hashtags tendance pour étendre votre réseau.

View File

@@ -1,16 +1,16 @@
A Mastodon a legnagyobb decentralizált közösségi hálózat az interneten. Egyetlen weboldal helyett, ez több millió felhasználóból álló, független közösségek hálózata, amelyek egymással kapcsolatba tudnak lépni, zökkenőmentesen. Nem számít, mi az érdeklődésed, a Mastodonon találkozhatsz róla posztoló lelkes emberekkel!
Csatlakozz egy közösséghez és készítsd el a profilodat. Találj meg és kövess be fantasztikus embereket, olvasd el a bejegyzéseiket reklámmentesen, időrendben. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú bejegyzésekben. Válaszolj szálakban, told meg bárki bejegyzését, hogy megoszthass szuper dolgokat. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat.
Csatlakozz egy közösséghez és készítsd el a profilodat. Találj meg és kövess fantasztikus embereket, olvasd el a bejegyzéseiket reklámmentesen, időrendben. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú bejegyzésekben. Válaszolj szálakban, told meg bárki bejegyzését, hogy megoszthass szuper dolgokat. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat.
A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a bejegyzéseidet csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. A tartalomfigyelmeztetések elrejthetővé teszik az érzékeny vagy ingerlő tartalmakat addig, amíg nem vagy kész azok megtekintésére. Minden közösségnek saját irényelvei és moderátorai vannak arra, hogy biztonságban tudják a tagjaikat. Erőteljes letiltási és bejelentési eszközök segítik a visszaélések megelőzését.
A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a bejegyzéseidet csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. A tartalmi figyelmeztetések elrejthetővé teszik az érzékeny vagy ingerlő tartalmakat addig, amíg nem vagy kész azok megtekintésére. Minden közösségnek saját irányelvei és moderátorai vannak arra, hogy biztonságban tudják a tagjaikat. Robusztus letiltási és jelentési eszközök segítik a visszaélések megelőzését.
További funkciók:
• Sötét mód: Olvasd a bejegyzéseket világos, sötét vagy teljesen fekete módban
• Sötét mód: olvasd a bejegyzéseket világos, sötét vagy valódi fekete módban
• Szavazás: Kérd ki a követőid véleményét és összesítsd a szavazataikat
• Felfedezés: A felkapott hashtagek és fiókok egyetlen kattintásra vannak
• Értesítések: Értesülj az új követőidről, válaszokról, megtolásokról
• Megosztás: Írj bejegyzést Mastodonra bármely app megosztási funkciójával
• Cukiság: A kabalánk egy cuki elefánt, mely fel fog bukkanni időről időre
A Mastodon egy bejegyzett non-profit szervezet, a fejlesztés közvetlenül a te adományaidból történik. Nincs hirdetés, se monetizáció, se kockázati tőke, és ez így is fog maradni.
A Mastodon egy bejegyzett nonprofit szervezet, a fejlesztés közvetlenül a felhasználók adományaiból történik. Nincs hirdetés, se monetizáció, se kockázati tőke, és ez így is fog maradni.

View File

@@ -0,0 +1,16 @@
Mastodon es le rete social decentralisate le plus grande sur internet. In vice que un sol sito web, illo es un rete de milliones de usatores in communitates independente que pote interager le unes con le alteres, transparentemente. Non importa lo que te interessa, tu pote incontrar personas passionate que parla de illo sur Mastodon!
Inscribe te a un communitate e crea tu profilo. Trova e seque personas fascinante e lege lor messages in un chronologia libere de publicitate. Exprime te con emojis personalisate, imagines, GIFs, videos e audio in messages de 500 characteres. Responde a discussiones e impulsa messages de quicunque pro compartir cosas genial. Trova nove contos a sequer e hashtags de tendentia pro expander tu rete.
Mastodon es producite con attention a confidentialitate e securitate. Decide si tu messages es compartite con tu sequitores, con solo le gente que tu mentiona, o con tote le mundo. Le advertimentos de contento te permitte celar messages que contine material sensibile o provocatori usque tu es preste a interager con illos. Cata communitate ha su proprie directivas e moderatores pro mantener su membros secur, e robuste utensiles de blocage e de reportage pro adjutar a impedir abusos.
Altere functiones:
• Modo obscur: lege messages in modo clar, obscur, o vermente nigre
• Sondages: Demanda al sequitores lor opinion e conta le votos
• Explorar: le hashtags e contos popular es a portata de mano
• Notificationes: sia notificate de nove sequitores, responsas e impulsos
• Compartir: publica directemente sur Mastodon desde le function Compartir de qualcunque app
• Pachydermo: nostre mascotte es un adorabile elephante, e tu lo videra apparer de tempore in tempore
Mastodon es un organisation registrate sin scopo lucrative e le disveloppamento es sustenite directemente per tu donationes. Il non ha publicitate, ni monetisation, ni capital de risco, e nos intende mantener lo assi.

View File

@@ -0,0 +1 @@
Rete social decentralisate

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -1,6 +1,6 @@
Mastodon dalah jaringan sosial terdesentralisasi terbesar di internet. Bukan hanya satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat saling interaksi antar sesama, tanpa batasan. Apapun yang kamu minati, kamu dapat bertemu orang-orang baru yang mengirimkan apa yang mereka minati di Mastodon!
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam linimasa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam lini masa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.

View File

@@ -1,16 +1,16 @@
Mastodon didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
Mastodon tai didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon“ platformoje gali sutikti aistringų žmonių, skelbiančių apie tai!
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perdalyk bet kurio asmens įrašus, kad galėtum bendrinti puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
Daugiau funkcijų:
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
• Naršyti: tendencingos saitažodžiai ir paskyros vos nuo vieno prisilietimo
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu.
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus.
• Naršyti: tendencingos saitažodžiai ir paskyros vos nuo vieno prisilietimo.
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir pasidalinimus.
• Bendrinti: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
• Mielumas: mūsų talismanas žavus drambliukas, kurį retkarčiais pamatysi.
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.
Mastodon registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokių reklamų, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.

View File

@@ -1,6 +1,6 @@
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个单一的网站,而是由一个个独立社区中的数百万用户组成的网络,所有这些用户都可以无缝交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
加入一个社区节点并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频在最多 500 字的帖文中表达自我。 通过回复或转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
加入一个社区并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频在最多 500 字的帖文中表达自我。 通过回复或转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分享分享对象,可以是你的关注者、你提到的人或是整个世界。 在你做好充足的互动准备之前,内容警告可以隐藏包含敏感或刺激内容的帖文。 每个社区都有自己的规则和管理员来保证其成员安全,同时还有强力的屏蔽和举报工具来避免滥用。
@@ -10,7 +10,7 @@ Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分
• 投票:询问关注者的意见并统计他们的投票
• 探索:热门的话题标签及账号只有一触之遥
• 通知:获取关注、回复和转发相关的通知提醒
• 分享:从其他应用的分享菜单直接发布到 Mastodon
• 分享:从任意应用的分享菜单直接发布到 Mastodon
• 吉祥物:你会不时地看到我们可爱的长毛象
Mastodon 是一个直接由用户捐赠支持、已注册非营利开发项目。 它没有广告、没有商业化,也没有风险资本,并且我们也计划保持这种方式。

View File

@@ -4,15 +4,18 @@ plugins {
}
android {
compileSdk 33
androidResources {
generateLocaleConfig = true
}
compileSdk 34
defaultConfig {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 89
versionName "2.4.1"
targetSdk 34
versionCode 111
versionName "2.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -87,7 +90,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.16'
implementation 'me.grishka.appkit:appkit:1.3.0'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'
@@ -98,7 +101,7 @@ dependencies {
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
def appCenterSdkVersion = "4.4.2"
def appCenterSdkVersion = "5.0.4"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"

View File

@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
@@ -31,10 +32,11 @@
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:largeHeap="true">
android:largeHeap="true"
android:enableOnBackInvokedCallback="true">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
@@ -78,8 +80,10 @@
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".DonationFragmentActivity" android:exported="false" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"/>
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
<service android:name=".NotificationActionHandlerService" android:exported="false"/>
<receiver android:name=".PushNotificationReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>

View File

@@ -88,8 +88,13 @@ public class AudioPlayerService extends Service{
nm=getSystemService(NotificationManager.class);
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE), RECEIVER_EXPORTED);
registerReceiver(receiver, new IntentFilter(ACTION_STOP), RECEIVER_EXPORTED);
}else{
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
}
instance=this;
}

View File

@@ -0,0 +1,29 @@
package org.joinmastodon.android;
import android.os.Bundle;
import org.joinmastodon.android.fragments.DonationWebViewFragment;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
// This exists because our designer wanted to avoid extra sheet showing/hiding animations.
// This is the only way to show a fragment on top of a sheet without having to rewrite way too many things.
public class DonationFragmentActivity extends FragmentStackActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
DonationWebViewFragment fragment=new DonationWebViewFragment();
fragment.setArguments(getIntent().getBundleExtra("fragmentArgs"));
showFragment(fragment);
overridePendingTransition(R.anim.fragment_enter, R.anim.no_op_300ms);
}
}
@Override
public void finish(){
super.finish();
overridePendingTransition(0, R.anim.fragment_exit);
}
}

View File

@@ -10,7 +10,7 @@ public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
public static ThemePreference theme;
public static ThemePreference theme=ThemePreference.AUTO;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);

View File

@@ -61,6 +61,7 @@ public class MainActivity extends FragmentStackActivity{
@Override
protected void onNewIntent(Intent intent){
super.onNewIntent(intent);
setIntent(intent);
if(intent.getBooleanExtra("fromNotification", false)){
String accountID=intent.getStringExtra("accountID");
AccountSession accountSession;
@@ -85,6 +86,8 @@ public class MainActivity extends FragmentStackActivity{
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}else if(intent.getBooleanExtra("explore", false)){
restartHomeFragment();
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
}*/
@@ -152,6 +155,11 @@ public class MainActivity extends FragmentStackActivity{
}
fragment.setArguments(args);
showFragment(fragment);
Intent intent=getIntent();
intent.removeExtra("fromNotification");
intent.removeExtra("notification");
intent.removeExtra("accountID");
setIntent(intent);
}
private void showCompose(){
@@ -206,6 +214,8 @@ public class MainActivity extends FragmentStackActivity{
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(intent.getBooleanExtra("explore", false) && fragment instanceof HomeFragment hf){
getWindow().getDecorView().post(()->hf.setCurrentTab(R.id.tab_search));
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}else{
@@ -213,4 +223,10 @@ public class MainActivity extends FragmentStackActivity{
}
}
}
public Fragment getTopmostFragment(){
if(fragmentContainers.isEmpty())
return null;
return getFragmentManager().findFragmentById(fragmentContainers.get(fragmentContainers.size()-1).getId());
}
}

View File

@@ -0,0 +1,177 @@
package org.joinmastodon.android;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.service.notification.StatusBarNotification;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.util.UUID;
import androidx.annotation.Nullable;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class NotificationActionHandlerService extends Service{
private static final String TAG="NotificationActionHandl";
private int runningRequestCount=0;
@Nullable
@Override
public IBinder onBind(Intent intent){
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId){
String action=intent.getStringExtra("action");
String account=intent.getStringExtra("account");
String postID=intent.getStringExtra("post");
String notificationTag=intent.getStringExtra("notificationTag");
if(action==null || account==null || postID==null || notificationTag==null){
maybeStopSelf();
return START_NOT_STICKY;
}
NotificationManager nm=getSystemService(NotificationManager.class);
StatusBarNotification notification=findNotification(notificationTag);
if("reply".equals(action)){
Bundle remoteInputResults=RemoteInput.getResultsFromIntent(intent);
if(remoteInputResults==null){
maybeStopSelf();
return START_NOT_STICKY;
}
CharSequence replyText=remoteInputResults.getCharSequence("replyText");
if(replyText==null){
maybeStopSelf();
return START_NOT_STICKY;
}
CreateStatus.Request req=new CreateStatus.Request();
req.inReplyToId=postID;
req.status=intent.getStringExtra("replyPrefix")+replyText;
req.visibility=StatusPrivacy.valueOf(intent.getStringExtra("visibility"));
runningRequestCount++;
new CreateStatus(req, UUID.randomUUID().toString())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
E.post(new StatusCreatedEvent(result, account));
if(notification!=null){
Notification n=notification.getNotification();
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
}
runningRequestCount--;
maybeStopSelf();
}
@Override
public void onError(ErrorResponse error){
error.showToast(NotificationActionHandlerService.this);
if(notification!=null){
Notification n=notification.getNotification();
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
}
runningRequestCount--;
maybeStopSelf();
}
})
.exec(account);
}else if("favorite".equals(action)){
PendingIntent prevActionIntent;
if(notification!=null){
Notification n=notification.getNotification();
prevActionIntent=n.actions[1].actionIntent;
n.actions[1].actionIntent=null;
n.actions[1].title=getString(R.string.button_favorited);
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
}else{
prevActionIntent=null;
}
runningRequestCount++;
new SetStatusFavorited(postID, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
E.post(new StatusCountersUpdatedEvent(result));
runningRequestCount--;
maybeStopSelf();
}
@Override
public void onError(ErrorResponse error){
if(notification!=null){
Notification n=notification.getNotification();
n.actions[1].actionIntent=prevActionIntent;
n.actions[1].title=getString(R.string.button_favorite);
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
}
error.showToast(NotificationActionHandlerService.this);
runningRequestCount--;
maybeStopSelf();
}
})
.exec(account);
}else if("boost".equals(action)){
PendingIntent prevActionIntent;
if(notification!=null){
Notification n=notification.getNotification();
prevActionIntent=n.actions[2].actionIntent;
n.actions[2].actionIntent=null;
n.actions[2].title=getString(R.string.button_reblogged);
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
}else{
prevActionIntent=null;
}
runningRequestCount++;
new SetStatusReblogged(postID, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
E.post(new StatusCountersUpdatedEvent(result));
runningRequestCount--;
maybeStopSelf();
}
@Override
public void onError(ErrorResponse error){
if(notification!=null){
Notification n=notification.getNotification();
n.actions[2].actionIntent=prevActionIntent;
n.actions[2].title=getString(R.string.button_reblog);
nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
}
error.showToast(NotificationActionHandlerService.this);
runningRequestCount--;
maybeStopSelf();
}
})
.exec(account);
}
return START_NOT_STICKY;
}
private void maybeStopSelf(){
if(runningRequestCount==0)
stopSelf();
}
private StatusBarNotification findNotification(String tag){
for(StatusBarNotification sbn:getSystemService(NotificationManager.class).getActiveNotifications()){
if(tag.equals(sbn.getTag())){
return sbn;
}
}
return null;
}
}

View File

@@ -5,14 +5,15 @@ import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
@@ -21,10 +22,13 @@ import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
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.Mention;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -75,7 +79,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
new GetNotificationByID(pn.notificationId+"")
new GetNotificationByID(pn.notificationId)
.setCallback(new Callback<>(){
@Override
public void onSuccess(org.joinmastodon.android.model.Notification result){
@@ -144,19 +148,114 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setContentText(pn.body)
.setStyle(new Notification.BigTextStyle().bigText(pn.body))
.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setContentIntent(PendingIntent.getActivity(context, (accountID+pn.notificationId).hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
.setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setLights(context.getColor(R.color.primary_700), 500, 1000)
.setColor(context.getColor(R.color.primary_700));
.setColor(context.getColor(R.color.primary_700))
.setGroup(accountID);
if(avatar!=null){
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
}
if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){
builder.setSubText(accountName);
}
nm.notify(accountID, NOTIFICATION_ID, builder.build());
String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id);
if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){
ArrayList<String> mentions=new ArrayList<>();
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!notification.status.account.id.equals(ownID))
mentions.add('@'+notification.status.account.acct);
for(Mention mention:notification.status.mentions){
if(mention.id.equals(ownID))
continue;
String m='@'+mention.acct;
if(!mentions.contains(m))
mentions.add(m);
}
String replyPrefix=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
Intent replyIntent=new Intent(context, NotificationActionHandlerService.class);
replyIntent.putExtra("action", "reply");
replyIntent.putExtra("account", accountID);
replyIntent.putExtra("post", notification.status.id);
replyIntent.putExtra("notificationTag", notificationTag);
replyIntent.putExtra("visibility", notification.status.visibility.toString());
replyIntent.putExtra("replyPrefix", replyPrefix);
builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px),
context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE))
.addRemoteInput(new RemoteInput.Builder("replyText").setLabel(context.getString(R.string.button_reply)).build())
.build());
}
Intent favIntent=new Intent(context, NotificationActionHandlerService.class);
favIntent.putExtra("action", "favorite");
favIntent.putExtra("account", accountID);
favIntent.putExtra("post", notification.status.id);
favIntent.putExtra("notificationTag", notificationTag);
builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_star_24px),
context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)).build());
PendingIntent boostActionIntent;
if(notification.status.visibility!=StatusPrivacy.DIRECT){
Intent boostIntent=new Intent(context, NotificationActionHandlerService.class);
boostIntent.putExtra("action", "boost");
boostIntent.putExtra("account", accountID);
boostIntent.putExtra("post", notification.status.id);
boostIntent.putExtra("notificationTag", notificationTag);
boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}else{
boostActionIntent=null;
}
builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_boost_24px),
context.getString(R.string.button_reblog), boostActionIntent).build());
}
nm.notify(notificationTag, NOTIFICATION_ID, builder.build());
StatusBarNotification[] activeNotifications=nm.getActiveNotifications();
ArrayList<String> summaryLines=new ArrayList<>();
int notificationCount=0;
for(StatusBarNotification sbn:activeNotifications){
String tag=sbn.getTag();
if(tag!=null && tag.startsWith(accountID+"_")){
if((sbn.getNotification().flags & Notification.FLAG_GROUP_SUMMARY)==0){
if(summaryLines.size()<5){
summaryLines.add(sbn.getNotification().extras.getString("android.title"));
}
notificationCount++;
}
}
}
if(summaryLines.size()>1){
Notification.Builder summaryBuilder;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
summaryBuilder=new Notification.Builder(context, accountID+"_"+pn.notificationType);
}else{
summaryBuilder=new Notification.Builder(context)
.setPriority(Notification.PRIORITY_DEFAULT);
}
Notification.InboxStyle inboxStyle=new Notification.InboxStyle();
for(String line:summaryLines){
inboxStyle.addLine(line);
}
summaryBuilder.setContentTitle(context.getString(R.string.app_name))
.setContentText(context.getResources().getQuantityString(R.plurals.x_new_notifications, notificationCount, notificationCount))
.setSmallIcon(R.drawable.ic_ntf_logo)
.setColor(context.getColor(R.color.primary_700))
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
.setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
.setGroup(accountID)
.setGroupSummary(true)
.setStyle(inboxStyle.setSummaryText(accountName));
nm.notify(accountID+"_summary", NOTIFICATION_ID, summaryBuilder.build());
}
}
}

View File

@@ -24,7 +24,6 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
@@ -45,8 +44,8 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=3;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
public static final WorkerThread databaseThread=new WorkerThread("databaseThread");
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
private final String accountID;
private DatabaseHelper db;
@@ -467,9 +466,4 @@ public class CacheController{
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
}
}
@FunctionalInterface
private interface DatabaseRunnable{
void run(SQLiteDatabase db) throws IOException;
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api;
import android.database.sqlite.SQLiteDatabase;
import java.io.IOException;
@FunctionalInterface
public interface DatabaseRunnable{
void run(SQLiteDatabase db) throws IOException;
}

View File

@@ -12,10 +12,12 @@ import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.time.Instant;
@@ -29,6 +31,8 @@ import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.WorkerThread;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
@@ -49,8 +53,11 @@ public class MastodonAPIController{
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.cache(new Cache(new File(MastodonApp.context.getCacheDir(), "http"), 10*1024*1024))
.build();
private static final CacheControl NO_CACHE_WHATSOEVER=new CacheControl.Builder().noCache().noStore().build();
private AccountSession session;
static{
@@ -80,6 +87,9 @@ public class MastodonAPIController{
if(token!=null)
builder.header("Authorization", "Bearer "+token);
if(!req.cacheable)
builder.cacheControl(NO_CACHE_WHATSOEVER);
if(req.headers!=null){
for(Map.Entry<String, String> header:req.headers.entrySet()){
builder.header(header.getKey(), header.getValue());
@@ -113,8 +123,10 @@ public class MastodonAPIController{
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
if(req.canceled)
if(req.canceled){
response.close();
return;
}
if(BuildConfig.DEBUG)
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
synchronized(req){

View File

@@ -46,6 +46,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
boolean canceled;
Map<String, String> headers;
long timeout;
boolean cacheable;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@@ -132,6 +133,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
this.timeout=timeout;
}
protected void setCacheable(){
cacheable=true;
}
protected String getPathPrefix(){
return "/api/v1";
}

View File

@@ -5,31 +5,62 @@ import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.R;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{
public final String error;
public final int httpStatus;
public final Throwable underlyingException;
public final int messageResource;
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
this.error=error;
this.httpStatus=httpStatus;
this.underlyingException=exception;
if(exception instanceof UnknownHostException){
this.messageResource=R.string.could_not_reach_server;
}else if(exception instanceof SocketTimeoutException){
this.messageResource=R.string.connection_timed_out;
}else if(exception instanceof JsonSyntaxException || exception instanceof JsonIOException || httpStatus>=500){
this.messageResource=R.string.server_error;
}else if(httpStatus==404){
this.messageResource=R.string.not_found;
}else{
this.messageResource=0;
}
}
@Override
public void bindErrorView(View view){
TextView text=view.findViewById(R.id.error_text);
text.setText(error);
String message;
if(messageResource>0){
message=view.getContext().getString(messageResource, error);
}else{
message=error;
}
text.setText(message);
}
@Override
public void showToast(Context context){
if(context==null)
return;
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
String message;
if(messageResource>0){
message=context.getString(messageResource, error);
}else{
message=error;
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -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());
}

View File

@@ -13,11 +13,13 @@ import java.util.List;
public class GetCatalogInstances extends MastodonAPIRequest<List<CatalogInstance>>{
private String lang, category;
private boolean includeClosedSignups;
public GetCatalogInstances(String lang, String category){
public GetCatalogInstances(String lang, String category, boolean includeClosedSignups){
super(HttpMethod.GET, null, new TypeToken<>(){});
this.lang=lang;
this.category=category;
this.includeClosedSignups=includeClosedSignups;
}
@Override
@@ -30,6 +32,8 @@ public class GetCatalogInstances extends MastodonAPIRequest<List<CatalogInstance
builder.appendQueryParameter("language", lang);
if(!TextUtils.isEmpty(category))
builder.appendQueryParameter("category", category);
if(includeClosedSignups)
builder.appendQueryParameter("registrations", "all");
return builder.build();
}
}

View File

@@ -0,0 +1,40 @@
package org.joinmastodon.android.api.requests.catalog;
import android.net.Uri;
import android.text.TextUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.donations.DonationCampaign;
public class GetDonationCampaigns extends MastodonAPIRequest<DonationCampaign>{
private final String locale, seed, source;
private boolean staging;
public GetDonationCampaigns(String locale, String seed, String source){
super(HttpMethod.GET, null, DonationCampaign.class);
this.locale=locale;
this.seed=seed;
this.source=source;
setCacheable();
}
public void setStaging(boolean staging){
this.staging=staging;
}
@Override
public Uri getURL(){
Uri.Builder builder=new Uri.Builder()
.scheme("https")
.authority("api.joinmastodon.org")
.path("/v1/donations/campaigns/active")
.appendQueryParameter("platform", "android")
.appendQueryParameter("locale", locale)
.appendQueryParameter("seed", seed);
if(staging)
builder.appendQueryParameter("environment", "staging");
if(!TextUtils.isEmpty(source))
builder.appendQueryParameter("source", source);
return builder.build();
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.NotificationRequest;
public class GetNotificationRequests extends HeaderPaginationRequest<NotificationRequest>{
public GetNotificationRequests(String maxID){
super(HttpMethod.GET, "/notifications/requests", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.annotations.SerializedName;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
@@ -12,6 +13,10 @@ import java.util.List;
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
this(maxID, limit, includeTypes, null);
}
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@@ -25,6 +30,8 @@ public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
addQueryParameter("exclude_types[]", type);
}
}
if(!TextUtils.isEmpty(onlyAccountID))
addQueryParameter("account_id", onlyAccountID);
removeUnsupportedItems=true;
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationsPolicy;
public class GetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
public GetNotificationsPolicy(){
super(HttpMethod.GET, "/notifications/policy", NotificationsPolicy.class);
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class RespondToNotificationRequest extends ResultlessMastodonAPIRequest{
public RespondToNotificationRequest(String id, boolean allow){
super(HttpMethod.POST, "/notifications/requests/"+id+(allow ? "/accept" : "/dismiss"));
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationsPolicy;
public class SetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
public SetNotificationsPolicy(NotificationsPolicy policy){
super(HttpMethod.PUT, "/notifications/policy", NotificationsPolicy.class);
setRequestBody(policy);
}
}

View File

@@ -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 SetStatusConversationMuted extends MastodonAPIRequest<Status>{
public SetStatusConversationMuted(String id, boolean muted){
super(HttpMethod.POST, "/statuses/"+id+(muted ? "/mute" : "/unmute"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -33,7 +33,8 @@ import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator;
import java.io.File;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -44,6 +45,7 @@ import me.grishka.appkit.api.ErrorResponse;
public class AccountSession{
private static final String TAG="AccountSession";
private static final int MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS=28;
public Token token;
public Account self;
@@ -276,4 +278,12 @@ public class AccountSession{
public void setNotificationsMentionsOnly(boolean mentionsOnly){
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
}
public boolean isEligibleForDonations(){
return ("mastodon.social".equalsIgnoreCase(domain) || "mastodon.online".equalsIgnoreCase(domain)) && self.createdAt.isBefore(Instant.now().minus(MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS, ChronoUnit.DAYS));
}
public int getDonationSeed(){
return Math.abs(getFullUsername().hashCode())%100;
}
}

View File

@@ -3,11 +3,16 @@ package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
@@ -18,11 +23,13 @@ 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.CacheController;
import org.joinmastodon.android.api.DatabaseRunnable;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent;
@@ -30,9 +37,10 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
@@ -60,6 +68,7 @@ public class AccountSessionManager{
private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI="mastodon-android-auth://callback";
private static final int DB_VERSION=1;
private static final AccountSessionManager instance=new AccountSessionManager();
@@ -73,6 +82,8 @@ public class AccountSessionManager{
private String lastActiveAccountID;
private SharedPreferences prefs;
private boolean loadedInstances;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
public static AccountSessionManager getInstance(){
return instance;
@@ -94,7 +105,7 @@ public class AccountSessionManager{
Log.e(TAG, "Error loading accounts", x);
}
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
readInstanceInfo(domains);
maybeUpdateShortcuts();
}
@@ -270,11 +281,11 @@ public class AccountSessionManager{
}
}
if(loadedInstances){
maybeUpdateCustomEmojis(domains);
maybeUpdateInstanceInfo(domains);
}
}
private void maybeUpdateCustomEmojis(Set<String> domains){
private void maybeUpdateInstanceInfo(Set<String> domains){
long now=System.currentTimeMillis();
for(String domain:domains){
Long lastUpdated=instancesLastUpdated.get(domain);
@@ -388,7 +399,7 @@ public class AccountSessionManager{
}
if(!loadedInstances){
loadedInstances=true;
maybeUpdateCustomEmojis(domains);
MastodonAPIController.runInBackground(()->maybeUpdateInstanceInfo(domains));
}
}
@@ -425,7 +436,7 @@ public class AccountSessionManager{
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")
ShortcutInfo compose=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))
@@ -433,15 +444,85 @@ public class AccountSessionManager{
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
ShortcutInfo explore=new ShortcutInfo.Builder(MastodonApp.context, "explore")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.tab_search))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_explore))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("explore", true))
.build();
sm.setDynamicShortcuts(List.of(compose, explore));
}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));
sm.disableShortcuts(List.of("compose", "explore"), MastodonApp.context.getString(R.string.err_not_logged_in));
}else{
sm.enableShortcuts(Collections.singletonList("compose"));
sm.enableShortcuts(List.of("compose", "explore"));
}
}
private void closeDelayed(){
CacheController.databaseThread.postRunnable(databaseCloseRunnable, 10_000);
}
public void closeDatabase(){
if(db!=null){
if(BuildConfig.DEBUG)
Log.d(TAG, "closeDatabase");
db.close();
db=null;
}
}
private void cancelDelayedClose(){
if(db!=null){
CacheController.databaseThread.handler.removeCallbacks(databaseCloseRunnable);
}
}
private SQLiteDatabase getOrOpenDatabase(){
if(db==null)
db=new DatabaseHelper();
return db.getWritableDatabase();
}
private void runOnDbThread(DatabaseRunnable r){
cancelDelayedClose();
CacheController.databaseThread.postRunnable(()->{
try{
SQLiteDatabase db=getOrOpenDatabase();
r.run(db);
}catch(SQLiteException|IOException x){
Log.w(TAG, x);
}finally{
closeDelayed();
}
}, 0);
}
public void runIfDonationCampaignNotDismissed(String id, Runnable action){
runOnDbThread(db->{
try(Cursor cursor=db.query("dismissed_donation_campaigns", null, "id=?", new String[]{id}, null, null, null)){
if(!cursor.moveToFirst()){
UiUtils.runOnUiThread(action);
}
}
});
}
public void markDonationCampaignAsDismissed(String id){
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("id", id);
values.put("dismissed_at", System.currentTimeMillis());
db.insert("dismissed_donation_campaigns", null, values);
});
}
public void clearDismissedDonationCampaigns(){
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
}
@@ -451,4 +532,24 @@ public class AccountSessionManager{
public List<Emoji> emojis;
public long lastUpdated;
}
private static class DatabaseHelper extends SQLiteOpenHelper{
public DatabaseHelper(){
super(MastodonApp.context, "accounts.db", null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL("""
CREATE TABLE `dismissed_donation_campaigns` (
`id` text PRIMARY KEY,
`dismissed_at` bigint
)""");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
}
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.events;
public class DismissDonationCampaignBannerEvent{
public final String campaignID;
public DismissDonationCampaignBannerEvent(String campaignID){
this.campaignID=campaignID;
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.events;
public class NotificationRequestRespondedEvent{
public final String accountID, requestID;
public NotificationRequestRespondedEvent(String accountID, String requestID){
this.accountID=accountID;
this.requestID=requestID;
}
}

View File

@@ -0,0 +1,181 @@
package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class AccountNotificationsListFragment extends BaseNotificationsListFragment{
private Account account;
private String requestID;
private TextView expandedTitle;
private boolean choiceMade, allowed;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
requestID=getArguments().getString("requestID");
setTitleMarqueeEnabled(false);
loadData();
setTitle(getString(R.string.notifications_from_user, account.displayName));
setHasOptionsMenu(true);
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Notification> result){
onDataLoaded(result, !result.isEmpty());
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=list.getAdapter().getItemCount());
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
expandedTitle=(TextView) LayoutInflater.from(getActivity()).inflate(R.layout.expanded_title_medium, list, false);
expandedTitle.setText(getTitle());
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(expandedTitle));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(recyclerView.getChildCount()==0)
return;
float fraction;
View topChild=recyclerView.getChildAt(0);
if(recyclerView.getChildAdapterPosition(topChild)>0){
fraction=1;
}else{
fraction=(-topChild.getTop())/(float)(topChild.getHeight()-topChild.getPaddingBottom());
}
expandedTitle.setAlpha(1f-fraction);
toolbarTitleView.setAlpha(fraction);
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notification_request, menu);
MenuItem mute=menu.findItem(R.id.mute);
MenuItem allow=menu.findItem(R.id.allow);
if(choiceMade && allowed){
allow.setIcon(R.drawable.ic_check_wght700_24px);
tintMenuIcon(allow, R.attr.colorM3Primary);
}else{
tintMenuIcon(allow, R.attr.colorM3OnSurfaceVariant);
}
if(choiceMade && !allowed){
mute.setIcon(R.drawable.ic_delete_wght700_24px);
tintMenuIcon(mute, R.attr.colorM3Primary);
}else{
tintMenuIcon(mute, R.attr.colorM3OnSurfaceVariant);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(choiceMade)
return true;
allowed=item.getItemId()==R.id.allow;
new RespondToNotificationRequest(requestID, allowed)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
choiceMade=true;
invalidateOptionsMenu();
E.post(new NotificationRequestRespondedEvent(accountID, requestID));
new Snackbar.Builder(getActivity())
.setText(getString(allowed ? R.string.notifications_allowed : R.string.notifications_muted, account.displayName))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
return true;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
}
return super.buildDisplayItems(n);
}
@Override
protected boolean wantsToolbarMenuIconsTinted(){
return false;
}
private void tintMenuIcon(MenuItem item, int color){
int tintColor=UiUtils.getThemeColor(getActivity(), color);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
Drawable icon=item.getIcon();
if(icon!=null && icon.getColorFilter()==null){
icon=icon.mutate();
icon.setTintList(ColorStateList.valueOf(tintColor));
item.setIcon(icon);
}
}else{
item.setIconTintList(ColorStateList.valueOf(tintColor));
}
}
}

View File

@@ -140,11 +140,6 @@ public class AccountTimelineFragment extends StatusListFragment{
return mergeAdapter;
}
@Override
protected int getMainAdapterOffset(){
return super.getMainAdapterOffset()+1;
}
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
return switch(filter){
case DEFAULT -> defaultFilter;

View File

@@ -0,0 +1,120 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
protected String maxID;
protected View endMark;
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
private Notification getNotificationByID(String id){
for(Notification n : data){
if(n.id.equals(id))
return n;
}
return null;
}
protected void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0; i<displayItems.size(); i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
}

View File

@@ -59,6 +59,9 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -79,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
private SpringAnimation listShakeAnimation;
public BaseStatusListFragment(){
super(20);
@@ -283,8 +287,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
}
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
if(holder instanceof StatusDisplayItem.Holder<?> sih){
if(sih.getItem() instanceof StatusDisplayItem sdi && sdi.getType()==StatusDisplayItem.Type.GAP){
outRect.setEmpty();
return;
}
@@ -292,8 +296,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(holder instanceof StatusDisplayItem.Holder<?> sih2){
String otherID=sih2.getItemID();
if(otherID.equals(id)){
list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left);
@@ -325,7 +329,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
toolbar.setNavigationContentDescription(R.string.back);
}
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}
@@ -662,7 +666,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
proceed.run();
}, status.account, accountID).show();
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS)) && !status.account.id.equals(AccountSessionManager.get(accountID).self.id)){
new OldPostPreReplySheet(getActivity(), notAgain->{
if(notAgain)
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
@@ -675,6 +679,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
public void shakeListView(){
if(listShakeAnimation!=null)
listShakeAnimation.cancel();
SpringAnimation anim=new SpringAnimation(list, DynamicAnimation.TRANSLATION_X, 0);
anim.setStartVelocity(V.dp(-500));
anim.getSpring().setStiffness(500).setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
listShakeAnimation=anim;
anim.addEndListener((animation, canceled, value, velocity)->listShakeAnimation=null);
anim.start();
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){
@@ -745,7 +760,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
// Do not draw dividers between hashtag and/or account rows
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
return false;
return !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
return !ih.getItemID().equals(sh.getItemID()) && ih.getItem() instanceof StatusDisplayItem sdi && sdi.getType()!=StatusDisplayItem.Type.GAP;
}
return false;
}

View File

@@ -23,6 +23,10 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -32,15 +36,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -66,7 +69,9 @@ import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.ExtendedPopupMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PopupKeyboard;
@@ -87,21 +92,22 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{
public class ComposeFragment extends MastodonToolbarFragment implements ComposeEditText.SelectionListener, CustomTransitionsFragment{
private static final int MEDIA_RESULT=717;
public static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -131,7 +137,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, languageBtn;
private TextView replyText;
private Button visibilityBtn;
private LinearLayout visibilityBtn;
private TextView visibilityText1, visibilityText2, visibilityCurrentText;
private LinearLayout bottomBar;
private View autocompleteDivider;
@@ -165,6 +172,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private BackgroundColorSpan overLimitBG;
private ForegroundColorSpan overLimitFG;
private Runnable emojiKeyboardHider;
private Runnable sendingBackButtonBlocker=()->{};
private Runnable discardConfirmationCallback=this::confirmDiscardDraftAndFinish;
private boolean prevHadDraft;
public ComposeFragment(){
super(R.layout.toolbar_fragment_with_progressbar);
}
@@ -241,6 +253,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
});
emojiKeyboardHider=emojiKeyboard::hide;
View view=inflater.inflate(R.layout.fragment_compose, container, false);
mainLayout=view.findViewById(R.id.compose_main_ll);
@@ -271,6 +284,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
visibilityText1=view.findViewById(R.id.visibility_text1);
visibilityText2=view.findViewById(R.id.visibility_text2);
visibilityCurrentText=visibilityText1;
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text);
@@ -280,14 +296,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setOnClickListener(v->toggleSpoiler());
languageBtn.setOnClickListener(v->showLanguageAlert());
visibilityBtn.setOnClickListener(this::onVisibilityClick);
visibilityBtn.setAccessibilityDelegate(new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName("android.widget.Spinner");
}
});
Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate();
arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
visibilityBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrow, null);
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
public void onIconChanged(int icon){
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
updateNavigationBarColor(icon!=PopupKeyboard.ICON_HIDDEN);
if(icon!=PopupKeyboard.ICON_HIDDEN)
addBackCallback(emojiKeyboardHider);
else
removeBackCallback(emojiKeyboardHider);
if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
if(icon==PopupKeyboard.ICON_HIDDEN)
@@ -323,7 +349,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
}
updateVisibilityIcon();
updateVisibilityIcon(false);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
@@ -378,6 +404,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
super.onResume();
}
@Override
protected void onHidden(){
super.onHidden();
if(prevHadDraft){
prevHadDraft=false;
removeBackCallback(discardConfirmationCallback);
}
}
@Override
protected void onShown(){
super.onShown();
updateDraftState();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
@@ -463,6 +504,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
updateCharCounter();
updateDraftState();
}
});
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
@@ -502,7 +544,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
ignoreSelectionChanges=true;
mainEditText.setSelection(mainEditText.length());
ignoreSelectionChanges=false;
mediaViewController.onViewCreated(savedInstanceState);;
mediaViewController.onViewCreated(savedInstanceState);
}else{
String prefilledText=getArguments().getString("prefilledText");
if(!TextUtils.isEmpty(prefilledText)){
@@ -604,6 +646,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(publishButton==null)
return;
publishButton.setEnabled((trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
updateDraftState();
}
private void onCustomEmojiClick(Emoji emoji){
@@ -679,6 +722,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
overlayParams.token=mainEditText.getWindowToken();
wm.addView(sendingOverlay, overlayParams);
addBackCallback(sendingBackButtonBlocker);
publishButton.setEnabled(false);
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
@@ -703,8 +747,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(!pollViewController.isEmpty()){
req.poll=pollViewController.getPollForRequest();
}
if(hasSpoiler && spoilerEdit.length()>0){
req.spoilerText=spoilerEdit.getText().toString();
if(hasSpoiler){
if(spoilerEdit.length()>0)
req.spoilerText=spoilerEdit.getText().toString();
else
req.sensitive=true;
}
if(postLang!=null){
req.language=postLang.locale.toLanguageTag();
@@ -717,6 +764,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onSuccess(Status result){
wm.removeView(sendingOverlay);
sendingOverlay=null;
removeBackCallback(sendingBackButtonBlocker);
removeBackCallback(discardConfirmationCallback);
removeBackCallback(emojiKeyboardHider);
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
@@ -749,6 +799,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void handlePublishError(ErrorResponse error){
wm.removeView(sendingOverlay);
sendingOverlay=null;
removeBackCallback(sendingBackButtonBlocker);
V.setVisibilityAnimated(sendProgress, View.GONE);
publishButton.setEnabled(true);
if(error instanceof MastodonErrorResponse me){
@@ -776,19 +827,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !mediaViewController.isEmpty() || pollFieldsHaveContent;
}
@Override
public boolean onBackPressed(){
if(emojiKeyboard.isVisible()){
emojiKeyboard.hide();
return true;
private void updateDraftState(){
boolean hasDraft=hasDraft();
if(hasDraft!=prevHadDraft){
prevHadDraft=hasDraft;
if(hasDraft){
addBackCallback(discardConfirmationCallback);
}else{
removeBackCallback(discardConfirmationCallback);
}
}
if(hasDraft()){
confirmDiscardDraftAndFinish();
return true;
}
if(sendingOverlay!=null)
return true;
return false;
}
@Override
@@ -822,7 +870,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
.setPositiveButton(R.string.discard, (dialog, which)->{
removeBackCallback(discardConfirmationCallback);
Nav.finish(this);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -909,22 +960,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void onVisibilityClick(View v){
PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility);
menu.setOnMenuItemClickListener(item->{
int id=item.getItemId();
if(id==R.id.vis_public){
statusVisibility=StatusPrivacy.PUBLIC;
}else if(id==R.id.vis_followers){
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT;
ArrayList<ListItem<StatusPrivacy>> items=new ArrayList<>();
ExtendedPopupMenu menu=new ExtendedPopupMenu(getActivity(), items);
Consumer<ListItem<StatusPrivacy>> onClick=i->{
if(statusVisibility!=i.parentObject){
statusVisibility=i.parentObject;
updateVisibilityIcon(true);
}
item.setChecked(true);
updateVisibilityIcon();
return true;
});
menu.show();
menu.dismiss();
};
items.add(new ListItem<>(R.string.visibility_public, R.string.visibility_subtitle_public, R.drawable.ic_public_24px, StatusPrivacy.PUBLIC, onClick));
items.add(new ListItem<>(R.string.visibility_unlisted, R.string.visibility_subtitle_unlisted, R.drawable.ic_clear_night_24px, StatusPrivacy.UNLISTED, onClick));
items.add(new ListItem<>(R.string.visibility_followers_only, R.string.visibility_subtitle_followers, R.drawable.ic_lock_24px, StatusPrivacy.PRIVATE, onClick));
items.add(new ListItem<>(R.string.visibility_private, R.string.visibility_subtitle_private, R.drawable.ic_alternate_email_24px, StatusPrivacy.DIRECT, onClick));
menu.showAsDropDown(v);
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState){
@@ -950,12 +999,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void applyPreferencesForPostVisibility(Preferences prefs, Bundle savedInstanceState){
// Only override the reply visibility if our preference is more private
if(prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)){
// Map unlisted from the API onto public, because we don't have unlisted in the UI
statusVisibility=switch(prefs.postingDefaultVisibility){
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
statusVisibility=prefs.postingDefaultVisibility;
}
// A saved privacy setting from a previous compose session wins over all
@@ -963,28 +1007,45 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
updateVisibilityIcon();
updateVisibilityIcon(false);
}
private void updateVisibilityIcon(){
private void updateVisibilityIcon(boolean animated){
if(getActivity()==null)
return;
if(statusVisibility==null){ // TODO find out why this happens
statusVisibility=StatusPrivacy.PUBLIC;
}
visibilityBtn.setText(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.string.visibility_public;
TextView visibilityText;
if(!animated){
visibilityText=visibilityCurrentText;
}else{
TransitionManager.beginDelayedTransition(visibilityBtn, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds().excludeTarget(TextView.class, true))
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
visibilityText=visibilityCurrentText==visibilityText1 ? visibilityText2 : visibilityText1;
visibilityText.setVisibility(View.VISIBLE);
visibilityCurrentText.setVisibility(View.GONE);
visibilityCurrentText=visibilityText;
}
visibilityText.setText(switch(statusVisibility){
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
});
Drawable icon=getResources().getDrawable(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.drawable.ic_public_20px;
case PUBLIC -> R.drawable.ic_public_20px;
case UNLISTED -> R.drawable.ic_clear_night_20px;
case PRIVATE -> R.drawable.ic_group_20px;
case DIRECT -> R.drawable.ic_alternate_email_20px;
}, getActivity().getTheme()).mutate();
icon.setBounds(0, 0, V.dp(18), V.dp(18));
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
visibilityBtn.setCompoundDrawablesRelative(icon, null, visibilityBtn.getCompoundDrawablesRelative()[2], null);
visibilityText.setCompoundDrawablesRelative(icon, null, null, null);
}
@Override

View File

@@ -32,12 +32,11 @@ import java.util.Collections;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
private static final String TAG="ComposeImageDescription";
private String accountID, attachmentID;
@@ -138,9 +137,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
deliverResult();
return false;
}
@Override

View File

@@ -42,13 +42,11 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
public class CreateListAddMembersFragment extends BaseAccountListFragment implements AddNewListMembersFragment.Listener{
private FollowList followList;
private Button nextButton;
private View buttonBar;
@@ -59,6 +57,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
private WindowInsets lastInsets;
private boolean dismissingSearchFragment;
private HashSet<String> accountIDsInList=new HashSet<>();
private Runnable searchFragmentDismisser=this::dismissSearchFragment;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -156,6 +155,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
addBackCallback(searchFragmentDismisser);
return true;
}
@@ -183,6 +183,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
removeBackCallback(searchFragmentDismisser);
dismissingSearchFragment=true;
rootView.setVisibility(View.VISIBLE);
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
@@ -201,15 +202,6 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
@Override
public boolean isAccountInList(AccountViewModel account){
return accountIDsInList.contains(account.account.id);

View File

@@ -0,0 +1,96 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebResourceRequest;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import java.util.Objects;
import me.grishka.appkit.Nav;
public class DonationWebViewFragment extends WebViewFragment{
public static final String SUCCESS_URL="https://sponsor.joinmastodon.org/donate/success";
public static final String FAILURE_URL="https://sponsor.joinmastodon.org/donate/failure";
public static final String CANCEL_URL="https://sponsor.joinmastodon.org/donate/cancel";
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(BuildConfig.DEBUG){
setHasOptionsMenu(true);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
webView.loadUrl(Objects.requireNonNull(getArguments().getString("url")));
}
@Override
protected boolean shouldOverrideUrlLoading(WebResourceRequest req){
String url=req.getUrl().buildUpon().clearQuery().fragment(null).build().toString();
if(url.equalsIgnoreCase(SUCCESS_URL)){
onSuccess();
return true;
}else if(url.equalsIgnoreCase(FAILURE_URL)){
onFailure();
return true;
}else if(url.equalsIgnoreCase(CANCEL_URL)){
onCancel();
return true;
}
return false;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
super.onCreateOptionsMenu(menu, inflater);
if(BuildConfig.DEBUG){
menu.add(0, 0, 0, "Simulate success");
menu.add(0, 1, 0, "Simulate failure");
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==0)
onSuccess();
else if(item.getItemId()==1)
onFailure();
return super.onOptionsItemSelected(item);
}
private void onFailure(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.donation_server_error)
.setPositiveButton(R.string.ok, null)
.setOnDismissListener(dlg->Nav.finish(this))
.show();
}
private void onSuccess(){
String campaignID=getArguments().getString("campaignID");
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(campaignID);
E.post(new DismissDonationCampaignBannerEvent(campaignID));
getActivity().setResult(Activity.RESULT_OK, new Intent().putExtra("postText", getArguments().getString("successPostText")));
getActivity().finish();
}
private void onCancel(){
getActivity().finish();
}
}

View File

@@ -34,7 +34,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
@@ -155,7 +154,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
}
@Override
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
return 1;
}
@@ -276,4 +275,8 @@ public class HashtagTimelineFragment extends StatusListFragment{
})
.exec(accountID);
}
public String getHashtagName(){
return hashtagName;
}
}

View File

@@ -30,8 +30,8 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ObjectIdComparator;
@@ -48,13 +48,12 @@ import me.grishka.appkit.api.Callback;
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.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
public class HomeFragment extends AppKitFragment{
private FragmentRootLinearLayout content;
private HomeTimelineFragment homeTimelineFragment;
private NotificationsListFragment notificationsFragment;
@@ -272,15 +271,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
return false;
}
@Override
public boolean onBackPressed(){
if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed();
if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed();
return false;
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);

View File

@@ -5,15 +5,23 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AnimationUtils;
import android.widget.Button;
@@ -26,14 +34,17 @@ import android.widget.Toolbar;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
@@ -41,8 +52,11 @@ import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.donations.DonationCampaign;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
@@ -53,6 +67,7 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import androidx.annotation.NonNull;
@@ -64,8 +79,11 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{
private static final int DONATION_RESULT=211;
private ImageButton fab;
private LinearLayout listsDropdown;
private FixedAspectRatioImageView listsDropdownArrow;
@@ -81,9 +99,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private FollowList currentList;
private MergeRecyclerAdapter mergeAdapter;
private DiscoverInfoBannerHelper localTimelineBannerHelper;
private View donationBanner;
private boolean donationBannerDismissing;
private String maxID;
private String lastSavedMarkerID;
private DonationCampaign currentDonationCampaign;
private BottomSheet donationSheet;
public HomeTimelineFragment(){
setListLayoutId(R.layout.fragment_timeline);
@@ -93,6 +115,32 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), null);
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
req.setStaging(true);
}
req.setCallback(new Callback<>(){
@Override
public void onSuccess(DonationCampaign result){
if(result==null)
return;
AccountSessionManager.getInstance().runIfDonationCampaignNotDismissed(result.id, ()->showDonationBanner(result));
}
@Override
public void onError(ErrorResponse error){}
})
.execNoAuth("");
}
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
@@ -230,9 +278,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
});
if(GithubSelfUpdater.needSelfUpdating()){
E.register(this);
updateUpdateState(GithubSelfUpdater.getInstance().getState());
}
if(currentDonationCampaign!=null)
showDonationBanner(currentDonationCampaign);
}
@Override
@@ -584,9 +633,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
@Override
public void onDestroyView(){
super.onDestroyView();
if(GithubSelfUpdater.needSelfUpdating()){
E.unregister(this);
}
donationBanner=null;
donationBannerDismissing=false;
}
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
@@ -599,6 +647,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
updateUpdateState(ev.state);
}
@Subscribe
public void onDismissDonationCampaignBanner(DismissDonationCampaignBannerEvent ev){
if(currentDonationCampaign!=null && ev.campaignID.equals(currentDonationCampaign.id)){
dismissDonationBanner();
}
}
@Override
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true;
@@ -653,6 +708,17 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
super.onDataLoaded(d, more);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==DONATION_RESULT){
if(donationSheet!=null)
donationSheet.dismissWithoutAnimation();
if(resultCode==Activity.RESULT_OK){
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
}
}
}
private String getCurrentListTitle(){
return switch(listMode){
case FOLLOWING -> getString(R.string.timeline_following);
@@ -661,6 +727,77 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
};
}
private void showDonationBanner(DonationCampaign campaign){
if(getActivity()==null)
return;
currentDonationCampaign=campaign;
if(donationBanner==null){
ViewStub stub=contentView.findViewById(R.id.donation_banner);
donationBanner=stub.inflate();
donationBanner.findViewById(R.id.banner_dismiss).setOnClickListener(v->{
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(currentDonationCampaign.id);
dismissDonationBanner();
});
donationBanner.setOnClickListener(v->openDonationSheet());
}else{
donationBanner.setVisibility(View.VISIBLE);
}
TextView text=donationBanner.findViewById(R.id.banner_text);
SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage);
ssb.append(' ');
int start=ssb.length();
ssb.append(campaign.bannerButtonText);
ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0);
ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0);
text.setText(ssb);
donationBanner.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
donationBanner.getViewTreeObserver().removeOnPreDrawListener(this);
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight(), 0),
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, -donationBanner.getHeight())
);
set.setDuration(250);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.start();
return true;
}
});
}
private void dismissDonationBanner(){
if(donationBanner==null || donationBannerDismissing)
return;
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight()),
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, 0)
);
set.setDuration(250);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
donationBanner.setVisibility(View.GONE);
donationBannerDismissing=false;
}
});
donationBannerDismissing=true;
set.start();
currentDonationCampaign=null;
}
private void openDonationSheet(){
donationSheet=new DonationSheet(getActivity(), currentDonationCampaign, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
donationSheet.setOnDismissListener(dialog->donationSheet=null);
donationSheet.show();
}
private enum ListMode{
FOLLOWING,
LOCAL,

View File

@@ -44,12 +44,11 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener{
private ImageButton fab;
private FollowList followList;
private boolean inSelectionMode;
@@ -63,6 +62,8 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
private WindowInsets lastInsets;
private HashSet<String> accountIDsInList=new HashSet<>();
private boolean dismissingSearchFragment;
private Runnable searchFragmentDismisser=this::dismissSearchFragment;;
private Runnable actionModeDismisser=()->actionMode.finish();
public ListMembersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -214,6 +215,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
addBackCallback(searchFragmentDismisser);
}
private void onItemClick(AccountViewHolder holder){
@@ -293,9 +295,11 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
selectedAccounts.clear();
updateItemsForSelectionModeTransition();
V.setVisibilityAnimated(fab, View.VISIBLE);
removeBackCallback(actionModeDismisser);
}
});
updateActionModeTitle();
addBackCallback(actionModeDismisser);
}
private void updateActionModeTitle(){
@@ -371,15 +375,6 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
removeAccounts(Set.of(account.account.id), onDone);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
@@ -393,6 +388,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
searchFragment=null;
dismissingSearchFragment=false;
}).start();
removeBackCallback(searchFragmentDismisser);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}

View File

@@ -0,0 +1,250 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotificationRequests;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.NotificationRequest;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.Snackbar;
import org.parceler.Parcels;
import java.util.HashMap;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class NotificationRequestsFragment extends MastodonRecyclerFragment<NotificationRequest>{
private String accountID;
private String maxID;
private HashMap<String, AccountViewModel> accountViewModels=new HashMap<>();
private View endMark;
private NotificationRequestsAdapter adapter;
public NotificationRequestsFragment(){
super(50);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.filtered_notifications);
loadData();
E.register(this);
}
@Override
public void onDestroy(){
E.unregister(this);
super.onDestroy();
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotificationRequests(offset==0 ? null : maxID)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<NotificationRequest> result){
if(data.isEmpty() || refreshing)
accountViewModels.clear();
maxID=result.getNextPageMaxID();
for(NotificationRequest req:result){
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
}
onDataLoaded(result, !TextUtils.isEmpty(maxID));
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
return adapter=new NotificationRequestsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof NotificationRequestViewHolder).setDrawBelowLastItem(true));
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Subscribe
public void onNotificationRequestResponded(NotificationRequestRespondedEvent ev){
if(adapter==null || !ev.accountID.equals(accountID))
return;
for(int i=0;i<data.size();i++){
if(data.get(i).id.equals(ev.requestID)){
data.remove(i);
adapter.notifyItemRemoved(i);
return;
}
}
for(NotificationRequest nr:preloadedData){
if(nr.id.equals(ev.requestID)){
preloadedData.remove(nr);
break;
}
}
}
private class NotificationRequestsAdapter extends UsableRecyclerView.Adapter<NotificationRequestViewHolder> implements ImageLoaderRecyclerAdapter{
public NotificationRequestsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public NotificationRequestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new NotificationRequestViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public void onBindViewHolder(NotificationRequestViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return Objects.requireNonNull(accountViewModels.get(data.get(position).account.id)).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(data.get(position).account.id));
return switch(image){
case 0 -> model.avaRequest;
default -> model.emojiHelper.getImageRequest(image-1);
};
}
}
private class NotificationRequestViewHolder extends BindableViewHolder<NotificationRequest> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, badge;
private final ImageView ava;
private final ImageButton allow, mute;
public NotificationRequestViewHolder(){
super(getActivity(), R.layout.item_notification_request, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
badge=findViewById(R.id.badge);
ava=findViewById(R.id.ava);
allow=findViewById(R.id.btn_allow);
mute=findViewById(R.id.btn_mute);
ava.setOutlineProvider(OutlineProviders.roundedRect(8));
ava.setClipToOutline(true);
allow.setOnClickListener(this::onAllowClick);
mute.setOnClickListener(this::onMuteClick);
}
@SuppressLint("DefaultLocale")
@Override
public void onBind(NotificationRequest item){
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
name.setText(model.parsedName);
username.setText(item.account.getDisplayUsername());
badge.setText(item.notificationsCount>99 ? String.format("%d+", 99) : String.format("%d", item.notificationsCount));
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
if(image==null)
ava.setImageResource(R.drawable.image_placeholder);
else
ava.setImageDrawable(image);
}else{
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
model.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(item.account));
args.putString("requestID", item.id);
Nav.go(getActivity(), AccountNotificationsListFragment.class, args);
}
private void onAllowClick(View v){
acceptOrDecline(true);
}
private void onMuteClick(View v){
acceptOrDecline(false);
}
private void acceptOrDecline(boolean accept){
new RespondToNotificationRequest(item.id, accept)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
int pos=data.indexOf(item);
data.remove(pos);
adapter.notifyItemRemoved(pos);
new Snackbar.Builder(getActivity())
.setText(getString(accept ? R.string.notifications_allowed : R.string.notifications_muted, item.account.displayName))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
}
}

View File

@@ -1,57 +1,70 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.GetNotificationsPolicy;
import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolicy;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationsPolicy;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.GenericListItemsViewController;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
public class NotificationsListFragment extends BaseNotificationsListFragment{
private boolean onlyMentions;
private String maxID;
private View tabBar;
private View mentionsTab, allTab;
private View endMark;
private String unreadMarker, realUnreadMarker;
private MenuItem markAllReadItem;
private boolean reloadingFromCache;
private ListItem<Void> requestsItem=new ListItem<>(R.string.filtered_notifications, 0, R.drawable.ic_inventory_2_24px, i->openNotificationRequests());
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
private NotificationsPolicy lastPolicy;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -74,43 +87,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
setTitle(R.string.notifications);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && !reloadingFromCache)
endMark.setVisibility(View.GONE);
if(offset==0)
reloadPolicy();
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@@ -142,30 +124,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
resetUnreadBackground();
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
tabBar=view.findViewById(R.id.tabbar);
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
View tabBarItself=view.findViewById(R.id.tabbar_inner);
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
@@ -215,14 +177,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return views;
}
private Notification getNotificationByID(String id){
for(Notification n:data){
if(n.id.equals(id))
return n;
}
return null;
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
@@ -249,25 +203,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
private void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount()) || holder.getAbsoluteAdapterPosition()<requestsItems.size();
}
private void onTabClick(View v){
@@ -285,34 +223,34 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
markAllReadItem=menu.findItem(R.id.mark_all_read);
MenuItem filters=menu.findItem(R.id.filters);
filters.setVisible(lastPolicy!=null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.mark_all_read){
int id=item.getItemId();
if(id==R.id.mark_all_read){
markAsRead();
resetUnreadBackground();
}else if(id==R.id.filters){
showFiltersAlert();
}
return true;
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(requestsRowAdapter);
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void markAsRead(){
if(data.isEmpty())
return;
@@ -366,4 +304,93 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
return true;
}
private void updatePolicy(NotificationsPolicy policy){
int count=policy.summary==null ? 0 : policy.summary.pendingRequestsCount;
boolean isShown=!requestsItems.isEmpty();
boolean needShow=count>0;
if(isShown && !needShow){
requestsItems.clear();
requestsRowAdapter.notifyItemRemoved(0);
}else if(!isShown && needShow){
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
requestsItems.add(requestsItem);
requestsRowAdapter.notifyItemInserted(0);
}else if(isShown){
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
requestsRowAdapter.notifyItemChanged(0);
}
lastPolicy=policy;
invalidateOptionsMenu();
}
private void reloadPolicy(){
new GetNotificationsPolicy()
.setCallback(new Callback<>(){
@Override
public void onSuccess(NotificationsPolicy policy){
updatePolicy(policy);
}
@Override
public void onError(ErrorResponse errorResponse){
}
})
.exec(accountID);
}
private void showFiltersAlert(){
GenericListItemsViewController<Void> controller=new GenericListItemsViewController<>(getActivity());
Consumer<CheckableListItem<Void>> toggler=item->{
item.toggle();
controller.rebindItem(item);
};
CheckableListItem<Void> followingItem, followersItem, newAccountsItem, mentionsItem;
List<ListItem<Void>> items=List.of(
followingItem=new CheckableListItem<>(R.string.notification_filter_following, R.string.notification_filter_following_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowing, toggler, true),
followersItem=new CheckableListItem<>(R.string.notification_filter_followers, R.string.notification_filter_followers_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowers, toggler, true),
newAccountsItem=new CheckableListItem<>(R.string.notification_filter_new_accounts, R.string.notification_filter_new_accounts_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNewAccounts, toggler, true),
mentionsItem=new CheckableListItem<>(R.string.notification_filter_mentions, R.string.notification_filter_mentions_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterPrivateMentions, toggler, true)
);
controller.setItems(items);
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.filter_notifications)
.setView(controller.getView())
.setPositiveButton(R.string.save, null)
.show();
Button btn=dlg.getButton(Dialog.BUTTON_POSITIVE);
btn.setOnClickListener(v->{
UiUtils.showProgressForAlertButton(btn, true);
NotificationsPolicy newPolicy=new NotificationsPolicy();
newPolicy.filterNotFollowing=followingItem.checked;
newPolicy.filterNotFollowers=followersItem.checked;
newPolicy.filterNewAccounts=newAccountsItem.checked;
newPolicy.filterPrivateMentions=mentionsItem.checked;
new SetNotificationsPolicy(newPolicy)
.setCallback(new Callback<>(){
@Override
public void onSuccess(NotificationsPolicy policy){
updatePolicy(policy);
dlg.dismiss();
}
@Override
public void onError(ErrorResponse errorResponse){
Activity activity=getActivity();
if(activity==null)
return;
UiUtils.showProgressForAlertButton(btn, false);
errorResponse.showToast(activity);
}
})
.exec(accountID);
});
}
private void openNotificationRequests(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), NotificationRequestsFragment.class, args);
}
}

View File

@@ -198,6 +198,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
private void showAllFeaturedHashtags(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("hashtags", tags);
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);

View File

@@ -20,7 +20,6 @@ import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
@@ -29,7 +28,6 @@ import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -67,6 +65,7 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
@@ -101,14 +100,13 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
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.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@@ -136,6 +134,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private ImageButton qrCodeButton;
private ProgressBar innerProgress;
private View actions;
private Account account;
private String accountID;
@@ -157,6 +157,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private Animator tabBarColorAnim;
private MenuItem editSaveMenuItem;
private boolean savingEdits;
private Runnable editModeBackCallback=this::onEditModeBackCallback;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -219,6 +220,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
qrCodeButton=content.findViewById(R.id.qr_code);
innerProgress=content.findViewById(R.id.profile_progress);
actions=content.findViewById(R.id.profile_actions);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@@ -306,6 +309,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
if(account==null)
return true;
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
@@ -331,7 +336,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
usernameDomain.setOnClickListener(v->{
if(account==null)
return;
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
});
qrCodeButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -462,6 +471,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return true;
}
});
if(!loaded)
bindHeaderViewForPreviewMaybe();
}
@Override
@@ -506,7 +517,41 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private void bindHeaderViewForPreviewMaybe(){
if(loaded)
return;
String username=getArguments().getString("accountUsername");
String domain=getArguments().getString("accountDomain");
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(domain))
return;
content.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
innerProgress.setVisibility(View.VISIBLE);
this.username.setText(username);
name.setText(username);
usernameDomain.setText(domain);
avatar.setImageResource(R.drawable.image_placeholder);
cover.setImageResource(R.drawable.image_placeholder);
actions.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
tabsDivider.setVisibility(View.GONE);
}
private void bindHeaderView(){
if(innerProgress.getVisibility()==View.VISIBLE){
TransitionManager.beginDelayedTransition(contentView, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.excludeChildren(actions, true)
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
innerProgress.setVisibility(View.GONE);
countersLayout.setVisibility(View.VISIBLE);
actions.setVisibility(View.VISIBLE);
tabsDivider.setVisibility(View.VISIBLE);
}
setTitle(account.displayName);
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)));
@@ -635,7 +680,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
menu.findItem(R.id.block_domain).setVisible(false);
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){
if(relationship.following){
MenuItem notifications=menu.findItem(R.id.notifications);
notifications.setVisible(true);
notifications.setIcon(relationship.notifying ? R.drawable.ic_notifications_fill1_24px : R.drawable.ic_notifications_24px);
notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername()));
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()){
menu.setGroupDividerEnabled(true);
}
}
@@ -663,7 +715,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
}, this::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){
@@ -693,6 +745,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
}else if(id==R.id.notifications){
new SetAccountFollowed(account.id, true, relationship.showingReblogs, !relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
updateRelationship(result);
new Snackbar.Builder(getActivity())
.setText(result.notifying ? R.string.new_post_notifications_enabled : R.string.new_post_notifications_disabled)
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
@@ -913,12 +983,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
refreshLayout.setEnabled(false);
editDirty=false;
V.setVisibilityAnimated(fab, View.GONE);
addBackCallback(editModeBackCallback);
}
private void exitEditMode(){
if(!isInEditMode)
throw new IllegalStateException();
isInEditMode=false;
removeBackCallback(editModeBackCallback);
invalidateOptionsMenu();
actionButton.setText(R.string.edit_profile);
@@ -1028,23 +1100,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
}
@Override
public boolean onBackPressed(){
if(isInEditMode){
if(savingEdits)
return true;
if(editDirty || aboutFragment.isEditDirty()){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
.setNegativeButton(R.string.cancel, null)
.show();
}else{
exitEditMode();
}
return true;
private void onEditModeBackCallback(){
if(savingEdits)
return;
if(editDirty || aboutFragment.isEditDirty()){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
.setNegativeButton(R.string.cancel, null)
.show();
}else{
exitEditMode();
}
return false;
}
private List<Attachment> createFakeAttachments(String url, Drawable drawable){
@@ -1058,6 +1125,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void onAvatarClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(AVATAR_RESULT);
}else{
@@ -1071,11 +1140,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void onCoverClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(COVER_RESULT);
}else{
Drawable drawable=cover.getDrawable();
if(drawable==null || drawable instanceof ColorDrawable)
if(drawable==null || drawable instanceof ColorDrawable || account.headerStatic.endsWith("/missing.png"))
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));

View File

@@ -53,6 +53,8 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
@@ -144,12 +146,16 @@ public class ProfileQrCodeFragment extends AppKitFragment{
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
dlg.setOnKeyListener((dialog, keyCode, event)->{
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
dismiss();
}
return true;
});
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
dlg.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this::dismiss);
}else{
dlg.setOnKeyListener((dialog, keyCode, event)->{
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
dismiss();
}
return true;
});
}
}
@Override

View File

@@ -0,0 +1,102 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.api.MastodonErrorResponse;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.LoaderFragment;
public abstract class WebViewFragment extends LoaderFragment{
private static final String TAG="WebViewFragment";
protected WebView webView;
private Runnable backCallback=this::onGoBack;
private boolean backCallbackSet;
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
webView=new WebView(getActivity());
webView.setWebChromeClient(new WebChromeClient(){
@Override
public void onReceivedTitle(WebView view, String title){
setTitle(title);
}
});
webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url){
if(BuildConfig.DEBUG){
Log.d(TAG, "onPageFinished: "+url);
}
dataLoaded();
updateBackCallback();
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error){
if(!loaded){
onError(new MastodonErrorResponse(error.getDescription().toString(), -1, null));
updateBackCallback();
}
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
return WebViewFragment.this.shouldOverrideUrlLoading(request);
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload){
updateBackCallback();
}
});
webView.getSettings().setJavaScriptEnabled(true);
return webView;
}
@Override
protected void doLoadData(){
}
@Override
public void onRefresh(){
webView.reload();
}
@Override
public void onToolbarNavigationClick(){
Nav.finish(this);
}
private void updateBackCallback(){
boolean canGoBack=webView.canGoBack();
if(canGoBack!=backCallbackSet){
if(canGoBack){
addBackCallback(backCallback);
backCallbackSet=true;
}else{
removeBackCallback(backCallback);
backCallbackSet=false;
}
}
}
private void onGoBack(){
if(webView.canGoBack())
webView.goBack();
}
protected abstract boolean shouldOverrideUrlLoading(WebResourceRequest req);
}

View File

@@ -36,10 +36,9 @@ import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
private static final int QUERY_RESULT=937;
private static final int SCAN_RESULT=456;
@@ -62,6 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private String accountID;
private String currentQuery;
private Intent scannerIntent;
private Runnable searchExitCallback=this::exitSearch;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -232,6 +232,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
addBackCallback(searchExitCallback);
}
}
@@ -248,6 +249,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
tabsDivider.setVisibility(View.VISIBLE);
currentQuery=null;
removeBackCallback(searchExitCallback);
}
private Fragment getFragmentForPage(int page){
@@ -260,15 +262,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
};
}
@Override
public boolean onBackPressed(){
if(searchActive){
exitSearch();
return true;
}
return false;
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==QUERY_RESULT && success){

View File

@@ -2,56 +2,35 @@ package org.joinmastodon.android.fragments.discover;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.HorizontalScrollingTouchListener;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.LinkCardHolder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
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;
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
public class DiscoverNewsFragment extends BaseRecyclerFragment<DiscoverNewsFragment.CardItem> implements ScrollableToTop{
private String accountID;
private DiscoverInfoBannerHelper bannerHelper;
private MergeRecyclerAdapter mergeAdapter;
private UsableRecyclerView cardsList;
private ArrayList<CardViewModel> top3=new ArrayList<>();
private CardLinksAdapter cardsAdapter;
public DiscoverNewsFragment(){
super(10);
@@ -70,12 +49,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Card> result){
top3.clear();
top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
cardsAdapter.notifyDataSetChanged();
onDataLoaded(result.subList(top3.size(), result.size()).stream()
.map(card->new CardViewModel(card, 56, 56))
int[] index={0};
onDataLoaded(result.stream()
.map(card->{
int actualIndex=index[0]+(refreshing ? 0 : (data.size()+preloadedData.size()));
index[0]++;
int size=actualIndex==0 ? 1000 : 192;
return new CardItem(new CardViewModel(card, size, size, card, accountID));
})
.collect(Collectors.toList()), false);
bannerHelper.onBannerBecameVisible();
}
@@ -86,27 +67,9 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
@SuppressLint("ClickableViewAccessibility")
@Override
protected RecyclerView.Adapter getAdapter(){
cardsList=new UsableRecyclerView(getActivity());
cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
cardsList.setClipToPadding(false);
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.right=V.dp(16);
}
});
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
cardsList.setDrawSelectorOnTop(true);
cardsList.setOnTouchListener(new HorizontalScrollingTouchListener(getActivity()));
mergeAdapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, mergeAdapter);
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
mergeAdapter.addAdapter(new LinksAdapter(imgLoader));
return mergeAdapter;
}
@@ -115,18 +78,46 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
smoothScrollRecyclerViewToTop(list);
}
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
private final List<CardViewModel> data;
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildAdapterPosition(view)==0 && !bannerHelper.isBannerShown()){
outRect.top=V.dp(16);
}
if(parent.getChildViewHolder(view) instanceof LinkCardHolder<?>){
outRect.bottom=V.dp(8);
}
}
});
}
public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
public static class CardItem implements LinkCardHolder.LinkCardProvider{
public final CardViewModel card;
private CardItem(CardViewModel card){
this.card=card;
}
@Override
public CardViewModel getCard(){
return card;
}
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkCardHolder<CardItem>> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(ListImageLoaderWrapper imgLoader){
super(imgLoader);
this.data=data;
}
@NonNull
@Override
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkViewHolder();
public LinkCardHolder<CardItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
LinkCardHolder<CardItem> vh=new LinkCardHolder<>(getActivity(), list, viewType==1, accountID);
vh.setTryResolving(false);
return vh;
}
@Override
@@ -135,91 +126,24 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
}
@Override
public void onBindViewHolder(BaseLinkViewHolder holder, int position){
holder.bind(data.get(position).card);
public void onBindViewHolder(LinkCardHolder<CardItem> holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return data.get(position).imageRequest==null ? 0 : 1;
return data.get(position).card.getImageCount();
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return data.get(position).imageRequest;
}
}
private class CardLinksAdapter extends LinksAdapter{
public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader, data);
}
@NonNull
@Override
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkCardViewHolder();
}
}
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
protected final TextView name, title;
protected final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
public BaseLinkViewHolder(int layout){
super(getActivity(), layout, list);
name=findViewById(R.id.name);
title=findViewById(R.id.title);
photo=findViewById(R.id.photo);
return data.get(position).card.getImageRequest(image);
}
@Override
public void onBind(Card item){
name.setText(item.providerName);
title.setText(item.title);
crossfadeDrawable.setSize(item.width, item.height);
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
didClear=false;
}
@Override
public void setImage(int index, Drawable drawable){
crossfadeDrawable.setImageDrawable(drawable);
if(didClear)
crossfadeDrawable.animateAlpha(0f);
}
@Override
public void clearImage(int index){
crossfadeDrawable.setCrossfadeAlpha(1f);
didClear=true;
}
@Override
public void onClick(){
UiUtils.launchWebBrowser(getActivity(), item.url);
}
}
private class LinkViewHolder extends BaseLinkViewHolder{
public LinkViewHolder(){
super(R.layout.item_trending_link);
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
photo.setClipToOutline(true);
}
}
private class LinkCardViewHolder extends BaseLinkViewHolder{
public LinkCardViewHolder(){
super(R.layout.item_trending_link_card);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
public int getItemViewType(int position){
return position==0 ? 1 : 2;
}
}
}

View File

@@ -57,14 +57,13 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment{
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
@@ -371,6 +370,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
if(!enter){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
}
return set;
}
@@ -437,14 +441,6 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;

View File

@@ -73,8 +73,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected boolean isSignup;
protected CatalogInstance fakeInstance=new CatalogInstance();
private static final double DUNBAR=Math.log(800);
public InstanceCatalogFragment(int layout, int perPage){
super(layout, perPage);
}
@@ -155,7 +153,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain))
if(TextUtils.isEmpty(_domain) || _domain.indexOf('.')==-1)
return;
String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain);
@@ -316,8 +314,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
if(a==null) {
response.close();
return;
}
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->{

View File

@@ -5,8 +5,9 @@ import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
@@ -26,16 +27,12 @@ import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -49,11 +46,9 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -63,17 +58,11 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
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;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{
private MastodonAPIRequest<?> getCategoriesRequest;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
private View topBar;
private List<String> languages=Collections.emptyList();
@@ -94,6 +83,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
private Runnable exitQueryModeCallback=()->setSearchQueryMode(false);
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@@ -113,7 +104,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetCatalogInstances(null, null)
currentRequest=new GetCatalogInstances(null, null, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
@@ -149,58 +140,17 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
}
})
.execNoAuth("");
getCategoriesRequest=new GetCatalogCategories(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogCategory> result){
getCategoriesRequest=null;
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
updateCategories();
}
@Override
public void onError(ErrorResponse error){
getCategoriesRequest=null;
error.showToast(getActivity());
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
updateCategories();
}
})
.execNoAuth("");
}
private void updateCategories(){
// categoriesList.removeAllTabs();
// for(CatalogCategory cat:categories){
// int titleRes=getTitleForCategory(cat.category);
// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
// emoji.setImageResource(getEmojiForCategory(cat.category));
// categoriesList.addTab(tab);
// }
}
@Override
public void onDestroy(){
removeBackCallback(exitQueryModeCallback);
super.onDestroy();
if(getCategoriesRequest!=null)
getCategoriesRequest.cancel();
}
@Override
protected RecyclerView.Adapter getAdapter(){
View headerView=new View(getActivity());
headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
return adapter=new InstancesAdapter();
}
@SuppressLint("ClickableViewAccessibility")
@@ -222,7 +172,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
setStatusBarColor(0);
topBar=view.findViewById(R.id.top_bar);
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar));
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar));
if(buttonBar.getBackground() instanceof LayerDrawable ld){
ld=(LayerDrawable) ld.mutate();
buttonBar.setBackground(ld);
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
overlay.setAlpha(20);
}
}
buttonBar.setElevation(V.dp(3));
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
@@ -572,6 +531,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
filteredData.add(instance);
}
}
setEmptyText(getString(R.string.no_servers_found, currentSearchQuery));
}else{
setEmptyText("");
}
}else{
for(CatalogInstance instance:data){
@@ -591,27 +553,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
}
}
}
DiffUtil.calculateDiff(new DiffUtil.Callback(){
@Override
public int getOldListSize(){
return prevData.size();
}
UiUtils.updateRecyclerViewKeepingAbsoluteScrollPosition(list, ()->{
DiffUtil.calculateDiff(new DiffUtil.Callback(){
@Override
public int getOldListSize(){
return prevData.size();
}
@Override
public int getNewListSize(){
return filteredData.size();
}
@Override
public int getNewListSize(){
return filteredData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
}).dispatchUpdatesTo(adapter);
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
}).dispatchUpdatesTo(adapter);
});
}
@Override
@@ -620,19 +584,13 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
@Override
public boolean onBackPressed(){
if(searchQueryMode){
setSearchQueryMode(false);
return true;
}
return false;
}
private void setSearchQueryMode(boolean enabled){
if(searchQueryMode==enabled)
return;
searchQueryMode=enabled;
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams();
if(searchQueryMode){
addBackCallback(exitQueryModeCallback);
filtersScroll.setVisibility(View.GONE);
lp.removeRule(RelativeLayout.END_OF);
backBtn.setScaleX(0.83333333f);
@@ -640,6 +598,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
backBtn.setTranslationX(V.dp(8));
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0));
}else{
removeBackCallback(exitQueryModeCallback);
filtersScroll.setVisibility(View.VISIBLE);
focusThing.requestFocus();
searchEdit.setText("");
@@ -732,7 +691,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
boolean found=false;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
if(holder instanceof InstanceViewHolder ivh && holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx){
ivh.radioButton.setChecked(false);
found=true;
break;

View File

@@ -1,12 +1,11 @@
package org.joinmastodon.android.fragments.onboarding;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
@@ -65,7 +64,7 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
if(currentSearchQuery.length()>0){
if(!TextUtils.isEmpty(currentSearchQuery)){
boolean foundExactMatch=false;
for(CatalogInstance inst:data){
if(inst.normalizedDomain.contains(currentSearchQuery)){
@@ -74,9 +73,16 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
foundExactMatch=true;
}
}
if(!foundExactMatch)
if(!foundExactMatch && currentSearchQuery.indexOf('.')!=-1)
filteredData.add(0, fakeInstance);
}
if(filteredData.isEmpty()){
for(CatalogInstance inst:data){
if(inst.normalizedDomain.equals("mastodon.social") || inst.normalizedDomain.equals("mastodon.online")){
filteredData.add(inst);
}
}
}
UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals);
for(int i=0;i<list.getChildCount();i++){
list.getChildAt(i).invalidateOutline();
@@ -90,12 +96,15 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
private void loadAutocompleteServers(){
loadedAutocomplete=true;
new GetCatalogInstances(null, null)
new GetCatalogInstances(null, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
if(getActivity()==null)
return;
data.clear();
data.addAll(sortInstances(result));
updateFilteredList();
}
@Override
@@ -112,6 +121,9 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
Toolbar toolbar=getToolbar();
toolbar.setElevation(0);
toolbar.setBackground(null);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
toolbar.setContentInsetStartWithNavigation(V.dp(80));
}
}
@Override

View File

@@ -133,7 +133,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
}
numRunningFollowRequests++;
String id=accountIdsToFollow.remove(0);
new SetAccountFollowed(id, true, true)
new SetAccountFollowed(id, true, true, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@@ -50,6 +50,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
@@ -62,6 +63,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends ToolbarFragment{
private static final String TAG="SignupFragment";
private final Pattern emailRegex=Pattern.compile("^[^@]+@[^@]+\\.[^@]{2,}$");
private Instance instance;
@@ -97,6 +99,7 @@ public class SignupFragment extends ToolbarFragment{
View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false);
TextView domain=view.findViewById(R.id.domain);
TextView atSign=view.findViewById(R.id.at_sign);
displayName=view.findViewById(R.id.display_name);
username=view.findViewById(R.id.username);
email=view.findViewById(R.id.email);
@@ -118,7 +121,7 @@ public class SignupFragment extends ToolbarFragment{
@Override
public boolean onPreDraw(){
username.getViewTreeObserver().removeOnPreDrawListener(this);
username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom());
username.setPadding(atSign.getWidth(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom());
return true;
}
});
@@ -145,6 +148,10 @@ public class SignupFragment extends ToolbarFragment{
reasonExplain.setVisibility(View.GONE);
}
password.setOnFocusChangeListener(this::onPasswordFieldFocusChange);
passwordConfirm.setOnFocusChangeListener(this::onPasswordFieldFocusChange);
email.setOnFocusChangeListener(this::onEmailFieldFocusChange);
return view;
}
@@ -281,34 +288,44 @@ public class SignupFragment extends ToolbarFragment{
.exec(instance.uri, apiToken);
}
private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", onClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
return ssb;
}
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
return switch(fieldName){
case "email" -> switch(error.error){
case "ERR_BLOCKED" -> {
String emailAddr=email.getText().toString();
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
yield ssb;
yield makeLinkInErrorMessage(s, this::onGoBackLinkClick);
}
case "ERR_INVALID" -> getString(R.string.signup_email_invalid);
case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_email_taken), this::onForgotPasswordLinkClick);
default -> error.description;
};
case "username" -> switch(error.error){
case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_username_taken), this::onGoBackLinkClick);
default -> error.description;
};
default -> error.description;
@@ -345,7 +362,9 @@ public class SignupFragment extends ToolbarFragment{
}
private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0));
btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find()
&& password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString())
&& (!instance.approvalRequired || reason.length()>0));
}
private void createAppAndGetToken(){
@@ -406,6 +425,24 @@ public class SignupFragment extends ToolbarFragment{
Nav.finish(this);
}
private void onForgotPasswordLinkClick(LinkSpan span){
UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new");
}
private void onPasswordFieldFocusChange(View v, boolean hasFocus){
if(hasFocus || password.length()==0 || passwordConfirm.length()==0)
return;
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
}
}
private void onEmailFieldFocusChange(View v, boolean hasFocus){
if(!hasFocus && email.length()>0 && !emailRegex.matcher(email.getText()).find()){
emailWrap.setErrorState(getString(R.string.signup_email_invalid));
}
}
private class ErrorClearingListener implements TextWatcher{
public final EditText editText;

View File

@@ -134,7 +134,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
@Override
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
return 1;
}

View File

@@ -34,7 +34,6 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ReportDoneFragment extends MastodonToolbarFragment{
@@ -177,7 +176,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){

View File

@@ -24,6 +24,7 @@ import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
@@ -44,11 +45,10 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
public class EditFilterFragment extends BaseSettingsFragment<Void>{
private static final int WORDS_RESULT=370;
private static final int CONTEXT_RESULT=651;
@@ -63,6 +63,13 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
private ArrayList<String> deletedWordIDs=new ArrayList<>();
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
private boolean dirty;
private boolean wasDirty;
private Runnable confirmCallback=()->{
if(isDirty()){
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
}
};
@Override
public void onCreate(Bundle savedInstanceState){
@@ -90,6 +97,12 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
setRetainInstance(true);
}
@Override
public void onDestroy(){
removeBackCallback(confirmCallback);
super.onDestroy();
}
@Override
protected void doLoadData(int offset, int count){}
@@ -101,6 +114,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
titleEditLayout.updateHint();
if(filter!=null)
titleEdit.setText(filter.title);
titleEdit.addTextChangedListener(new SimpleTextWatcher(e->updateBackCallback()));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
@@ -158,6 +172,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
a.dismiss();
}
updateBackCallback();
})
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
@@ -309,6 +324,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
}
updateBackCallback();
}
}
@@ -317,11 +333,19 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
@Override
public boolean onBackPressed(){
if(isDirty()){
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
return true;
protected void toggleCheckableItem(ListItem<?> item){
super.toggleCheckableItem(item);
updateBackCallback();
}
private void updateBackCallback(){
boolean dirty=isDirty();
if(dirty!=wasDirty){
wasDirty=dirty;
if(dirty)
addBackCallback(confirmCallback);
else
removeBackCallback(confirmCallback);
}
return false;
}
}

View File

@@ -11,9 +11,7 @@ import java.util.Arrays;
import java.util.EnumSet;
import java.util.stream.Collectors;
import me.grishka.appkit.fragments.OnBackPressedListener;
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
public class FilterContextFragment extends BaseSettingsFragment<FilterContext>{
private EnumSet<FilterContext> context;
@Override
@@ -33,7 +31,8 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
protected void doLoadData(int offset, int count){}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
context=EnumSet.noneOf(FilterContext.class);
for(ListItem<FilterContext> item:data){
if(((CheckableListItem<FilterContext>) item).checked)
@@ -42,6 +41,5 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
Bundle args=new Bundle();
args.putSerializable("context", context);
setResult(true, args);
return false;
}
}

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
@@ -11,11 +10,9 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.FilterKeyword;
@@ -33,15 +30,15 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword>{
private Button fab;
private ActionMode actionMode;
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
private ArrayList<String> deletedItemIDs=new ArrayList<>();
private MenuItem deleteItem;
private Runnable actionModeDismisser=()->actionMode.finish();
public FilterWordsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
@@ -80,12 +77,12 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
Bundle result=new Bundle();
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
result.putStringArrayList("deleted", deletedItemIDs);
setResult(true, result);
return false;
}
@Override
@@ -259,6 +256,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
updateActionModeTitle();
addBackCallback(actionModeDismisser);
}
private void leaveSelectionMode(boolean fromActionMode){
@@ -280,6 +278,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
data.set(i, newItem);
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
removeBackCallback(actionModeDismisser);
}
private void updateActionModeTitle(){

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Toast;
@@ -9,6 +11,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.updater.GithubSelfUpdater;
@@ -18,6 +21,8 @@ import java.util.List;
import me.grishka.appkit.Nav;
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> donationsStagingItem;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -28,7 +33,9 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick),
new ListItem<>("Clear dismissed donation campaigns", null, this::onClearDismissedCampaignsClick),
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", null, CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem)
));
if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@@ -39,6 +46,12 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
@Override
protected void doLoadData(int offset, int count){}
@Override
public void onStop(){
super.onStop();
getPrefs().edit().putBoolean("donationsStaging", donationsStagingItem.checked).apply();
}
private void onTestEmailConfirmClick(ListItem<?> item){
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false;
@@ -70,9 +83,18 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
}
private void onClearDismissedCampaignsClick(ListItem<?> item){
AccountSessionManager.getInstance().clearDismissedDonationCampaigns();
Toast.makeText(getActivity(), "Dismissed campaigns cleared. Restart app to see your current campaign, if any", Toast.LENGTH_LONG).show();
}
private void restartUI(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
private SharedPreferences getPrefs(){
return getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE);
}
}

View File

@@ -1,10 +1,14 @@
package org.joinmastodon.android.fragments.settings;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.squareup.otto.Subscribe;
@@ -12,27 +16,38 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
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.donations.DonationCampaign;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
private static final int DONATION_RESULT=433;
private boolean loggedOut;
private HideableSingleViewRecyclerAdapter bannerAdapter;
private Button updateButton1, updateButton2;
private TextView updateText;
private DonationSheet donationSheet;
private Runnable updateDownloadProgressUpdater=new Runnable(){
@Override
public void run(){
@@ -49,21 +64,26 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
super.onCreate(savedInstanceState);
setTitle(R.string.settings);
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
onDataLoaded(List.of(
ArrayList<ListItem<Void>> items=new ArrayList<>();
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
items.add(new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
}
items.addAll(List.of(
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick),
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick),
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true)
));
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
items.add(new ListItem<>(R.string.settings_donate, 0, R.drawable.ic_volunteer_activism_24px, this::onDonateClick));
items.add(new ListItem<>(R.string.settings_manage_donations, 0, R.drawable.ic_settings_heart_24px, this::onManageDonationClick, 0, true));
}
items.add(new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick));
items.add(new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false));
onDataLoaded(items);
AccountSession session=AccountSessionManager.get(accountID);
session.reloadPreferences(null);
@@ -117,6 +137,17 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==DONATION_RESULT){
if(donationSheet!=null)
donationSheet.dismissWithoutAnimation();
if(resultCode==Activity.RESULT_OK){
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
}
}
}
private Bundle makeFragmentArgs(){
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -167,6 +198,39 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
.show();
}
private void onDonateClick(ListItem<?> item){
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), "menu");
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
req.setStaging(true);
}
req.setCallback(new Callback<>(){
@Override
public void onSuccess(DonationCampaign result){
Activity activity=getActivity();
if(activity==null)
return;
if(result==null){
Toast.makeText(activity, "No campaign available (server misconfiguration?)", Toast.LENGTH_SHORT).show();
return;
}
donationSheet=new DonationSheet(getActivity(), result, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
donationSheet.setOnDismissListener(dialog->donationSheet=null);
donationSheet.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.execNoAuth("");
}
private void onManageDonationClick(ListItem<?> item){
UiUtils.launchWebBrowser(getActivity(), "https://sponsor.staging.joinmastodon.org/donate/manage");
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateBanner();

View File

@@ -100,15 +100,19 @@ public class SettingsServerAboutFragment extends LoaderFragment{
scroller.setClipToPadding(false);
scroller.addView(scrollingLayout);
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
if(!TextUtils.isEmpty(instance.thumbnail)){
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
}else{
scrollingLayout.setPadding(0, V.dp(24), 0, 0);
}
boolean needDivider=false;
if(instance.contactAccount!=null){

View File

@@ -36,6 +36,8 @@ public class Card extends BaseModel{
public String blurhash;
public List<History> history;
public Instant publishedAt;
public Account authorAccount;
public List<Author> authors;
public transient Drawable blurhashPlaceholder;
@@ -49,6 +51,13 @@ public class Card extends BaseModel{
if(placeholder!=null)
blurhashPlaceholder=new BlurHashDrawable(placeholder, width, height);
}
if(authorAccount!=null)
authorAccount.postprocess();
if(authors!=null){
for(Author a:authors){
a.postprocess();
}
}
}
@Override
@@ -82,4 +91,19 @@ public class Card extends BaseModel{
@SerializedName("rich")
RICH
}
@Parcel
public static class Author extends BaseModel{
@RequiredField
public String name;
public String url;
public Account account;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
if(account!=null)
account.postprocess();
}
}
}

View File

@@ -0,0 +1,27 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import java.time.Instant;
public class NotificationRequest extends BaseModel{
@RequiredField
public String id;
@RequiredField
public Instant createdAt;
@RequiredField
public Instant updatedAt;
public int notificationsCount;
@RequiredField
public Account account;
public Status lastStatus;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
account.postprocess();
if(lastStatus!=null)
lastStatus.postprocess();
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.model;
public class NotificationsPolicy extends BaseModel{
public boolean filterNewAccounts;
public boolean filterNotFollowers;
public boolean filterNotFollowing;
public boolean filterPrivateMentions;
public Summary summary;
public static class Summary{
public int pendingNotificationsCount;
public int pendingRequestsCount;
}
}

View File

@@ -10,7 +10,7 @@ import androidx.annotation.StringRes;
public class PushNotification extends BaseModel{
public String accessToken;
public String preferredLocale;
public long notificationId;
public String notificationId;
@RequiredField
public Type notificationType;
@RequiredField

View File

@@ -58,7 +58,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public boolean favourited;
public boolean reblogged;
public boolean muted;
public Boolean muted;
public boolean bookmarked;
public Boolean pinned;

View File

@@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
import java.net.IDN;
@@ -15,14 +16,18 @@ import java.util.List;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@AllFieldsAreRequired
public class CatalogInstance extends BaseModel{
@RequiredField
public String domain;
@RequiredField
public String version;
@RequiredField
public String description;
@RequiredField
public List<String> languages;
@SerializedName("region")
private String _region;
@RequiredField
public List<String> categories;
public String proxiedThumbnail;
public int totalUsers;

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.model.donations;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
import java.util.Map;
@AllFieldsAreRequired
public class DonationCampaign extends BaseModel{
public String id;
public String bannerMessage;
public String bannerButtonText;
public String donationMessage;
public String donationButtonText;
public Amounts amounts;
public String defaultCurrency;
public String donationUrl;
public String donationSuccessPost;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
amounts.postprocess();
}
public static class Amounts extends BaseModel{
public Map<String, long[]> oneTime;
@RequiredField
public Map<String, long[]> monthly;
public Map<String, long[]> yearly;
}
}

View File

@@ -25,6 +25,10 @@ public class AccountViewModel{
public final String verifiedLink;
public AccountViewModel(Account account, String accountID){
this(account, accountID, true);
}
public AccountViewModel(Account account, String accountID, boolean needBio){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
@@ -32,9 +36,13 @@ public class AccountViewModel{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
else
parsedName=account.displayName;
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
ssb.append(parsedBio);
if(needBio){
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
ssb.append(parsedBio);
}else{
parsedBio=null;
}
emojiHelper.setText(ssb);
String verifiedLink=null;
for(AccountField fld:account.fields){

View File

@@ -1,19 +1,62 @@
package org.joinmastodon.android.model.viewmodel;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class CardViewModel{
public final Object parentObject;
public final Card card;
public final ImageLoaderRequest imageRequest;
public final UrlImageLoaderRequest authorAvaRequest;
public final SpannableStringBuilder parsedAuthorName;
public final CustomEmojiHelper authorNameEmojiHelper=new CustomEmojiHelper();
public CardViewModel(Card card, int width, int height){
public CardViewModel(Card card, int width, int height, Object parentObject, String accountID){
this.card=card;
this.parentObject=parentObject;
this.imageRequest=TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(width), V.dp(height));
Account authorAccount=getAuthorAccount();
if(authorAccount!=null){
parsedAuthorName=new SpannableStringBuilder(authorAccount.displayName);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(parsedAuthorName, authorAccount.emojis);
authorNameEmojiHelper.setText(parsedAuthorName);
authorAvaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? authorAccount.avatar : authorAccount.avatarStatic, V.dp(50), V.dp(50));
}else{
parsedAuthorName=null;
authorAvaRequest=null;
}
}
public int getImageCount(){
return 1+(getAuthorAccount()!=null ? (1+authorNameEmojiHelper.getImageCount()) : 0);
}
public ImageLoaderRequest getImageRequest(int index){
return switch(index){
case 0 -> imageRequest;
case 1 -> authorAvaRequest;
default -> authorNameEmojiHelper.getImageRequest(index-2);
};
}
public Account getAuthorAccount(){
if(card.authors!=null && !card.authors.isEmpty() && card.authors.get(0).account!=null)
return card.authors.get(0).account;
else
return card.authorAccount;
}
}

View File

@@ -68,6 +68,12 @@ public class ListItem<T>{
this.subtitleRes=subtitleRes;
}
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, T parentObject, Consumer<ListItem<T>> onClick){
this(null, null, iconRes, onClick, parentObject, 0, false);
this.titleRes=titleRes;
this.subtitleRes=subtitleRes;
}
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
this.titleRes=titleRes;

View File

@@ -35,8 +35,9 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
this.drawDividerPredicate=drawDividerPredicate;
}
public void setDrawBelowLastItem(boolean drawBelowLastItem){
public DividerItemDecoration setDrawBelowLastItem(boolean drawBelowLastItem){
this.drawBelowLastItem=drawBelowLastItem;
return this;
}
@Override

View File

@@ -0,0 +1,61 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ExtendedPopupMenu extends PopupWindow{
private UsableRecyclerView list;
public <T> ExtendedPopupMenu(Context context, List<ListItem<T>> items){
super(context, null, 0, R.style.Widget_Mastodon_PopupMenu);
setWidth(V.dp(200));
setElevation(V.dp(3));
setOutsideTouchable(true);
setFocusable(true);
setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
list=new UsableRecyclerView(context);
list.setLayoutManager(new LinearLayoutManager(context));
list.setAdapter(new ReducedPaddingItemsAdapter<>(items));
list.setClipToPadding(false);
setContentView(list);
}
@Override
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity){
super.showAsDropDown(anchor, xoff, yoff, gravity);
View bgView=(View) list.getParent();
list.setPadding(0, bgView.getPaddingTop(), 0, bgView.getPaddingBottom());
bgView.setPadding(0, 0, 0, 0);
}
private static class ReducedPaddingItemsAdapter<T> extends GenericListItemsAdapter<T>{
public ReducedPaddingItemsAdapter(List<ListItem<T>> listItems){
super(listItems);
}
@NonNull
@Override
public ListItemViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
ListItemViewHolder<?> holder=super.onCreateViewHolder(parent, viewType);
int padH=V.dp(12), padV=V.dp(8);
holder.itemView.setPadding(padH, padV, padH, padV);
View icon=holder.itemView.findViewById(R.id.icon);
((ViewGroup.MarginLayoutParams)icon.getLayoutParams()).setMarginEnd(padH);
return holder;
}
}
}

View File

@@ -50,6 +50,11 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
helpButton.setSelected(helpText.getVisibility()==View.VISIBLE);
});
setCustomTitle(titleLayout);
}else if(!TextUtils.isEmpty(title)){
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title, null);
TextView title=titleLayout.findViewById(R.id.title);
title.setText(this.title);
setCustomTitle(titleLayout);
}
alert=super.create();

View File

@@ -58,7 +58,7 @@ public class PhotoLayoutHelper{
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
if(cnt==2){
if(allAreWide && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
if(allAreWide && avgRatio>1.4*maxRatio && Math.abs(ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
float h=Math.max(Math.min(MAX_WIDTH/ratios.get(0), Math.min(MAX_WIDTH/ratios.get(1), (MAX_HEIGHT-GAP)/2.0f)), MIN_HEIGHT/2f);
result.width=MAX_WIDTH;
@@ -69,7 +69,23 @@ public class PhotoLayoutHelper{
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1)
};
}else if(allAreWide || allAreSquare){ // next to each other, same ratio
}else if(allAreWide){ // two wide photos, one above the other, different ratios
result.width=MAX_WIDTH;
float h0=MAX_WIDTH/ratios.get(0);
float h1=MAX_WIDTH/ratios.get(1);
if(h0+h1<MIN_HEIGHT){
float prevTotalHeight=h0+h1;
h0=MIN_HEIGHT*(h0/prevTotalHeight);
h1=MIN_HEIGHT*(h1/prevTotalHeight);
}
result.height=Math.round(h0+h1+GAP);
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
result.columnSizes=new int[]{MAX_WIDTH};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1)
};
}else if(allAreSquare){ // next to each other, same ratio
float w=((MAX_WIDTH-GAP)/2);
float h=Math.max(Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), MAX_HEIGHT)), MIN_HEIGHT);

View File

@@ -6,6 +6,7 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -19,6 +20,9 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -48,6 +52,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private final ImageView share;
private final ColorStateList buttonColors;
private final View replyBtn, boostBtn, favoriteBtn, shareBtn;
private final PopupMenu boostLongTapMenu, favoriteLongTapMenu;
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@Override
@@ -97,11 +102,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
replyBtn.setOnClickListener(this::onReplyClick);
replyBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
boostBtn.setOnClickListener(this::onBoostClick);
boostBtn.setOnLongClickListener(this::onBoostLongClick);
boostBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
favoriteBtn.setOnClickListener(this::onFavoriteClick);
favoriteBtn.setOnLongClickListener(this::onFavoriteLongClick);
favoriteBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
shareBtn.setOnClickListener(this::onShareClick);
shareBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
favoriteLongTapMenu=new PopupMenu(activity, favoriteBtn);
favoriteLongTapMenu.inflate(R.menu.favorite_longtap);
favoriteLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
boostLongTapMenu=new PopupMenu(activity, boostBtn);
boostLongTapMenu.inflate(R.menu.boost_longtap);
boostLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
}
@Override
@@ -172,6 +186,45 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
UiUtils.openSystemShareSheet(v.getContext(), item.status);
}
private boolean onBoostLongClick(View v){
MenuItem boost=boostLongTapMenu.getMenu().findItem(R.id.boost);
boost.setTitle(item.status.reblogged ? R.string.undo_reblog : R.string.button_reblog);
boostLongTapMenu.show();
return true;
}
private boolean onFavoriteLongClick(View v){
MenuItem favorite=favoriteLongTapMenu.getMenu().findItem(R.id.favorite);
MenuItem bookmark=favoriteLongTapMenu.getMenu().findItem(R.id.bookmark);
favorite.setTitle(item.status.favourited ? R.string.undo_favorite : R.string.button_favorite);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
favoriteLongTapMenu.show();
return true;
}
private boolean onLongTapMenuItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.favorite){
onFavoriteClick(null);
}else if(id==R.id.boost){
onBoostClick(null);
}else if(id==R.id.bookmark){
AccountSessionManager.getInstance().getAccount(this.item.accountID).getStatusInteractionController().setBookmarked(this.item.status, !this.item.status.bookmarked);
}else if(id==R.id.view_favorites){
startAccountListFragment(StatusFavoritesListFragment.class);
}else if(id==R.id.view_boosts){
startAccountListFragment(StatusReblogsListFragment.class);
}
return true;
}
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), cls, args);
}
private int descriptionForId(int id){
if(id==R.id.reply_btn)
return R.string.button_reply;

View File

@@ -25,11 +25,13 @@ 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.requests.statuses.SetStatusConversationMuted;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
@@ -117,6 +119,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private final TextView name, timeAndUsername, extraText;
private final ImageView avatar, more;
private final PopupMenu optionsMenu;
private final View clickableThing;
public Holder(Activity activity, ViewGroup parent){
this(activity, R.layout.display_item_header, parent);
@@ -129,14 +132,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
clickableThing=findViewById(R.id.clickable_thing);
if(clickableThing!=null)
clickableThing.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
avatar.setClipToOutline(true);
more.setOnClickListener(this::onMoreClick);
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic())
optionsMenu.getMenu().setGroupDividerEnabled(true);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
@@ -228,6 +233,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
})
.wrapProgress(activity, R.string.loading, true)
.exec(item.accountID);
}else if(id==R.id.mute_conversation){
new SetStatusConversationMuted(item.status.id, !item.status.muted)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
// TODO snackbar?
item.status.muted=result.muted;
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, true)
.exec(item.accountID);
}
return true;
});
@@ -244,7 +265,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
timeAndUsername.setText(time+" · @"+item.user.acct);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : V.dp(4));
if(TextUtils.isEmpty(item.extraText)){
extraText.setVisibility(View.GONE);
}else{
@@ -252,8 +273,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
extraText.setText(item.extraText);
}
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
avatar.setClickable(!item.inset);
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
if(clickableThing!=null){
clickableThing.setClickable(!item.inset);
clickableThing.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
}
}
@Override
@@ -314,6 +337,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
MenuItem follow=menu.findItem(R.id.follow);
MenuItem bookmark=menu.findItem(R.id.bookmark);
MenuItem pin=menu.findItem(R.id.pin);
MenuItem muteConversation=menu.findItem(R.id.mute_conversation);
if(item.status!=null){
bookmark.setVisible(true);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
@@ -340,6 +364,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
}
if(item.status.muted!=null){
muteConversation.setVisible(isOwnPost || item.parentFragment instanceof NotificationsListFragment);
muteConversation.setTitle(item.status.muted ? R.string.unmute_conversation : R.string.mute_conversation);
}else{
muteConversation.setVisible(false);
}
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
}
}

View File

@@ -1,143 +1,55 @@
package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.app.Activity;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.viewholders.LinkCardHolder;
import java.util.Objects;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
public class LinkCardStatusDisplayItem extends StatusDisplayItem implements LinkCardHolder.LinkCardProvider{
private final Status status;
private final UrlImageLoaderRequest imgRequest;
private final CardViewModel cardViewModel;
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
super(parentID, parentFragment);
this.status=status;
if(status.card.image!=null)
imgRequest=new UrlImageLoaderRequest(status.card.image, 1000, 1000);
else
imgRequest=null;
int size=shouldUseLargeCard() ? 1000 : 192;
cardViewModel=new CardViewModel(status.card, size, size, status, parentFragment.getAccountID());
}
private boolean shouldUseLargeCard(){
return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height);
}
@Override
public Type getType(){
return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height) ? Type.CARD_LARGE : Type.CARD_COMPACT;
return shouldUseLargeCard() ? Type.CARD_LARGE : Type.CARD_COMPACT;
}
@Override
public int getImageCount(){
return imgRequest==null ? 0 : 1;
return cardViewModel.getImageCount();
}
@Override
public ImageLoaderRequest getImageRequest(int index){
return imgRequest;
return cardViewModel.getImageRequest(index);
}
public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView title, description, domain, timestamp;
private final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
private final View inner;
private final boolean isLarge;
@Override
public CardViewModel getCard(){
return cardViewModel;
}
public Holder(Context context, ViewGroup parent, boolean isLarge){
super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent);
this.isLarge=isLarge;
title=findViewById(R.id.title);
description=findViewById(R.id.description);
domain=findViewById(R.id.domain);
timestamp=findViewById(R.id.timestamp);
photo=findViewById(R.id.photo);
inner=findViewById(R.id.inner);
inner.setOnClickListener(this::onClick);
inner.setOutlineProvider(OutlineProviders.roundedRect(12));
inner.setClipToOutline(true);
}
public static class Holder extends LinkCardHolder<LinkCardStatusDisplayItem>{
@SuppressLint("SetTextI18n")
@Override
public void onBind(LinkCardStatusDisplayItem item){
Card card=item.status.card;
title.setText(card.title);
if(description!=null){
description.setText(card.description);
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
}
String cardDomain=HtmlParser.normalizeDomain(Objects.requireNonNull(Uri.parse(card.url).getHost()));
if(isLarge && !TextUtils.isEmpty(card.authorName)){
domain.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)+" · "+cardDomain);
}else{
domain.setText(cardDomain);
}
if(card.publishedAt!=null){
timestamp.setVisibility(View.VISIBLE);
timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt));
}else{
timestamp.setVisibility(View.GONE);
}
photo.setImageDrawable(null);
if(item.imgRequest!=null){
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
photo.setBackground(null);
photo.setImageTintList(null);
crossfadeDrawable.setSize(card.width, card.height);
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
didClear=false;
}else{
photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant));
photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
photo.setScaleType(ImageView.ScaleType.CENTER);
photo.setImageResource(R.drawable.ic_feed_48px);
}
}
@Override
public void setImage(int index, Drawable drawable){
crossfadeDrawable.setImageDrawable(drawable);
if(didClear)
crossfadeDrawable.animateAlpha(0f);
Card card=item.status.card;
// Make sure the image is not stretched if the server returned wrong dimensions
if(drawable!=null && (drawable.getIntrinsicWidth()!=card.width || drawable.getIntrinsicHeight()!=card.height)){
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
}
}
@Override
public void clearImage(int index){
crossfadeDrawable.setCrossfadeAlpha(1f);
didClear=true;
}
private void onClick(View v){
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url, item.status);
public Holder(Activity context, ViewGroup parent, boolean isLarge, String accountID){
super(context, parent, isLarge, accountID);
}
}
}

View File

@@ -173,7 +173,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
controllers.add(c);
if (item.status.translation != null){
if (item.status.translation!=null && item.status.translation.mediaAttachments!=null){
if(item.status.translationState==Status.TranslationState.SHOWN){
if(!item.translatedAttachments.containsKey(att.id)){
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();

View File

@@ -125,7 +125,8 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
public void onBind(NotificationHeaderStatusDisplayItem item){
text.setText(item.text);
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
// TODO use real icons
if(item.notification.type!=Notification.Type.POLL)
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.notification.account.acct));
icon.setImageResource(switch(item.notification.type){
case FAVORITE -> R.drawable.ic_star_fill1_24px;
case REBLOG -> R.drawable.ic_repeat_fill1_24px;

View File

@@ -11,6 +11,7 @@ import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
@@ -68,8 +69,8 @@ public abstract class StatusDisplayItem{
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true);
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false);
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true, ((BaseStatusListFragment<?>)parentFragment).getAccountID());
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false, ((BaseStatusListFragment<?>)parentFragment).getAccountID());
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
@@ -151,10 +152,12 @@ public abstract class StatusDisplayItem{
if(!imageAttachments.isEmpty()){
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
mediaGrid.sensitiveRevealed=false;
}else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia){
mediaGrid.sensitiveRevealed=true;
}
contentItems.add(mediaGrid);
}
for(Attachment att:statusForContent.mediaAttachments){
@@ -224,7 +227,7 @@ public abstract class StatusDisplayItem{
FILTER_SPOILER
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
public static abstract class Holder<T> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
public Holder(View itemView){
super(itemView);
}
@@ -234,17 +237,18 @@ public abstract class StatusDisplayItem{
}
public String getItemID(){
return item.parentID;
return item instanceof StatusDisplayItem sdi ? sdi.parentID : null;
}
@Override
public void onClick(){
item.parentFragment.onItemClick(item.parentID);
if(item instanceof StatusDisplayItem sdi)
sdi.parentFragment.onItemClick(sdi.parentID);
}
@Override
public boolean isEnabled(){
return item.parentFragment.isItemEnabled(item.parentID);
return item instanceof StatusDisplayItem sdi && sdi.parentFragment.isItemEnabled(sdi.parentID);
}
}
}

View File

@@ -94,7 +94,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
itemView.setClickable(false);
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom());
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(12), text.getPaddingRight(), text.getPaddingBottom());
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
updateTranslation(false);
}

View File

@@ -54,6 +54,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import android.window.OnBackInvokedDispatcher;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
@@ -169,7 +170,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
windowView=new FrameLayout(activity){
@Override
public boolean dispatchKeyEvent(KeyEvent event){
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
onStartSwipeToDismissTransition(0f);
}
@@ -257,6 +258,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
wm.addView(windowView, wlp);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
// TODO make use of the progress callback for nicer animation
windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->onStartSwipeToDismissTransition(0));
}
windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override

View File

@@ -69,6 +69,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
backButton.setElevation(V.dp(2));
backButton.setAlpha(0f);
backButton.setContentDescription(context.getString(R.string.back));
backButton.setOnClickListener(v->{
listener.onDismissEntireViewer();
dismiss();
@@ -82,6 +83,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
infoButton.setElevation(V.dp(2));
infoButton.setAlpha(0f);
infoButton.setSelected(true);
infoButton.setContentDescription(context.getString(R.string.info));
infoButton.setOnClickListener(v->dismiss());
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));

View File

@@ -0,0 +1,353 @@
package org.joinmastodon.android.ui.sheets;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import org.joinmastodon.android.DonationFragmentActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.DonationWebViewFragment;
import org.joinmastodon.android.model.donations.DonationCampaign;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CurrencyAmountInput;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.CustomViewHelper;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class DonationSheet extends BottomSheet{
private final DonationCampaign campaign;
private final String accountID;
private final Consumer<Intent> startCallback;
private DonationFrequency frequency=DonationFrequency.MONTHLY;
private View onceTab, monthlyTab, yearlyTab;
private int currentTab;
private CurrencyAmountInput amountField;
private ToggleButton[] suggestedAmountButtons=new ToggleButton[6];
private View button;
private TextView buttonText;
private Activity activity;
public DonationSheet(@NonNull Activity activity, DonationCampaign campaign, String accountID, Consumer<Intent> startCallback){
super(activity);
this.campaign=campaign;
this.accountID=accountID;
this.activity=activity;
this.startCallback=startCallback;
Context context=activity;
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
TextView text=findViewById(R.id.text);
text.setText(campaign.donationMessage);
onceTab=findViewById(R.id.once);
monthlyTab=findViewById(R.id.monthly);
yearlyTab=findViewById(R.id.yearly);
onceTab.setOnClickListener(this::onTabClick);
monthlyTab.setOnClickListener(this::onTabClick);
yearlyTab.setOnClickListener(this::onTabClick);
if(campaign.amounts.yearly==null)
yearlyTab.setVisibility(View.GONE);
if(campaign.amounts.oneTime==null)
onceTab.setVisibility(View.GONE);
if(campaign.amounts.monthly==null){
monthlyTab.setVisibility(View.GONE);
if(campaign.amounts.oneTime!=null){
onceTab.setSelected(true);
currentTab=R.id.once;
frequency=DonationFrequency.ONCE;
}else if(campaign.amounts.yearly!=null){
yearlyTab.setSelected(true);
currentTab=R.id.yearly;
frequency=DonationFrequency.YEARLY;
}else{
Toast.makeText(context, "Amounts object is empty", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
}else{
monthlyTab.setSelected(true);
currentTab=R.id.monthly;
}
View tabBarItself=findViewById(R.id.tabbar_inner);
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
tabBarItself.setClipToOutline(true);
amountField=findViewById(R.id.amount);
List<String> availableCurrencies=campaign.amounts.monthly.keySet().stream().sorted().collect(Collectors.toList());
amountField.setCurrencies(availableCurrencies);
try{
amountField.setSelectedCurrency(campaign.defaultCurrency);
}catch(IllegalArgumentException x){
new M3AlertDialogBuilder(context)
.setTitle(R.string.error)
.setMessage("Default currency "+campaign.defaultCurrency+" not in list of available currencies "+availableCurrencies)
.show();
dismiss();
return;
}
amountField.setChangeListener(new CurrencyAmountInput.ChangeListener(){
@Override
public void onCurrencyChanged(String code){
updateSuggestedAmounts(code);
button.setEnabled(amountField.getAmount()>=getMinimumChargeAmount(code));
updateSuggestedButtonsState();
}
@Override
public void onAmountChanged(long amount){
button.setEnabled(amount>=getMinimumChargeAmount(amountField.getCurrency()));
updateSuggestedButtonsState();
}
});
button=findViewById(R.id.button);
buttonText=findViewById(R.id.button_text);
ViewGroup suggestedAmounts=findViewById(R.id.suggested_amounts);
for(int i=0;i<suggestedAmountButtons.length;i++){
ToggleButton btn=new ToggleButton(context);
btn.setBackgroundResource(R.drawable.bg_filter_chip);
btn.setTextAppearance(R.style.m3_label_large);
btn.setTextColor(context.getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
btn.setMinWidth(V.dp(64));
btn.setMinimumWidth(0);
btn.setPadding(0, 0, 0, 0);
btn.setStateListAnimator(null);
btn.setTextOff(null);
btn.setTextOn(null);
btn.setOnClickListener(this::onSuggestedAmountClick);
btn.setTag(i);
btn.setSingleLine();
suggestedAmountButtons[i]=btn;
suggestedAmounts.addView(btn);
}
updateSuggestedAmounts(campaign.defaultCurrency);
button.setEnabled(false);
buttonText.setText(campaign.donationButtonText);
button.setOnClickListener(v->openWebView());
Arrays.stream(getCurrentSuggestedAmounts(campaign.defaultCurrency)).min().ifPresent(amountField::setAmount);
}
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Window window=getWindow();
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
private void onTabClick(View v){
if(v.getId()==currentTab)
return;
findViewById(currentTab).setSelected(false);
v.setSelected(true);
currentTab=v.getId();
if(currentTab==R.id.once)
frequency=DonationFrequency.ONCE;
else if(currentTab==R.id.monthly)
frequency=DonationFrequency.MONTHLY;
else if(currentTab==R.id.yearly)
frequency=DonationFrequency.YEARLY;
updateSuggestedAmounts(amountField.getCurrency());
}
private long[] getCurrentSuggestedAmounts(String currency){
long[] amounts=(switch(frequency){
case ONCE -> campaign.amounts.oneTime;
case MONTHLY -> campaign.amounts.monthly;
case YEARLY -> campaign.amounts.yearly;
}).get(currency);
if(amounts==null){
amounts=new long[0];
}
return amounts;
}
private void updateSuggestedAmounts(String currency){
NumberFormat format=NumberFormat.getCurrencyInstance();
try{
format.setCurrency(Currency.getInstance(currency));
}catch(IllegalArgumentException ignore){}
int defaultFractionDigits=format.getMinimumFractionDigits();
long[] amounts=getCurrentSuggestedAmounts(currency);
for(int i=0;i<suggestedAmountButtons.length;i++){
ToggleButton btn=suggestedAmountButtons[i];
if(i>=amounts.length){
btn.setVisibility(View.GONE);
continue;
}
btn.setVisibility(View.VISIBLE);
long amount=amounts[i];
format.setMinimumFractionDigits(amount%100==0 ? 0 : defaultFractionDigits);
btn.setText(format.format(amount/100.0));
}
updateSuggestedButtonsState();
}
private void onSuggestedAmountClick(View v){
int index=(int) v.getTag();
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
amountField.setAmount(amounts[index]);
}
private void updateSuggestedButtonsState(){
long amount=amountField.getAmount();
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
for(int i=0;i<Math.min(amounts.length, suggestedAmountButtons.length);i++){
ToggleButton btn=suggestedAmountButtons[i];
btn.setChecked(amounts[i]==amount);
}
}
private void openWebView(){
Uri.Builder builder=Uri.parse(campaign.donationUrl).buildUpon();
builder.appendQueryParameter("locale", Locale.getDefault().toLanguageTag().replace('-', '_'))
.appendQueryParameter("platform", "android")
.appendQueryParameter("currency", amountField.getCurrency())
.appendQueryParameter("amount", String.valueOf(amountField.getAmount()))
.appendQueryParameter("source", "campaign")
.appendQueryParameter("campaign_id", campaign.id)
.appendQueryParameter("frequency", switch(frequency){
case ONCE -> "one_time";
case MONTHLY -> "monthly";
case YEARLY -> "yearly";
})
.appendQueryParameter("success_callback_url", DonationWebViewFragment.SUCCESS_URL)
.appendQueryParameter("cancel_callback_url", DonationWebViewFragment.CANCEL_URL)
.appendQueryParameter("failure_callback_url", DonationWebViewFragment.FAILURE_URL);
Bundle args=new Bundle();
args.putString("url", builder.build().toString());
args.putString("account", accountID);
args.putString("campaignID", campaign.id);
args.putString("successPostText", campaign.donationSuccessPost);
args.putBoolean("_can_go_back", true);
startCallback.accept(new Intent(activity, DonationFragmentActivity.class).putExtra("fragmentArgs", args));
}
private static long getMinimumChargeAmount(String currency){
// https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts
// values are in cents
return switch(currency){
case "USD" -> 50;
case "AED" -> 2_00;
case "AUD" -> 50;
case "BGN" -> 1_00;
case "BRL" -> 50;
case "CAD" -> 50;
case "CHF" -> 50;
case "CZK" -> 15_00;
case "DKK" -> 2_50;
case "EUR" -> 50;
case "GBP" -> 30;
case "HKD" -> 4_00;
case "HUF" -> 175_00;
case "INR" -> 50;
case "JPY" -> 50_00;
case "MXN" -> 10_00;
case "MYR" -> 2_00;
case "NOK" -> 3_00;
case "NZD" -> 50;
case "PLN" -> 2_00;
case "RON" -> 2_00;
case "SEK" -> 3_00;
case "SGD" -> 50;
case "THB" -> 10_00;
default -> 50;
};
}
private enum DonationFrequency{
ONCE,
MONTHLY,
YEARLY
}
public static class SuggestedAmountsLayout extends ViewGroup implements CustomViewHelper{
private int visibleChildCount;
private static final int H_GAP=24;
private static final int V_GAP=8;
private static final int ROW_HEIGHT=32;
public SuggestedAmountsLayout(Context context){
this(context, null);
}
public SuggestedAmountsLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public SuggestedAmountsLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
visibleChildCount=0;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
visibleChildCount++;
}
int width=MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(width, visibleChildCount>4 ? dp(ROW_HEIGHT*2+V_GAP) : dp(ROW_HEIGHT));
int buttonsPerRow=visibleChildCount>4 ? 3 : visibleChildCount;
int buttonWidth=(width-dp(H_GAP)*(buttonsPerRow-1))/buttonsPerRow;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
child.measure(buttonWidth | MeasureSpec.EXACTLY, dp(ROW_HEIGHT) | MeasureSpec.EXACTLY);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
int width=r-l;
int buttonsPerRow=visibleChildCount>4 ? 3 : visibleChildCount;
int buttonWidth=(width-dp(H_GAP)*(buttonsPerRow-1))/buttonsPerRow;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
int column=i%buttonsPerRow;
int row=i/buttonsPerRow;
int left=(buttonWidth+dp(H_GAP))*column;
int top=dp(ROW_HEIGHT+V_GAP)*row;
child.layout(left, top, left+buttonWidth, top+dp(ROW_HEIGHT));
}
}
}
}

View File

@@ -0,0 +1,40 @@
package org.joinmastodon.android.ui.sheets;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
import me.grishka.appkit.views.BottomSheet;
public class DonationSuccessfulSheet extends BottomSheet{
public DonationSuccessfulSheet(@NonNull Context context, @NonNull String accountID, String postText){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation_success, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
content.findViewById(R.id.btn_done).setOnClickListener(v->dismiss());
View shareButton=content.findViewById(R.id.btn_share);
if(postText==null){
shareButton.setEnabled(false);
}
shareButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", postText);
Nav.go((Activity) context, ComposeFragment.class, args);
dismiss();
});
}
}

Some files were not shown because too many files have changed in this diff Show More