Compare commits

...

299 Commits

Author SHA1 Message Date
sk
0cc8cddfc3 support recently used emoji
closes sk22#832
2023-10-07 22:10:52 +02:00
sk
4428ef7ac2 display recent languages
closes sk22#654
2023-10-07 19:47:49 +02:00
sk
44912b7982 display timestamp in notification header
closes sk22#668
2023-10-07 19:17:45 +02:00
sk
c930db6068 refactor filtering
hopefully also fixes sk22#816
2023-10-07 18:25:37 +02:00
sk
d96d4dd581 unselect add button on bind
closes sk22#830
2023-10-07 17:52:39 +02:00
sk
67e3a5bb47 fix warning not clickable if main status 2023-10-07 17:50:24 +02:00
sk22
b0a5aa93e1 Translated using Weblate (German)
Currently translated at 100.0% (389 of 389 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-10-07 15:39:39 +00:00
sk22
0bc1459898 Translated using Weblate (English)
Currently translated at 100.0% (389 of 389 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/en/
2023-10-07 15:39:38 +00:00
sk
fae25e93a5 Merge remote-tracking branch 'weblate/main' 2023-10-07 17:29:18 +02:00
kallekn
c0c121050c Translated using Weblate (Finnish)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fi/
2023-10-07 15:29:11 +00:00
teknopata
afe572ca7f Translated using Weblate (Basque)
Currently translated at 78.6% (305 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/eu/
2023-10-07 15:29:11 +00:00
sk
8cb4db5fcf Merge remote-tracking branch 'weblate/main' 2023-10-07 17:28:51 +02:00
sk
de235ec7cc use upstream filtering for warn item 2023-10-07 17:21:20 +02:00
sk
140c2a7b9d remove status filter predicate from thread 2023-10-07 17:04:55 +02:00
sk
c833513344 use same filtering function everywhere 2023-10-07 17:02:03 +02:00
sk
1e95536208 fix issue with loading more 2023-10-07 17:00:16 +02:00
sk
b58fda9795 remove unused method 2023-10-07 16:12:35 +02:00
sk
3135aef398 fix wrong overflow button style 2023-10-07 16:03:30 +02:00
sk
c83dc51322 re-add ripple to filter warning 2023-10-07 15:43:16 +02:00
sk
9cfaed89e6 fix filter warning background color
closes sk22#823
2023-10-07 15:26:10 +02:00
sk
5374ac766c prettier visibility toggle animation
maybe fix sk22#845
2023-10-07 15:21:06 +02:00
sk
98596e77f2 fix interaction button colors
closes sk22#841
2023-10-07 12:44:39 +02:00
sk
54aa89c7f8 OOPS 2023-10-07 12:44:33 +02:00
sk
f59157b160 fix text not selectable
closes sk22#824
2023-10-07 12:33:14 +02:00
sk
6b38db9607 add heart symbol to extended footer 2023-10-07 12:08:37 +02:00
sk
c10cdfd795 fix color inheritance issue 2023-10-06 18:44:15 +02:00
sk
c2184e7bd8 don't dismiss when restarting activity 2023-10-06 18:00:59 +02:00
sk
baf756e163 allow using heart as fav icon ❤️
closes sk22#81
2023-10-06 17:53:43 +02:00
sk
efc67fd7e8 remove inset poll styles
closes sk22#801
2023-10-06 17:26:59 +02:00
sk
43e737425a fix null reference 2023-10-06 17:20:49 +02:00
sk
b5b3cb42a1 re-implement missing "translate opened only" 2023-10-06 17:09:57 +02:00
sk
f72f7cb831 hide username wrap in edit mode
closes sk22#828
2023-10-06 17:02:52 +02:00
sk
f86d60be23 fix color palette dialog title 2023-10-06 17:00:09 +02:00
sk
7c8624bd53 fix alpha animations
closes sk22#839
2023-10-06 16:55:02 +02:00
sk
331548b38d fix crash 2023-10-06 16:20:16 +02:00
sk
8b8f192dfa fix posts/bubble banner not disappearing
closes sk22#833
2023-10-06 16:17:18 +02:00
sk
061b2ee3de per-account color palette preference 2023-10-06 16:13:38 +02:00
sk
5ea2864bd5 Merge remote-tracking branch 'upstream/master' 2023-10-06 15:17:49 +02:00
LucasGGamerM
ee2b4b6a1f fix(hashtag-fragment): fix crash when opening some hashtags (#842) 2023-10-06 15:04:02 +02:00
LucasGGamerM
697f801c1a fix(hashtags): fix crash when hashtag is null (#844) 2023-10-06 15:03:25 +02:00
sk
ebb49c44fe refactor code 2023-10-06 15:01:46 +02:00
LucasGGamerM
bc4619e6b1 fix(translations): fix crash when status language is null 2023-10-06 15:01:46 +02:00
butterflyoffire
5d26ea85e9 Translated using Weblate (Arabic)
Currently translated at 80.1% (311 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-10-05 07:53:14 +00:00
ihor_ck
6efe263dd8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-05 07:53:14 +00:00
David Lapshin
0379347f2d Translated using Weblate (Russian)
Currently translated at 87.1% (338 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-10-05 07:53:14 +00:00
alextecplayz
1299b2ad42 Translated using Weblate (Romanian)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-10-05 07:53:14 +00:00
Linerly
f3b3bcaa0a Translated using Weblate (Indonesian)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-10-05 07:53:14 +00:00
Choukajohn
b1bec870c5 Translated using Weblate (French)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-05 07:53:13 +00:00
butterflyoffire
36e05a6d14 Translated using Weblate (French)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-05 07:53:13 +00:00
gallegonovato
2e11f78e9d Translated using Weblate (Spanish)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-10-05 07:53:13 +00:00
poesty
9fcfbe5593 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (387 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-10-05 07:53:12 +00:00
Grishka
6c1c5b7759 Merge branch 'l10n_master' 2023-10-03 03:53:50 +03:00
Grishka
1f4152b588 Fix #705 and improve handling of unknown attachment dimensions 2023-10-03 02:52:07 +03:00
Grishka
70386ea1b2 Update appkit to finally fix that ViewPager2 crash 2023-10-03 02:11:04 +03:00
Eugen Rochko
cbce90c461 New translations strings.xml (Sinhala) 2023-10-02 21:16:49 +02:00
Eugen Rochko
74ae3bf706 New translations strings.xml (Armenian) 2023-10-02 07:26:27 +02:00
Grishka
1feccdc26d Fixes 2023-10-01 23:11:33 +03:00
Eugen Rochko
c38c2a425b New translations strings.xml (Indonesian) 2023-10-01 17:30:46 +02:00
Eugen Rochko
f43352b790 New translations strings.xml (Indonesian) 2023-10-01 16:30:51 +02:00
Grishka
c5b52b2781 Fix default server not loading sometimes 2023-10-01 12:17:21 +03:00
Grishka
b91840fb95 Another attempt to fix ZoomPanView crash 2023-10-01 07:16:21 +03:00
sk
1988849b26 Merge remote-tracking branch 'upstream/master' 2023-09-30 21:21:47 +02:00
Grishka
fc10fbffb0 Clear fragment stack instead of restarting activity
grishka/appkit#13
2023-09-30 21:53:02 +03:00
Eugen Rochko
e40841c128 New translations strings.xml (Portuguese, Brazilian) 2023-09-30 20:26:54 +02:00
sk
a21a74a8e7 bonk version 2023-09-30 19:26:15 +02:00
sk
20e5d2a545 Merge remote-tracking branch 'upstream/l10n_master' 2023-09-30 19:24:52 +02:00
butterflyoffire
7da363fb87 Translated using Weblate (Arabic)
Currently translated at 80.3% (310 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-30 17:23:53 +00:00
sk
c46f78272d Merge remote-tracking branch 'weblate/main' 2023-09-30 19:23:42 +02:00
Tyler Baker
95685d4de8 Feature: Support filtering custom emoji in reaction view (#836)
* Support filtering custom emojis in reaction keyboard.

* Move creation of EditText to conditional block.

* Clear unused comment

* Update requests variable when publishing filter results so the images displayed will be correct.

* Combine text fields in emoji reaction keyboard, create new initializer for EmojiCategory so it can be copied properly.

* Performance optimization and fixed a typo in filter.

* improve layout

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-09-30 19:21:36 +02:00
FineFindus
cbee0fe72e fix: show individual chips (#838) 2023-09-30 19:04:04 +02:00
FineFindus
6d085ae6f0 fix: show multiline poll options (#837)
* fix: show multiline poll options

* fix resources not found exception

* don't force height on poll options

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-09-30 19:03:17 +02:00
LucasGGamerM
4de7211523 Fix notifications replies visibility/language not being consistent with replied status (#831)
* fix(notifications): make reply visibility consistent with status being replied to

* fix(notifications): make reply language consistent with status being replied to
2023-09-30 18:30:53 +02:00
FineFindus
05f7a44bd5 fix(timeline/gap): use plurals for time (#829)
Co-authored-by: sk <sk22@mailbox.org>
2023-09-30 18:30:18 +02:00
Jacoco
1079f600bc Revert "Fix media layout with unknown sizes" (#827)
This reverts commit a014fe9443.
2023-09-30 18:25:52 +02:00
FineFindus
72580dadd0 feat: display blocks and mutes list (#821)
* feat: add mutes fragment

* feat: add blocks fragment

* refactor: add query params

* rename "mutes" and "blocks"

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-09-30 18:24:24 +02:00
Eugen Rochko
98a02e874b New translations strings.xml (Vietnamese) 2023-09-30 15:05:49 +02:00
butterflyoffire
d219d7aa4b Translated using Weblate (Arabic)
Currently translated at 78.2% (302 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-30 03:45:04 +00:00
alextecplayz
68a9fe8376 Translated using Weblate (Romanian)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-09-30 03:45:04 +00:00
sk
f2f8620312 fix menu item icons and state 2023-09-29 21:45:41 +02:00
sk
4ee229ea79 Merge remote-tracking branch 'upstream/master' into try-to-merge-upstream 2023-09-29 20:59:12 +02:00
sk
c261214e49 implement new translation 2023-09-29 18:46:26 +02:00
Eugen Rochko
b06df8c3d0 New translations strings.xml (Galician) 2023-09-29 07:53:31 +02:00
Grishka
a00afd5d7f Same crash fix in 2 more places ugh 2023-09-29 03:20:58 +03:00
Grishka
9a41a2d6fb Merge branch 'l10n_master' 2023-09-28 20:14:21 +03:00
Grishka
2cd98a6620 More crash fixes 2023-09-28 20:11:43 +03:00
Grishka
283b56be5b Finally fix the mysterious RecyclerView crash 2023-09-28 19:56:25 +03:00
Eugen Rochko
6d56771aba New translations strings.xml (Galician) 2023-09-28 16:51:59 +02:00
Grishka
1724d8a532 Probably fix #703 2023-09-27 19:50:59 +03:00
butterflyoffire
52030b3b2d Translated using Weblate (Arabic)
Currently translated at 76.1% (294 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-27 07:53:09 +00:00
Eugen Rochko
b4cdf35d36 New translations strings.xml (Russian) 2023-09-25 23:39:42 +02:00
Eugen Rochko
cad0ad7a59 New translations strings.xml (Ukrainian) 2023-09-25 22:25:17 +02:00
Grishka
ca60003c39 Fix #682 2023-09-25 23:11:10 +03:00
Grishka
0f030e0bac Fix #683 2023-09-25 23:07:34 +03:00
Grishka
6d4f212a18 Probably need to set this too 2023-09-25 23:00:15 +03:00
Grishka
183b39bc24 Specify LED color for notifications
closes #695
2023-09-25 22:58:08 +03:00
Grishka
27ad0c6fcf Crash fixes 2023-09-25 22:52:51 +03:00
Grishka
b5f661f1af I forgot to increment the version code 2023-09-25 19:25:59 +03:00
Grishka
0015f3f0bf Merge branch 'l10n_master' 2023-09-25 19:22:38 +03:00
Eugen Rochko
c5d0fdd645 New translations strings.xml (Turkish) 2023-09-25 18:20:56 +02:00
Grishka
2d09ad44fb Merge branch 'l10n_master' 2023-09-25 19:18:24 +03:00
Eugen Rochko
667fffd124 New translations strings.xml (Chinese Traditional) 2023-09-25 18:18:22 +02:00
Eugen Rochko
699233d8c7 New translations strings.xml (Filipino) 2023-09-25 18:18:05 +02:00
Grishka
56aabdc4a6 Fix empty view text style
closes #701
2023-09-25 19:12:04 +03:00
Grishka
443e2c7a6f Add a tool to detect invalid formatting in localized strings 2023-09-25 18:51:49 +03:00
Eugen Rochko
985b0f6e63 New translations strings.xml (Filipino) 2023-09-25 17:49:57 +02:00
Grishka
cc86edf276 Fix #700 2023-09-25 17:18:42 +03:00
Grishka
4071b9342d Update appkit 2023-09-25 17:13:59 +03:00
Eugen Rochko
f71d1bc5d3 New translations strings.xml (Greek) 2023-09-24 23:27:13 +02:00
Andrewblasco
2b926ffa46 Translated using Weblate (Spanish)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-09-24 08:53:08 +00:00
Eugen Rochko
6bcdbaba34 New translations strings.xml (Turkish) 2023-09-24 08:49:38 +02:00
Eugen Rochko
a2beead3a5 New translations strings.xml (Chinese Simplified) 2023-09-23 18:34:38 +02:00
Eugen Rochko
e7a25e353d New translations strings.xml (Japanese) 2023-09-23 14:15:37 +02:00
Eugen Rochko
af04a01130 New translations strings.xml (Japanese) 2023-09-23 13:16:04 +02:00
Grishka
fe1cfa1d7b Fix indexable setting 2023-09-22 21:33:21 +03:00
Eugen Rochko
b248797bb0 New translations strings.xml (Russian) 2023-09-22 20:28:14 +02:00
Grishka
f24eba08d3 Fix custom emojis in names setting 2023-09-22 21:27:30 +03:00
Eugen Rochko
0e89559a47 New translations strings.xml (Slovenian) 2023-09-22 19:30:53 +02:00
Espasant3
858657799f Translated using Weblate (Galician)
Currently translated at 98.1% (379 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-09-22 12:53:08 +00:00
Eugen Rochko
d02a72e079 New translations strings.xml (Japanese) 2023-09-22 13:01:51 +02:00
Eugen Rochko
3be57d1b0b New translations strings.xml (Japanese) 2023-09-22 11:08:13 +02:00
Eugen Rochko
bed550e97c New translations strings.xml (Russian) 2023-09-22 09:19:11 +02:00
Eugen Rochko
7e2619ea75 New translations strings.xml (Italian) 2023-09-22 01:48:10 +02:00
Eugen Rochko
4b22f1d3a7 New translations strings.xml (Thai) 2023-09-21 21:39:59 +02:00
Eugen Rochko
9dcc7e293f New translations strings.xml (Icelandic) 2023-09-21 15:39:27 +02:00
Eugen Rochko
6a68cf5e41 New translations strings.xml (Persian) 2023-09-21 13:13:31 +02:00
Grishka
29297be4a3 Merge branch 'l10n_master' 2023-09-20 22:43:16 +03:00
Eugen Rochko
90b87529e0 New translations strings.xml (Swedish) 2023-09-20 21:16:11 +02:00
Grishka
39af05524d Privacy settings 2023-09-20 21:44:28 +03:00
Grishka
e3fb2cd03c Scroll profile tab views to top when tab is reselected 2023-09-20 14:47:25 +03:00
Gregory K
90f84d628a Merge pull request #694 from LucasGGamerM/mastodon-android
fix(compose): fix photoPicker not popping up when there is less than 2 spaces available for media
2023-09-20 14:10:26 +03:00
LucasGGamerM
b89e0b5c5a fix(compose): fix photoPicker not popping up when there is less than 2 spaces available for media 2023-09-20 07:55:23 -03:00
Espasant3
f724644d84 Translated using Weblate (Galician)
Currently translated at 89.6% (346 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-09-19 21:53:09 +00:00
Eugen Rochko
aac89c354c New translations full_description.txt (Thai) 2023-09-18 20:35:41 +02:00
Eugen Rochko
a032f9af10 New translations strings.xml (French) 2023-09-18 20:34:32 +02:00
Eugen Rochko
642aaec6da New translations strings.xml (Persian) 2023-09-17 14:08:04 +02:00
Eugen Rochko
ff667d6aed New translations strings.xml (Persian) 2023-09-17 12:27:57 +02:00
Eugen Rochko
5e98496ea6 New translations strings.xml (Vietnamese) 2023-09-17 10:39:46 +02:00
Eugen Rochko
972fe1d15b New translations strings.xml (Italian) 2023-09-17 10:39:45 +02:00
Eugen Rochko
26eaa36faa New translations strings.xml (Finnish) 2023-09-17 10:39:44 +02:00
Grishka
c517f41595 Paginate search results 2023-09-17 11:20:12 +03:00
Grishka
56a6d7243f Crash fixes 2023-09-17 10:49:13 +03:00
Grishka
18e43dfc22 Crash fix 2023-09-17 10:32:59 +03:00
Grishka
816f6370ef Fix #690 2023-09-17 10:26:27 +03:00
butterflyoffire
30866a5292 Translated using Weblate (Arabic)
Currently translated at 71.5% (276 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-16 08:53:08 +00:00
Espasant3
3e1403d18a Translated using Weblate (Galician)
Currently translated at 85.2% (329 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-09-16 08:53:08 +00:00
Eugen Rochko
ebc2b2e59d New translations strings.xml (Arabic) 2023-09-15 11:51:46 +02:00
Eugen Rochko
c9a796dbfe New translations strings.xml (Arabic) 2023-09-15 10:16:56 +02:00
Eugen Rochko
1ba185ea9c New translations strings.xml (Vietnamese) 2023-09-15 03:45:44 +02:00
EndermanCo
1a50c3ff5f Translated using Weblate (Persian)
Currently translated at 66.6% (12 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-09-14 17:53:07 +00:00
EndermanCo
35a85c3247 Translated using Weblate (Persian)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-09-14 17:53:07 +00:00
GunChleoc
6a729fa97f Translated using Weblate (Gaelic)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gd/
2023-09-14 17:53:07 +00:00
kallekn
923639a329 Translated using Weblate (Finnish)
Currently translated at 94.5% (365 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fi/
2023-09-14 17:53:07 +00:00
Eugen Rochko
a78be8bc1d New translations strings.xml (Chinese Traditional) 2023-09-14 12:55:27 +02:00
Eugen Rochko
abfb497577 New translations strings.xml (Chinese Traditional) 2023-09-14 11:17:24 +02:00
Eugen Rochko
a10b184508 New translations full_description.txt (Finnish) 2023-09-13 21:32:19 +02:00
Eugen Rochko
f0ea6660e6 New translations strings.xml (Finnish) 2023-09-13 21:32:18 +02:00
Eugen Rochko
a829f25d56 New translations strings.xml (Thai) 2023-09-13 20:36:48 +02:00
Eugen Rochko
deff3dd8e0 New translations strings.xml (Finnish) 2023-09-13 20:36:47 +02:00
EndermanCo
dab596f527 Translated using Weblate (Persian)
Currently translated at 55.5% (10 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-09-13 09:53:07 +00:00
EndermanCo
0c18ab2319 Translated using Weblate (Persian)
Currently translated at 95.5% (369 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-09-13 09:53:07 +00:00
Eugen Rochko
6c5fb5ea09 New translations strings.xml (Dutch) 2023-09-12 12:25:35 +02:00
Eugen Rochko
afe0c9e0db New translations strings.xml (Dutch) 2023-09-12 10:19:31 +02:00
Eugen Rochko
1f2213042f New translations strings.xml (Urdu (India)) 2023-09-12 06:51:43 +02:00
Eugen Rochko
5edd2466f9 New translations strings.xml (Kabyle) 2023-09-12 06:51:42 +02:00
Eugen Rochko
f3b3a1a577 New translations strings.xml (Igbo) 2023-09-12 06:51:41 +02:00
Eugen Rochko
068619b815 New translations strings.xml (Occitan) 2023-09-12 06:51:40 +02:00
Eugen Rochko
f121e94979 New translations strings.xml (Scottish Gaelic) 2023-09-12 06:51:39 +02:00
Eugen Rochko
b5b52529d4 New translations strings.xml (Sinhala) 2023-09-12 06:51:38 +02:00
Eugen Rochko
876bf73454 New translations strings.xml (Bosnian) 2023-09-12 06:51:37 +02:00
Eugen Rochko
522dbf6e4a New translations strings.xml (Filipino) 2023-09-12 06:51:36 +02:00
Eugen Rochko
ae685095ba New translations strings.xml (Burmese) 2023-09-12 06:51:35 +02:00
Eugen Rochko
30d5fe2f12 New translations strings.xml (Hindi) 2023-09-12 06:51:34 +02:00
Eugen Rochko
2bf27c561c New translations strings.xml (Croatian) 2023-09-12 06:51:33 +02:00
Eugen Rochko
bbdc72323d New translations strings.xml (Thai) 2023-09-12 06:51:31 +02:00
Eugen Rochko
6e335930f3 New translations strings.xml (Bengali) 2023-09-12 06:51:31 +02:00
Eugen Rochko
9b309939da New translations strings.xml (Persian) 2023-09-12 06:51:30 +02:00
Eugen Rochko
faf2e5115d New translations strings.xml (Indonesian) 2023-09-12 06:51:28 +02:00
Eugen Rochko
dc5d9412c8 New translations strings.xml (Portuguese, Brazilian) 2023-09-12 06:51:27 +02:00
Eugen Rochko
fc0680d66f New translations strings.xml (Icelandic) 2023-09-12 06:51:26 +02:00
Eugen Rochko
56c9a5433f New translations strings.xml (Galician) 2023-09-12 06:51:25 +02:00
Eugen Rochko
60e473ee55 New translations strings.xml (Vietnamese) 2023-09-12 06:51:24 +02:00
Eugen Rochko
ae34ecd5c3 New translations strings.xml (Chinese Traditional) 2023-09-12 06:51:23 +02:00
Eugen Rochko
fd1caa8729 New translations strings.xml (Chinese Simplified) 2023-09-12 06:51:22 +02:00
Eugen Rochko
1182e5c60c New translations strings.xml (Ukrainian) 2023-09-12 06:51:21 +02:00
Eugen Rochko
d99d515dfa New translations strings.xml (Turkish) 2023-09-12 06:51:20 +02:00
Eugen Rochko
70a15e7d9c New translations strings.xml (Swedish) 2023-09-12 06:51:19 +02:00
Eugen Rochko
1691382369 New translations strings.xml (Slovenian) 2023-09-12 06:51:18 +02:00
Eugen Rochko
b7da9c6d51 New translations strings.xml (Russian) 2023-09-12 06:51:17 +02:00
Eugen Rochko
3426538dca New translations strings.xml (Portuguese) 2023-09-12 06:51:16 +02:00
Eugen Rochko
63de2b200b New translations strings.xml (Polish) 2023-09-12 06:51:15 +02:00
Eugen Rochko
ff1ee766dc New translations strings.xml (Norwegian) 2023-09-12 06:51:14 +02:00
Eugen Rochko
f033411adf New translations strings.xml (Dutch) 2023-09-12 06:51:13 +02:00
Eugen Rochko
a738eaf8c0 New translations strings.xml (Korean) 2023-09-12 06:51:12 +02:00
Eugen Rochko
5074aadd6e New translations strings.xml (Japanese) 2023-09-12 06:51:11 +02:00
Eugen Rochko
0854961470 New translations strings.xml (Italian) 2023-09-12 06:51:10 +02:00
Eugen Rochko
227b077935 New translations strings.xml (Armenian) 2023-09-12 06:51:09 +02:00
Eugen Rochko
1e4358290a New translations strings.xml (Hungarian) 2023-09-12 06:51:08 +02:00
Eugen Rochko
925169eb31 New translations strings.xml (Hebrew) 2023-09-12 06:51:07 +02:00
Eugen Rochko
e1abeb9252 New translations strings.xml (Irish) 2023-09-12 06:51:07 +02:00
Eugen Rochko
cbe0add211 New translations strings.xml (Finnish) 2023-09-12 06:51:06 +02:00
Eugen Rochko
299b524d62 New translations strings.xml (Basque) 2023-09-12 06:51:05 +02:00
Eugen Rochko
31c094e696 New translations strings.xml (Greek) 2023-09-12 06:51:04 +02:00
Eugen Rochko
a8038a2863 New translations strings.xml (German) 2023-09-12 06:51:02 +02:00
Eugen Rochko
29933bb916 New translations strings.xml (Danish) 2023-09-12 06:51:01 +02:00
Eugen Rochko
5ec0c078d8 New translations strings.xml (Czech) 2023-09-12 06:51:00 +02:00
Eugen Rochko
e6287f1ff2 New translations strings.xml (Catalan) 2023-09-12 06:50:59 +02:00
Eugen Rochko
be9caf8905 New translations strings.xml (Belarusian) 2023-09-12 06:50:58 +02:00
Eugen Rochko
f375142084 New translations strings.xml (Arabic) 2023-09-12 06:50:57 +02:00
Eugen Rochko
fd3668d520 New translations strings.xml (Spanish) 2023-09-12 06:50:56 +02:00
Eugen Rochko
d5e03e9d9e New translations strings.xml (French) 2023-09-12 06:50:55 +02:00
Eugen Rochko
d62f094919 New translations strings.xml (Romanian) 2023-09-12 06:50:54 +02:00
Grishka
6d84f28600 Hashtag following
closes #684, closes #233
2023-09-12 07:49:14 +03:00
Grishka
209e603f2c oops 2023-09-12 06:05:45 +03:00
Grishka
1b4dc01c74 Post translation
closes #267, closes #671, closes #502
2023-09-12 06:00:40 +03:00
Andrewblasco
6aab8f6578 Translated using Weblate (Spanish)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-09-12 02:43:45 +00:00
Grishka
645af12c3f Merge branch 'l10n_master' 2023-09-12 02:32:38 +03:00
Grishka
fadc42d72b New version 2023-09-12 02:32:23 +03:00
Eugen Rochko
fc831e7d42 New translations strings.xml (Portuguese, Brazilian) 2023-09-11 22:36:20 +02:00
Eugen Rochko
2998ee9145 New translations strings.xml (Finnish) 2023-09-11 20:09:41 +02:00
Eugen Rochko
971c4e5879 New translations strings.xml (Finnish) 2023-09-11 19:00:28 +02:00
Linerly
48c53ee88b Translated using Weblate (Indonesian)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2023-09-11 14:53:07 +00:00
Linerly
acf1fa15da Translated using Weblate (Indonesian)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-09-11 14:53:07 +00:00
poesty
1c3b28f9d7 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (385 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-09-11 14:53:07 +00:00
Eugen Rochko
b396ee7987 New translations strings.xml (Indonesian) 2023-09-10 15:07:27 +02:00
butterflyoffire
90856a414a Translated using Weblate (Arabic)
Currently translated at 16.6% (3 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ar/
2023-09-09 20:53:07 +00:00
butterflyoffire
ea19925be6 Translated using Weblate (Arabic)
Currently translated at 69.1% (267 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-09 20:53:07 +00:00
ihor_ck
03b3775843 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-09-09 20:53:07 +00:00
edxkl
38b39751ae Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.1% (375 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-09-09 20:53:07 +00:00
Oliebol
54a4b0fe41 Translated using Weblate (Dutch)
Currently translated at 81.8% (316 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-09-09 20:53:07 +00:00
Choukajohn
3bf591c944 Translated using Weblate (French)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-09-09 20:53:06 +00:00
gallegonovato
584a6bbfa3 Translated using Weblate (Spanish)
Currently translated at 100.0% (386 of 386 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-09-09 20:53:06 +00:00
Eugen Rochko
0f803cd4fa New translations strings.xml (Finnish) 2023-09-09 20:30:25 +02:00
Eugen Rochko
167a14b8db New translations strings.xml (Finnish) 2023-09-09 19:29:29 +02:00
Eugen Rochko
81cbc2d10c New translations strings.xml (Ukrainian) 2023-09-09 15:40:28 +02:00
Eugen Rochko
9bd8aff99b New translations strings.xml (Finnish) 2023-09-09 12:08:15 +02:00
Eugen Rochko
a770828165 New translations strings.xml (Finnish) 2023-09-09 11:09:40 +02:00
Eugen Rochko
ab457035ff New translations strings.xml (Finnish) 2023-09-09 08:34:27 +02:00
Grishka
f886e4c1d2 Fix #658, fix #620 2023-09-09 03:39:27 +03:00
sk
380e4ff77e remove unused member 2023-09-09 01:48:49 +02:00
sk
58f0c07357 determine next display item using items list
closes sk22#815
2023-09-09 01:27:32 +02:00
sk
77dee59b9c fix string and tweak banner margin
closes sk22#812
2023-09-09 01:24:36 +02:00
edxkl
464dc93d99 Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.8% (368 of 384 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-09-08 20:22:51 +00:00
gallegonovato
dcdfd3e5d3 Translated using Weblate (Spanish)
Currently translated at 100.0% (384 of 384 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-09-08 20:22:51 +00:00
sk
646f83ff0a boop verison 2023-09-08 22:19:41 +02:00
sk
fdf0414698 fix missing banner icons and wrong strings
closes sk22#805
2023-09-08 22:05:06 +02:00
sk
cc699a3f5e fix spacing stuff in report fragment 2023-09-08 21:58:08 +02:00
LucasGGamerM
12eaa8d5f1 fix: fix window insets on ScheduledStatusListFragment
cc: @sk22
2023-09-08 21:49:29 +02:00
sk22
70680e39c6 Translated using Weblate (German)
Currently translated at 100.0% (384 of 384 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-09-08 19:46:45 +00:00
sk
8bd8f90d58 remove unused strings 2023-09-08 21:44:31 +02:00
edxkl
54b53a266e Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.2% (362 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-09-08 19:40:59 +00:00
sk22
66921e3b5a Translated using Weblate (German)
Currently translated at 97.1% (377 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-09-08 19:40:59 +00:00
sk22
9d7af3964b Translated using Weblate (English)
Currently translated at 100.0% (388 of 388 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/en/
2023-09-08 19:40:59 +00:00
ihor_ck
ec73687e9b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (382 of 382 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-09-08 19:17:03 +00:00
Choukajohn
c8af800b88 Translated using Weblate (French)
Currently translated at 100.0% (382 of 382 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-09-08 19:17:02 +00:00
gallegonovato
e74ac5da56 Translated using Weblate (Spanish)
Currently translated at 99.4% (380 of 382 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-09-08 19:17:02 +00:00
sk
efa003a9a5 implement bidirectional missing posts gap 2023-09-08 21:16:42 +02:00
butterflyoffire
de5165434d Translated using Weblate (Arabic)
Currently translated at 16.6% (3 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ar/
2023-09-08 15:43:13 +00:00
sk
be648cc5ab Merge remote-tracking branch 'upstream/l10n_master' 2023-09-08 17:42:47 +02:00
sk
90f1f464dc reset strings.xml to upstream 2023-09-08 17:41:30 +02:00
sk
734aa52816 Merge remote-tracking branch 'weblate/main' 2023-09-08 17:37:31 +02:00
sk
068d42175e Merge remote-tracking branch 'upstream/master' 2023-09-08 17:37:00 +02:00
sk
2314871246 temporary fix for pre-release users 2023-09-08 17:36:17 +02:00
butterflyoffire
e4f13c900b Translated using Weblate (Arabic)
Currently translated at 67.5% (250 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-07 19:23:10 +00:00
ppnplus
4ddfa483d4 Translated using Weblate (Thai)
Currently translated at 23.5% (87 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/th/
2023-09-07 19:23:10 +00:00
butterflyoffire
20f41ce7c9 Translated using Weblate (Kabyle)
Currently translated at 0.5% (2 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/kab/
2023-09-07 19:23:10 +00:00
Arkxv
c8df9e085e Translated using Weblate (Japanese)
Currently translated at 78.3% (290 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ja/
2023-09-07 19:23:10 +00:00
GunChleoc
0238aa4375 Translated using Weblate (Gaelic)
Currently translated at 84.3% (312 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gd/
2023-09-07 19:23:09 +00:00
butterflyoffire
79d7873790 Translated using Weblate (French)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-09-07 19:23:09 +00:00
kallekn
996842489d Translated using Weblate (Finnish)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fi/
2023-09-07 19:23:09 +00:00
Andrewblasco
23c2c2b5e7 Translated using Weblate (Spanish)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-09-07 19:23:08 +00:00
sk
9dd694ce2e Merge branch 'merge/load-more-direction' 2023-09-07 18:32:21 +02:00
FineFindus
f51f2a1197 fix: use random account id in notification endpoint (#806)
Closes https://github.com/sk22/megalodon/issues/803. This was due to registering with the actual accounId, whilst saving a random one. Once receiving a notification, the id could not be found.
2023-09-07 18:28:32 +02:00
LucasGGamerM
3020cab243 fix: fixes crashes when currentQuery is null and suicide prevention dialog is enabled (#807)
cc: @FineFindus
2023-09-07 10:03:22 +02:00
Eugen Rochko
be73c9e81c New translations strings.xml (Basque) 2023-09-07 02:28:42 +02:00
Eugen Rochko
1c2183bf1a New translations strings.xml (Slovenian) 2023-09-06 22:29:03 +02:00
Gregory K
1789d90dc3 Merge pull request #685 from LucasGGamerM/mastodon-android
fix(editing-alt-text): fix small oversight on editing existing attachments without alt text
2023-09-06 01:42:41 +03:00
LucasGGamerM
57306ff7fe fix(editing-alt-text): fix small oversight on editing existing attachments without alt text
This makes the implementation hopefully bug free
2023-09-05 19:37:12 -03:00
sk
2aba90f353 add media indicator for spoiler
closes sk22#598
2023-09-05 00:18:47 +02:00
sk
5065c7e7e2 add dummy to add spacing
closes sk22#782
2023-09-04 23:42:00 +02:00
sk
a10e661b21 fix wrong info banner color
closes sk22#790
2023-09-04 23:37:34 +02:00
sk
4975bde76f fix tab layout horizontal paddings 2023-09-04 23:33:35 +02:00
FineFindus
ebbd56e3bc feat(search): show suicide help dialog (#767)
* feat(search): show suicide help dialog

* change wording, add helpline url, change behavior

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-09-04 23:07:24 +02:00
Jacoco
454ec6b4c0 Fix errors when some entries were missing when retrieving account information (#757)
* Handle null avatar

* Handle empty account
2023-09-04 22:24:17 +02:00
FineFindus
10a8b195b1 fix: move tabbar padding to tabbar items (#792)
* feat: increase padding without labels

* fix: move tabbar padding to individual items
2023-09-04 22:19:47 +02:00
sk
6f273df060 use single-line linear layout 2023-09-04 19:54:07 +02:00
sk
7cfade62d3 fix click listener target 2023-09-04 19:53:33 +02:00
starstuff
0a338ad607 Translated using Weblate (Swedish)
Currently translated at 41.6% (154 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sv/
2023-09-04 08:05:19 +00:00
Arkxv
0cb3e1863e Translated using Weblate (Japanese)
Currently translated at 5.4% (20 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ja/
2023-09-04 08:05:19 +00:00
kallekn
209081f1f0 Translated using Weblate (Finnish)
Currently translated at 0.8% (3 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fi/
2023-09-04 08:05:19 +00:00
Eugen Rochko
f0eb6573f4 New translations strings.xml (Portuguese, Brazilian) 2023-09-04 08:10:24 +02:00
Eugen Rochko
e7f5dd3357 New translations strings.xml (Bengali) 2023-09-04 07:10:06 +02:00
Eugen Rochko
8101bb9ea1 New translations strings.xml (Portuguese, Brazilian) 2023-09-04 07:10:05 +02:00
butterflyoffire
54d48253d5 Translated using Weblate (Arabic)
Currently translated at 12.4% (46 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-09-04 00:53:06 +00:00
ca
3373b2bb04 Translated using Weblate (Catalan)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ca/
2023-09-04 00:53:06 +00:00
ca
a7e23aa228 Translated using Weblate (Catalan)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ca/
2023-09-04 00:53:06 +00:00
Eugen Rochko
228fdc8ffe New translations strings.xml (Swedish) 2023-09-03 11:26:37 +02:00
Angelo Suzuki
2d24e50ff2 Add a setting for "Load missing posts behavior"
This will make sure that items that are filtered out don't show up on the interface.
Fixes sk22#311
2023-09-01 21:51:14 +02:00
Eugen Rochko
e9df125cde New translations strings.xml (Swedish) 2023-09-01 16:10:32 +02:00
Eugen Rochko
d76e823489 New translations strings.xml (Italian) 2023-08-29 11:52:38 +02:00
232 changed files with 5762 additions and 1190 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 98
versionName "2.0.3+fork.98"
versionCode 100
versionName "2.1.6+fork.100"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
}
@@ -70,7 +70,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.9'
implementation 'me.grishka.appkit:appkit:1.2.14'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -32,7 +32,6 @@ public class ExternalShareActivity extends FragmentStackActivity{
UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);

View File

@@ -25,6 +25,8 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import androidx.annotation.StringRes;
public class GlobalUserPreferences{
private static final String TAG="GlobalUserPreferences";
@@ -55,11 +57,11 @@ public class GlobalUserPreferences{
public static boolean allowRemoteLoading;
public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers;
public static ColorPreference color;
public static boolean disableM3PillActiveIndicator;
public static boolean showNavigationLabels;
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
public static boolean overlayMedia;
public static boolean showSuicideHelp;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
@@ -119,6 +121,7 @@ public class GlobalUserPreferences{
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
overlayMedia=prefs.getBoolean("overlayMedia", false);
showSuicideHelp=prefs.getBoolean("showSuicideHelp", true);
if (prefs.contains("prefixRepliesWithRe")) {
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
@@ -129,14 +132,8 @@ public class GlobalUserPreferences{
.apply();
}
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
} catch (IllegalArgumentException|ClassCastException ignored) {
// invalid color name or color was previously saved as integer
color=ColorPreference.PINK;
}
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
if(prefs.getInt("migrationLevel", 0) < 101) migrateToVersion101();
}
public static void save(){
@@ -167,7 +164,6 @@ public class GlobalUserPreferences{
.putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab)
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
.putString("color", color.name())
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault)
@@ -177,9 +173,20 @@ public class GlobalUserPreferences{
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
.putBoolean("overlayMedia", overlayMedia)
.putBoolean("showSuicideHelp", showSuicideHelp)
.apply();
}
private static void migrateToVersion101(){
Log.d(TAG, "Migrating preferences to version 101!! (copy current theme to local preferences)");
AccountSessionManager asm=AccountSessionManager.getInstance();
for(AccountSession session : asm.getLoggedInAccounts()){
String accountID=session.getID();
AccountLocalPreferences localPrefs=session.getLocalPreferences();
}
}
private static void migrateToUpstreamVersion61(){
Log.d(TAG, "Migrating preferences to upstream version 61!!");
@@ -230,30 +237,6 @@ public class GlobalUserPreferences{
prefs.edit().putInt("migrationLevel", 61).apply();
}
public enum ColorPreference{
MATERIAL3,
PINK,
PURPLE,
GREEN,
BLUE,
BROWN,
RED,
YELLOW;
public @StringRes int getName() {
return switch(this){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
case GREEN -> R.string.sk_color_palette_green;
case BLUE -> R.string.sk_color_palette_blue;
case BROWN -> R.string.sk_color_palette_brown;
case RED -> R.string.sk_color_palette_red;
case YELLOW -> R.string.sk_color_palette_yellow;
};
}
}
public enum ThemePreference{
AUTO,
LIGHT,

View File

@@ -39,54 +39,12 @@ import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
AccountSession session=getCurrentSession();
UiUtils.setUserPreferredTheme(this, session);
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
AccountSessionManager.getInstance()
.setLastActiveAccountID(intent.getStringExtra("account"));
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
AccountSessionManager.getInstance().getLastActiveAccount());
showFragmentForExternalShare(intent.getExtras());
return;
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
String accountID=intent.getStringExtra("accountID");
try{
session=AccountSessionManager.getInstance().getAccount(accountID);
if(!hasNotification) args.putString("tab", "notifications");
}catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
if(fromNotification && hasNotification){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
} else if (intent.getBooleanExtra("compose", false)){
showCompose();
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
} else {
showFragmentClearingBackStack(fragment);
maybeRequestNotificationsPermission();
}
}
restartHomeFragment();
}
if(GithubSelfUpdater.needSelfUpdating()){
@@ -143,7 +101,7 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
}
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true)
new GetSearchResults(q, null, true, null, 0, 0)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
@@ -259,4 +217,81 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
Fragment fragment = getCurrentFragment();
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
}
public AccountSession getCurrentSession(){
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
return AccountSessionManager.getInstance()
.getAccount(intent.getStringExtra("account"));
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
String accountID=intent.getStringExtra("accountID");
try{
session=AccountSessionManager.getInstance().getAccount(accountID);
if(!hasNotification) args.putString("tab", "notifications");
}catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
return session;
}
public void restartActivity(){
finish();
startActivity(new Intent(this, MainActivity.class));
}
public void restartHomeFragment(){
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
AccountSessionManager.getInstance()
.setLastActiveAccountID(intent.getStringExtra("account"));
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
AccountSessionManager.getInstance().getLastActiveAccount());
showFragmentForExternalShare(intent.getExtras());
return;
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
String accountID=intent.getStringExtra("accountID");
try{
session=AccountSessionManager.getInstance().getAccount(accountID);
if(!hasNotification) args.putString("tab", "notifications");
}catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
if(fromNotification && hasNotification){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
} else if (intent.getBooleanExtra("compose", false)){
showCompose();
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
} else {
showFragmentClearingBackStack(fragment);
maybeRequestNotificationsPermission();
}
}
}
}

View File

@@ -176,6 +176,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
.map(type->{
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
channel.setLightColor(context.getColor(R.color.primary_700));
channel.enableLights(true);
channel.setGroup(accountID);
return channel;
})
@@ -205,6 +207,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
.setLights(UiUtils.getThemeColor(context, android.R.attr.colorAccent), 500, 1000)
.setColor(UiUtils.getThemeColor(context, android.R.attr.colorAccent));
if (!GlobalUserPreferences.uniformNotificationIcon) {
@@ -321,8 +324,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
CreateStatus.Request req=new CreateStatus.Request();
req.status = initialText + input.toString();
req.language = preferences.postingDefaultLanguage;
req.visibility = preferences.postingDefaultVisibility;
req.language = notification.status.language;
req.visibility = notification.status.visibility;
req.inReplyToId = notification.status.id;
if (notification.status.hasSpoiler() &&

View File

@@ -74,7 +74,6 @@ public class CacheController{
result.add(status);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
return;
}
@@ -86,9 +85,7 @@ public class CacheController{
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
ArrayList<Status> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
callback.onSuccess(new CacheablePaginatedResponse<>(result, result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null);
}

View File

@@ -97,7 +97,7 @@ public class PushSubscriptionManager{
deviceToken=getPrefs().getString("deviceToken", null);
int tokenVersion=getPrefs().getInt("version", 0);
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE){
registerAllAccountsForPush(false);
registerAllAccountsForPush(true); // TODO: revert this before release
return;
}
Log.i(TAG, "tryRegisterFCM: no token found or app was updated. Trying to get push token...");
@@ -130,12 +130,11 @@ public class PushSubscriptionManager{
return;
}
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/";
registerAccountForPush(subscription, endpoint);
}
public void registerAccountForPush(PushSubscription subscription, String endpoint){
MastodonAPIController.runInBackground(()->{
Log.d(TAG, "registerAccountForPush: started for "+accountID);
String encodedPublicKey, encodedAuthKey, pushAccountID;
@@ -164,7 +163,13 @@ public class PushSubscriptionManager{
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
return;
}
new RegisterForPushNotifications(endpoint,
//work-around for adding the randomAccountId
String newEndpoint = endpoint;
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/"))
newEndpoint += pushAccountID;
new RegisterForPushNotifications(newEndpoint,
encodedPublicKey,
encodedAuthKey,
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,

View File

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

View File

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

View File

@@ -6,18 +6,19 @@ import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.StatusPrivacy;
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable, Boolean indexable){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
setRequestBody(new Request(locked, discoverable, indexable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
}
private static class Request{
public Boolean locked, discoverable;
public Boolean locked, discoverable, indexable;
public RequestSource source;
public Request(Boolean locked, Boolean discoverable, RequestSource source){
public Request(Boolean locked, Boolean discoverable, Boolean indexable, RequestSource source){
this.locked=locked;
this.discoverable=discoverable;
this.indexable=indexable;
this.source=source;
}
}

View File

@@ -6,6 +6,9 @@ import org.joinmastodon.android.model.FilterContext;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.Keep;
@Keep
class FilterRequest{
public String title;
public EnumSet<FilterContext> context;

View File

@@ -4,13 +4,19 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.SearchResults;
public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
public GetSearchResults(String query, Type type, boolean resolve){
public GetSearchResults(String query, Type type, boolean resolve, String maxID, int offset, int count){
super(HttpMethod.GET, "/search", SearchResults.class);
addQueryParameter("q", query);
if(type!=null)
addQueryParameter("type", type.name().toLowerCase());
if(resolve)
addQueryParameter("resolve", "true");
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(offset>0)
addQueryParameter("offset", String.valueOf(offset));
if(count>0)
addQueryParameter("limit", String.valueOf(count));
}
public GetSearchResults limit(int limit){

View File

@@ -1,11 +1,13 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.TranslatedStatus;
import org.joinmastodon.android.model.Translation;
public class TranslateStatus extends MastodonAPIRequest<TranslatedStatus> {
public TranslateStatus(String id) {
super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class);
setRequestBody(new Object());
}
import java.util.Map;
public class TranslateStatus extends MastodonAPIRequest<Translation>{
public TranslateStatus(String id, String lang){
super(HttpMethod.POST, "/statuses/"+id+"/translate", Translation.class);
setRequestBody(Map.of("lang", lang));
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.tags;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
public class GetTag extends MastodonAPIRequest<Hashtag>{
public GetTag(String tag){
super(HttpMethod.GET, "/tags/"+tag, Hashtag.class);
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.tags;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
public class SetTagFollowed extends MastodonAPIRequest<Hashtag>{
public SetTagFollowed(String tag, boolean followed){
super(HttpMethod.POST, "/tags/"+tag+(followed ? "/follow" : "/unfollow"), Hashtag.class);
setRequestBody(new Object());
}
}

View File

@@ -6,9 +6,13 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
import android.content.SharedPreferences;
import androidx.annotation.StringRes;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
@@ -36,12 +40,15 @@ public class AccountLocalPreferences{
public String publishButtonText;
public String timelineReplyVisibility; // akkoma-only
public boolean keepOnlyLatestNotification;
public boolean emojiReactionsEnabled;
public ShowEmojiReactions showEmojiReactions;
public ColorPreference color;
public boolean likeIcon;
public ArrayList<Emoji> recentCustomEmoji;
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
private final static Type timelinesType=new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
private final static Type recentCustomEmojiType=new TypeToken<ArrayList<Emoji>>() {}.getType();
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
this.prefs=prefs;
@@ -66,6 +73,9 @@ public class AccountLocalPreferences{
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name()));
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.MATERIAL3.name()));
likeIcon=prefs.getBoolean("likeIcon", false);
recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>());
}
public long getNotificationsPauseEndTime(){
@@ -99,9 +109,36 @@ public class AccountLocalPreferences{
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
.putString("showEmojiReactions", showEmojiReactions.name())
.putString("color", color.name())
.putBoolean("likeIcon", likeIcon)
.putString("recentCustomEmoji", gson.toJson(recentCustomEmoji))
.apply();
}
public enum ColorPreference{
MATERIAL3,
PINK,
PURPLE,
GREEN,
BLUE,
BROWN,
RED,
YELLOW;
public @StringRes int getName() {
return switch(this){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
case GREEN -> R.string.sk_color_palette_green;
case BLUE -> R.string.sk_color_palette_blue;
case BROWN -> R.string.sk_color_palette_brown;
case RED -> R.string.sk_color_palette_red;
case YELLOW -> R.string.sk_color_palette_yellow;
};
}
}
public enum ShowEmojiReactions{
HIDE_EMPTY,
ONLY_OPENED,

View File

@@ -40,7 +40,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -219,7 +218,7 @@ public class AccountSession{
public void savePreferencesIfPending(){
if(preferencesNeedSaving){
new UpdateAccountCredentialsPreferences(preferences, null, null)
new UpdateAccountCredentialsPreferences(preferences, null, self.discoverable, self.source.indexable)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
@@ -255,52 +254,66 @@ public class AccountSession{
filterStatusContainingObjects(objects, extractor, context, null);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
private boolean statusIsOnOwnProfile(Status s, Account profile){
return self != null && profile != null && s.account != null
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
}
if(getLocalPreferences().serverSideFiltersSupported){
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
if(s.filtered==null)
return false;
// don't hide own posts in own profile
if (statusIsOnOwnProfile.test(s))
return false;
for(FilterResult filter:s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true;
}
return false;
});
return;
}
if(wordFilters==null)
return;
for(T obj:objects){
private boolean isFilteredType(Status s){
return (!localPreferences.showReplies && s.inReplyToId != null)
|| (!localPreferences.showBoosts && s.reblog != null);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
Status s=extractor.apply(obj);
if(s!=null && s.filtered!=null){
getLocalPreferences().serverSideFiltersSupported=true;
getLocalPreferences().save();
return;
localPreferences.serverSideFiltersSupported=true;
localPreferences.save();
}
}
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
// don't hide own posts in own profile
if (statusIsOnOwnProfile.test(s))
return false;
for(LegacyFilter filter:wordFilters){
List<T> removeUs=new ArrayList<>();
for(int i=0; i<objects.size(); i++){
T o=objects.get(i);
if(filterStatusContainingObject(o, extractor, context, profile)){
Status s=extractor.apply(o);
removeUs.add(o);
if(s!=null && s.hasGapAfter && i > 0){
Status prev=extractor.apply(objects.get(i - 1));
if(prev!=null) prev.hasGapAfter=true;
}
}
}
objects.removeAll(removeUs);
}
public <T> boolean filterStatusContainingObject(T object, Function<T, Status> extractor, FilterContext context, Account profile){
Status s=extractor.apply(object);
if(s==null)
return false;
// don't hide own posts in own profile
if(statusIsOnOwnProfile(s, profile))
return false;
if(isFilteredType(s))
return true;
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
if(localPreferences.serverSideFiltersSupported){
for(FilterResult filter : s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true;
}
}else if(wordFilters!=null){
for(LegacyFilter filter : wordFilters){
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
return true;
}
return false;
});
}
return false;
}
public void updateAccountInfo(){
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
}
public Optional<Instance> getInstance() {
@@ -313,4 +326,10 @@ public class AccountSession{
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
.build();
}
public String getDefaultAvatarUrl() {
return getInstance()
.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
.orElse("");
}
}

View File

@@ -303,8 +303,7 @@ public class AccountSessionManager{
}
}
private void updateSessionLocalInfo(AccountSession session){
/*package*/ void updateSessionLocalInfo(AccountSession session){
new GetOwnAccount()
.setCallback(new Callback<>(){
@Override

View File

@@ -9,19 +9,16 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
@@ -60,7 +57,6 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
AccountSessionManager asm = AccountSessionManager.getInstance();
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
onDataLoaded(result, !empty);

View File

@@ -9,7 +9,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -22,6 +21,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
@@ -29,7 +29,9 @@ import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
@@ -55,6 +57,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -555,12 +558,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
// to do this if the media grid is not bound, tho - so, doing it ourselves here
status.sensitiveRevealed = !status.sensitiveRevealed;
}
holder.rebind();
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
}
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(header != null) header.rebind();
if(header != null && header.getItem().hasVisibilityToggle) header.animateVisibilityToggle(true);
}
protected void toggleSpoiler(Status status, String itemID){
@@ -617,7 +620,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
int startPos = warning.getAbsoluteAdapterPosition();
@@ -772,6 +775,61 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
public void togglePostTranslation(Status status, String itemID){
switch(status.translationState){
case LOADING -> {
return;
}
case SHOWN -> {
status.translationState=Status.TranslationState.HIDDEN;
}
case HIDDEN -> {
if(status.translation!=null){
status.translationState=Status.TranslationState.SHOWN;
}else{
status.translationState=Status.TranslationState.LOADING;
new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Translation result){
if(getActivity()==null)
return;
status.translation=result;
status.translationState=Status.TranslationState.SHOWN;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
}
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
status.translationState=Status.TranslationState.HIDDEN;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
}
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.translation_failed)
.setPositiveButton(R.string.ok, null)
.show();
}
})
.exec(accountID);
}
}
}
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
}
}
public void rebuildAllDisplayItems(){
displayItems.clear();
for(T item:data){
@@ -787,7 +845,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(getContext()==null) return;
super.onDataLoaded(d, more);
// more available, but the page isn't even full yet? seems wrong, let's load some more
if(more && d.size() < itemsPerPage){
if(more && data.size() < itemsPerPage){
preloader.onScrolledToLastItem();
}
}

View File

@@ -29,6 +29,7 @@ import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -161,7 +162,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private int charCount, charLimit, trimmedCharCount;
private Button publishButton, languageButton, scheduleTimeBtn;
private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
private PopupMenu contentTypePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, draftsBtn, scheduleDraftDismiss, contentTypeBtn;
private View sensitiveBtn;
private TextView replyText;
@@ -293,7 +294,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
creatingView=true;
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain);
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), accountID, customEmojis, instanceDomain);
emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){
@Override
public void onEmojiSelected(Emoji emoji){
@@ -826,6 +827,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
publishButton = wrap.findViewById(R.id.publish_btn);
languageButton = wrap.findViewById(R.id.language_btn);
languageButton.setOnClickListener(v->showLanguageAlert());
languageButton.setOnLongClickListener(v->{
languageButton.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if(!getLocalPrefs().bottomEncoding){
getLocalPrefs().bottomEncoding=true;
getLocalPrefs().save();
}
return false;
});
publishButton.setOnClickListener(v -> {
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
@@ -1308,7 +1317,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
boolean usePhotoPicker=photoPicker && UiUtils.isPhotoPickerAvailable();
if(usePhotoPicker){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount());
if(mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount()>1)
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount());
}else{
intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);

View File

@@ -26,12 +26,12 @@ import android.widget.ImageView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.utils.ColorPalette;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import java.util.Collections;
@@ -54,16 +54,16 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
attachmentID=getArguments().getString("attachment");
setHasOptionsMenu(true);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
accountID=getArguments().getString("account");
attachmentID=getArguments().getString("attachment");
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
ColorPalette.palettes.get(GlobalUserPreferences.color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
ColorPalette.palettes.get(AccountSessionManager.get(accountID).getLocalPreferences().color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
setTitle(R.string.add_alt_text);
}

View File

@@ -239,16 +239,21 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
if (tags == null || tags.isEmpty()) return false;
editText.setText(String.join(",", tags));
editText.chipifyAllUnterminatedTokens();
editText.setText(tags);
editText.chipifyAllUnterminatedTokens();
return true;
}
private NachoTextView prepareChipTextView(NachoTextView nacho) {
nacho.addChipTerminator(',', BEHAVIOR_CHIPIFY_ALL);
nacho.addChipTerminator('\n', BEHAVIOR_CHIPIFY_ALL);
nacho.addChipTerminator(' ', BEHAVIOR_CHIPIFY_ALL);
nacho.addChipTerminator(';', BEHAVIOR_CHIPIFY_ALL);
//Ill Be Back
nacho.setChipTerminators(
Map.of(
',', BEHAVIOR_CHIPIFY_ALL,
'\n', BEHAVIOR_CHIPIFY_ALL,
' ', BEHAVIOR_CHIPIFY_ALL,
';', BEHAVIOR_CHIPIFY_ALL
)
);
nacho.enableEditChipOnTouch(true, true);
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
return nacho;

View File

@@ -45,8 +45,8 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
}
@Override
public void onItemClick(String hashtag){
UiUtils.openHashtagTimeline(getActivity(), accountID, hashtag, data.stream().filter(h -> Objects.equals(h.name, hashtag)).findAny().map(h -> h.following).orElse(null));
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
}
@Override

View File

@@ -17,8 +17,10 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -357,8 +359,9 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
public AccountWrapper(Account account){
this.account=account;
if(!TextUtils.isEmpty(account.avatar))
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? AccountSessionManager.get(getAccountID()).getDefaultAvatarUrl() : account.avatar,
V.dp(50), V.dp(50));
if(!TextUtils.isEmpty(account.header))
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);

View File

@@ -121,7 +121,7 @@ public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag>
@Override
public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
}
}
}

View File

@@ -1,46 +1,65 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetHashtag;
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.tags.GetTag;
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.parceler.Parcels;
import java.util.List;
import java.util.stream.Collectors;
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;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class HashtagTimelineFragment extends PinnableStatusListFragment {
private String hashtag;
public class HashtagTimelineFragment extends PinnableStatusListFragment{
private Hashtag hashtag;
private String hashtagName;
private TextView headerTitle, headerSubtitle;
private ProgressBarButton followButton;
private ProgressBar followProgress;
private MenuItem followMenuItem, pinMenuItem;
private boolean followRequestRunning;
private boolean toolbarContentVisible;
private List<String> any;
private List<String> all;
private List<String> none;
private boolean following;
private boolean localOnly;
private MenuItem followButton;
private Menu optionsMenu;
private MenuInflater optionsMenuInflater;
@Override
protected boolean wantsComposeButton() {
@@ -50,89 +69,36 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
updateTitle(getArguments().getString("hashtag"));
following=getArguments().getBoolean("following", false);
localOnly=getArguments().getBoolean("localOnly", false);
any=getArguments().getStringArrayList("any");
all=getArguments().getStringArrayList("all");
none=getArguments().getStringArrayList("none");
setHasOptionsMenu(true);
}
private void updateTitle(String hashtagName) {
hashtag = hashtagName;
setTitle('#'+hashtag);
}
private void updateFollowingState(boolean newFollowing) {
this.following = newFollowing;
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
E.post(new HashtagUpdatedEvent(hashtag, following));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.hashtag_timeline, menu);
super.onCreateOptionsMenu(menu, inflater);
followButton = menu.findItem(R.id.follow_hashtag);
updateFollowingState(following);
new GetHashtag(hashtag).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag hashtag) {
if (getActivity() == null) return;
updateTitle(hashtag.name);
updateFollowingState(hashtag.following);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (super.onOptionsItemSelected(item)) return true;
if (item.getItemId() == R.id.follow_hashtag) {
updateFollowingState(!following);
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag i) {
if (getActivity() == null) return;
if (i.following == following) Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
updateFollowingState(i.following);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
updateFollowingState(!following);
}
}).exec(accountID);
return true;
if(getArguments().containsKey("hashtag")){
hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag"));
hashtagName=hashtag.name;
}else{
hashtagName=getArguments().getString("hashtagName");
}
return false;
setTitle('#'+hashtagName);
setHasOptionsMenu(true);
}
@Override
protected TimelineDefinition makeTimelineDefinition() {
return TimelineDefinition.ofHashtag(hashtag);
return TimelineDefinition.ofHashtag(hashtagName);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
if(getActivity()==null) return;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
}
})
.exec(accountID);
@@ -146,15 +112,40 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
}
@Override
public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
public void loadData(){
reloadTag();
super.loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
if(getParentFragment() instanceof HomeTabFragment) return;
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
View topChild=recyclerView.getChildAt(0);
int firstChildPos=recyclerView.getChildAdapterPosition(topChild);
float newAlpha=firstChildPos>0 ? 1f : Math.min(1f, -topChild.getTop()/(float)headerTitle.getHeight());
toolbarTitleView.setAlpha(newAlpha);
boolean newToolbarVisibility=newAlpha>0.5f;
if(newToolbarVisibility!=toolbarContentVisible){
toolbarContentVisible=newToolbarVisibility;
createOptionsMenu();
}
}
});
}
@Override
public void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", '#'+hashtag+' ');
args.putString("prefilledText", '#'+hashtagName+' ');
Nav.go(getActivity(), ComposeFragment.class, args);
}
@@ -170,6 +161,202 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtag).build();
}
@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.header_hashtag_timeline, list, false);
headerTitle=header.findViewById(R.id.title);
headerSubtitle=header.findViewById(R.id.subtitle);
followButton=header.findViewById(R.id.profile_action_btn);
followProgress=header.findViewById(R.id.action_progress);
headerTitle.setText("#"+hashtagName);
followButton.setVisibility(View.GONE);
followButton.setOnClickListener(v->{
if(hashtag==null)
return;
setFollowed(!hashtag.following);
});
followButton.setOnLongClickListener(v->{
if(hashtag==null) return false;
UiUtils.pickAccount(getActivity(), accountID, R.string.sk_follow_as, R.drawable.ic_fluent_person_add_28_regular, session -> {
new SetTagFollowed(hashtagName, true).setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag hashtag) {
Toast.makeText(
getActivity(),
getString(R.string.sk_followed_as, session.self.getShortUsername()),
Toast.LENGTH_SHORT
).show();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(session.getID());
}, null);
return true;
});
updateHeader();
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
if(!(getParentFragment() instanceof HomeTabFragment)){
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(header));
}
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
protected int getMainAdapterOffset(){
return 1;
}
private void createOptionsMenu(){
optionsMenu.clear();
optionsMenuInflater.inflate(R.menu.hashtag_timeline, optionsMenu);
followMenuItem=optionsMenu.findItem(R.id.follow_hashtag);
pinMenuItem=optionsMenu.findItem(R.id.pin);
followMenuItem.setVisible(toolbarContentVisible);
pinMenuItem.setShowAsAction(toolbarContentVisible ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_ALWAYS);
super.updatePinButton(pinMenuItem);
if(toolbarContentVisible){
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu);
}else{
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.pin);
}
}
@Override
public void updatePinButton(MenuItem pin){
super.updatePinButton(pin);
if(toolbarContentVisible) UiUtils.insetPopupMenuIcon(getContext(), pin);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.hashtag_timeline, menu);
super.onCreateOptionsMenu(menu, inflater);
optionsMenu=menu;
optionsMenuInflater=inflater;
createOptionsMenu();
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if (super.onOptionsItemSelected(item)) return true;
if (item.getItemId() == R.id.follow_hashtag && hashtag!=null) {
setFollowed(!hashtag.following);
}
return true;
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f);
createOptionsMenu();
}
private void updateHeader(){
if(hashtag==null || getActivity()==null)
return;
if(hashtag.history!=null && !hashtag.history.isEmpty()){
int weekPosts=hashtag.history.stream().mapToInt(h->h.uses).sum();
int todayPosts=hashtag.history.get(0).uses;
int numAccounts=hashtag.history.stream().mapToInt(h->h.accounts).sum();
int hSpace=V.dp(8);
SpannableStringBuilder ssb=new SpannableStringBuilder();
ssb.append(getResources().getQuantityString(R.plurals.x_posts, weekPosts, weekPosts));
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append('·');
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append(getResources().getQuantityString(R.plurals.x_participants, numAccounts, numAccounts));
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append('·');
ssb.append(" ", new SpacerSpan(hSpace, 0), 0);
ssb.append(getResources().getQuantityString(R.plurals.x_posts_today, todayPosts, todayPosts));
headerSubtitle.setText(ssb);
}
int styleRes;
followButton.setVisibility(View.VISIBLE);
if(hashtag.following){
followButton.setText(R.string.button_following);
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
}else{
followButton.setText(R.string.button_follow);
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}
TypedArray ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
followButton.setBackground(ta.getDrawable(0));
ta.recycle();
ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
followButton.setTextColor(ta.getColorStateList(0));
followProgress.setIndeterminateTintList(ta.getColorStateList(0));
ta.recycle();
followButton.setTextVisible(true);
followProgress.setVisibility(View.GONE);
if(followMenuItem!=null){
followMenuItem.setTitle(getString(hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
followMenuItem.setIcon(hashtag.following ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), followMenuItem);
}
}
private void reloadTag(){
new GetTag(hashtagName)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag result){
hashtag=result;
updateHeader();
}
@Override
public void onError(ErrorResponse error){
}
})
.exec(accountID);
}
private void setFollowed(boolean followed){
if(followRequestRunning)
return;
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
followButton.setTextVisible(false);
followProgress.setVisibility(View.VISIBLE);
followRequestRunning=true;
new SetTagFollowed(hashtagName, followed)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag result){
if(getActivity()==null)
return;
hashtag=result;
updateHeader();
followRequestRunning=false;
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
if(error instanceof MastodonErrorResponse er && "Duplicate record".equals(er.error)){
hashtag.following=true;
}else{
error.showToast(getActivity());
}
updateHeader();
followRequestRunning=false;
}
})
.exec(accountID);
}
}

View File

@@ -184,6 +184,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
});
}
}
tabBar.selectTab(currentTab);
return content;
}

View File

@@ -524,9 +524,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
} else if ((hashtag = hashtagsItems.get(id)) != null) {
args.putString("hashtag", hashtag.name);
args.putBoolean("following", hashtag.following);
Nav.go(getActivity(), HashtagTimelineFragment.class, args);
UiUtils.openHashtagTimeline(getContext(), accountID, hashtag);
}
return true;
}

View File

@@ -11,27 +11,25 @@ import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class HomeTimelineFragment extends StatusListFragment {
private HomeTabFragment parent;
@@ -50,16 +48,6 @@ public class HomeTimelineFragment extends StatusListFragment {
loadData();
}
private boolean typeFilterPredicate(Status s) {
AccountLocalPreferences lp=getLocalPrefs();
return (lp.showReplies || s.inReplyToId == null) &&
(lp.showBoosts || s.reblog == null);
}
private List<Status> filterPosts(List<Status> items) {
return items.stream().filter(this::typeFilterPredicate).collect(Collectors.toList());
}
@Override
protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance()
@@ -67,10 +55,11 @@ public class HomeTimelineFragment extends StatusListFragment {
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
@Override
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
if (getActivity() == null) return;
List<Status> filteredItems = filterPosts(result.items);
if(getActivity()==null) return;
boolean empty=result.items.isEmpty();
maxID=result.maxID;
onDataLoaded(filteredItems, !result.items.isEmpty());
AccountSessionManager.get(accountID).filterStatuses(result.items, getFilterContext());
onDataLoaded(result.items, !empty);
if(result.isFromCache())
loadNewPosts();
}
@@ -143,7 +132,6 @@ public class HomeTimelineFragment extends StatusListFragment {
public void onSuccess(List<Status> result){
currentRequest=null;
dataLoading=false;
result = filterPosts(result);
if(result.isEmpty() || getActivity()==null)
return;
Status last=result.get(result.size()-1);
@@ -154,12 +142,14 @@ public class HomeTimelineFragment extends StatusListFragment {
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
List<Status> toAddUnfiltered=new ArrayList<>(toAdd);
AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
if(parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
}
if(toAddUnfiltered.isEmpty())
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAddUnfiltered, false);
}
@Override
@@ -176,15 +166,23 @@ public class HomeTimelineFragment extends StatusListFragment {
}
@Override
public void onGapClick(GapStatusDisplayItem.Holder item){
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){
if(dataLoading)
return;
item.getItem().loading=true;
V.setVisibilityAnimated(item.progress, View.VISIBLE);
V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem();
gap.loading=true;
dataLoading=true;
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null, getLocalPrefs().timelineReplyVisibility)
String maxID = null;
String minID = null;
if (downwards) {
maxID = item.getItemID();
} else {
int gapPos=displayItems.indexOf(gap);
StatusDisplayItem nextItem=displayItems.get(gapPos + 1);
minID=nextItem.parentID;
}
currentRequest=new GetHomeTimeline(maxID, minID, 20, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
@@ -204,52 +202,96 @@ public class HomeTimelineFragment extends StatusListFragment {
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
}
}else{
Set<String> idsBelowGap=new HashSet<>();
boolean belowGap=false;
int gapPostIndex=0;
for(Status s:data){
if(belowGap){
idsBelowGap.add(s.id);
}else if(s.id.equals(gap.parentID)){
belowGap=true;
s.hasGapAfter=false;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
}else{
gapPostIndex++;
if(downwards) {
Set<String> idsBelowGap=new HashSet<>();
boolean belowGap=false;
int gapPostIndex=0;
for(Status s:data){
if(belowGap){
idsBelowGap.add(s.id);
}else if(s.id.equals(gap.parentID)){
belowGap=true;
s.hasGapAfter=false;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
}else{
gapPostIndex++;
}
}
}
int endIndex=0;
for(Status s:result){
endIndex++;
if(idsBelowGap.contains(s.id))
break;
}
if(endIndex==result.size()){
result.get(result.size()-1).hasGapAfter=true;
}else{
result=result.subList(0, endIndex);
}
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
if(typeFilterPredicate(s) && filterPredicate.test(s)){
int endIndex=0;
for(Status s:result){
endIndex++;
if(idsBelowGap.contains(s.id))
break;
}
if(endIndex==result.size()){
result.get(result.size()-1).hasGapAfter=true;
}else{
result=result.subList(0, endIndex);
}
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
}else{
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
}
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
} else {
String aboveGapID = gap.parentID;
int gapPostIndex = 0;
for (;gapPostIndex<data.size();gapPostIndex++){
if (Objects.equals(aboveGapID, data.get(gapPostIndex).id)) {
break;
}
}
// find if there's an overlap between the new data and the current data
int indexOfGapInResponse = 0;
for (;indexOfGapInResponse<result.size();indexOfGapInResponse++){
if (Objects.equals(aboveGapID, result.get(indexOfGapInResponse).id)) {
break;
}
}
// there is an overlap between new and current data
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
if(indexOfGapInResponse<result.size()){
result=result.subList(indexOfGapInResponse+1,result.size());
Optional<Status> gapStatus=data.stream()
.filter(s->Objects.equals(s.id, gap.parentID))
.findFirst();
if (gapStatus.isPresent()) {
gapStatus.get().hasGapAfter=false;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus.get()), false);
}
targetList.clear();
} else {
gap.loading=false;
}
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
for(Status s:result){
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, FilterContext.HOME);
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
}else{
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
}
list.scrollToPosition(getMainAdapterOffset()+gapPos+targetList.size());
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
}
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, getFilterContext());
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
}else{
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
}
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
}
}

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetList;
import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.FilterContext;
@@ -25,10 +26,8 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ListEditor;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -138,9 +137,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<Status> result) {
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
if(getActivity()==null) return;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
}
})
.exec(accountID);

View File

@@ -96,7 +96,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if (n.type == Notification.Type.FOLLOW_REQUEST) {
ArrayList<StatusDisplayItem> items = new ArrayList<>();
items.add(titleItem);
items.add(new AccountCardStatusDisplayItem(n.id, this, n.account, n));
items.add(new AccountCardStatusDisplayItem(n.id, this, accountID, n.account, n));
return items;
}
if(n.status!=null){
@@ -134,6 +134,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return;
maxID=result.maxID;
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
if(bannerHelper!=null) bannerHelper.onBannerBecameVisible();
reloadingFromCache=false;
if (getParentFragment() instanceof NotificationsFragment nf) {
nf.updateMarkAllReadButton();

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
@@ -35,6 +36,8 @@ public class PinnedPostsListFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, false);
}
}).exec(accountID);

View File

@@ -23,7 +23,6 @@ import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
@@ -60,8 +59,10 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
import org.joinmastodon.android.model.Account;
@@ -132,6 +133,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private View usernameWrap;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private ImageView lockIcon, botIcon;
private ProgressBarButton actionButton, notifyButton;
@@ -232,6 +234,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
cover=content.findViewById(R.id.cover);
avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name);
usernameWrap=content.findViewById(R.id.username_wrap);
username=content.findViewById(R.id.username);
lockIcon=content.findViewById(R.id.lock_icon);
botIcon=content.findViewById(R.id.bot_icon);
@@ -304,6 +307,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabbar.setTabTextSize(V.dp(14));
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, (tab, position)->tab.setText(switch(position){
case 0 -> R.string.profile_featured;
case 1 -> R.string.profile_timeline;
case 2 -> R.string.profile_about;
default -> throw new IllegalStateException();
}));
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
@@ -317,6 +326,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if (position == 4) tab.view.setVisibility(View.GONE);
}
});
tabbar.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@Override
public void onTabReselected(TabLayout.Tab tab){
if(getFragmentForPage(tab.getPosition()) instanceof ScrollableToTop stt)
stt.scrollToTop();
}
});
cover.setOutlineProvider(new ViewOutlineProvider(){
@Override
@@ -377,7 +399,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
});
username.setOnLongClickListener(v->{
content.findViewById(R.id.username_wrap).setOnLongClickListener(v->{
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+domain;
@@ -601,7 +623,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void bindHeaderView(){
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)));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() :
GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic,
V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
@@ -824,6 +849,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileDisplayUsername", account.getDisplayUsername());
}
Nav.go(getActivity(), ListsFragment.class, args);
}else if(id==R.id.muted_accounts){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), MutedAccountsListFragment.class, args);
}else if(id==R.id.blocked_accounts){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), BlockedAccountsListFragment.class, args);
}else if(id==R.id.followed_hashtags){
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -1081,7 +1116,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
name.setVisibility(View.GONE);
rolesView.setVisibility(View.GONE);
username.setVisibility(View.GONE);
usernameWrap.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
@@ -1130,7 +1165,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEditWrap.setVisibility(View.GONE);
name.setVisibility(View.VISIBLE);
rolesView.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
usernameWrap.setVisibility(View.VISIBLE);
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
refreshLayout.setEnabled(true);
@@ -1229,7 +1264,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(ava==null)
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() : account.avatar, ava), 0,
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
@@ -1301,9 +1336,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=tabViews[viewType];
if (view.getParent() != null) ((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE);
FrameLayout view=new FrameLayout(parent.getContext());
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view);
}
@@ -1311,8 +1344,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
Fragment fragment=getFragmentForPage(position);
FrameLayout fragmentView=tabViews[position];
fragmentView.setVisibility(View.VISIBLE);
if(fragmentView.getParent() instanceof ViewGroup parent)
parent.removeView(fragmentView);
((FrameLayout)holder.itemView).addView(fragmentView);
if(!fragment.isAdded()){
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
getChildFragmentManager().beginTransaction().add(fragmentView.getId(), fragment).commit();
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){

View File

@@ -2,8 +2,11 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
@@ -26,6 +29,7 @@ import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
private String nextMaxID;
@@ -186,6 +190,21 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
return null;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(contentView!=null){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
int insetBottom=insets.getSystemWindowInsetBottom();
((ViewGroup.MarginLayoutParams) list.getLayoutParams()).bottomMargin=insetBottom;
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+insetBottom;
insets=insets.inset(0, 0, 0, insetBottom);
}else{
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16);
}
}
super.onApplyWindowInsets(insets);
}
@Override
public Uri getWebUri(Uri.Builder base) {
// TODO: adapt when frontends finally implement a scheduled posts list

View File

@@ -47,13 +47,12 @@ public class SplashFragment extends AppKitFragment{
private ProgressBarButton defaultServerButton;
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer;
private boolean loadingDefaultServer, loadedDefaultServer;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
loadAndChooseDefaultServer();
}
@Nullable
@@ -101,6 +100,8 @@ public class SplashFragment extends AppKitFragment{
});
}
});
if(!loadedDefaultServer && !loadingDefaultServer)
loadAndChooseDefaultServer();
return contentView;
}
@@ -239,6 +240,7 @@ public class SplashFragment extends AppKitFragment{
private void setChosenDefaultServer(String domain){
chosenDefaultServer=domain;
loadingDefaultServer=false;
loadedDefaultServer=true;
if(defaultServerButton!=null && getActivity()!=null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);

View File

@@ -9,6 +9,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
@@ -47,8 +48,8 @@ public class StatusEditHistoryFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
if (getActivity() == null) return;
onDataLoaded(result, false);
}
})
@@ -143,7 +144,8 @@ public class StatusEditHistoryFragment extends StatusListFragment{
}
}
String sep = getString(R.string.sk_separator);
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null));
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null, s));
items.add(1, new DummyStatusDisplayItem(s.id, this));
}
return items;
}

View File

@@ -42,10 +42,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
boolean isMainThreadStatus = this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id);
int flags = 0;
AccountLocalPreferences lp=getLocalPrefs();
if (GlobalUserPreferences.spectatorMode)
if(GlobalUserPreferences.spectatorMode)
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
if (!lp.emojiReactionsEnabled || lp.showEmojiReactions==ONLY_OPENED)
if(!lp.emojiReactionsEnabled || lp.showEmojiReactions==ONLY_OPENED)
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
if(GlobalUserPreferences.translateButtonOpenedOnly)
flags |= StatusDisplayItem.FLAG_NO_TRANSLATE;
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags);
}

View File

@@ -13,8 +13,8 @@ import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
@@ -24,13 +24,13 @@ import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayDeque;
@@ -105,6 +105,12 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
else if(item instanceof SpoilerStatusDisplayItem spoiler){
for(StatusDisplayItem subItem:spoiler.contentItems){
if(subItem instanceof TextStatusDisplayItem text)
text.textSelectable=true;
}
}
}
}
@@ -188,8 +194,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
// TODO: figure out how this code works
if (isInstanceAkkoma()) sortStatusContext(mainStatus, result);
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
filterStatuses(result.descendants);
filterStatuses(result.ancestors);
restoreStatusStates(result.descendants, oldData);
restoreStatusStates(result.ancestors, oldData);
@@ -325,11 +331,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
.collect(Collectors.toList());
}
private List<Status> filterStatuses(List<Status> statuses){
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
return statuses.stream()
.filter(statusFilterPredicate)
.collect(Collectors.toList());
private void filterStatuses(List<Status> statuses){
AccountSessionManager.get(accountID).filterStatuses(statuses, getFilterContext());
}
@Override

View File

@@ -0,0 +1,36 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountBlocks;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
public class BlockedAccountsListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.sk_blocked_accounts);
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountBlocks(maxID, count);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath("/blocks").build();
}
}

View File

@@ -48,7 +48,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false, null, 0, 0)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){

View File

@@ -0,0 +1,36 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountMutes;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
public class MutedAccountsListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.sk_muted_accounts);
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountMutes(maxID, count);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath("/mutes").build();
}
}

View File

@@ -6,14 +6,13 @@ import android.os.Bundle;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
@@ -40,11 +39,12 @@ public class BubbleTimelineFragment extends StatusListFragment {
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
if(getActivity()==null) return;
boolean empty=result.isEmpty();
if(!empty) maxID=result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);

View File

@@ -16,6 +16,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
@@ -319,8 +320,9 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
public AccountWrapper(Account account){
this.account=account;
if(!TextUtils.isEmpty(account.avatar))
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? AccountSessionManager.getInstance().getAccount(accountID).getDefaultAvatarUrl() : account.avatar,
V.dp(50), V.dp(50));
if(!TextUtils.isEmpty(account.header))
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);

View File

@@ -289,15 +289,19 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=tabViews[viewType];
((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE);
FrameLayout view=new FrameLayout(parent.getContext());
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){}
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
FrameLayout view=tabViews[position];
if(view.getParent() instanceof ViewGroup parent)
parent.removeView(view);
view.setVisibility(View.VISIBLE);
((FrameLayout)holder.itemView).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
@Override
public int getItemCount(){

View File

@@ -4,6 +4,7 @@ import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
@@ -30,7 +31,10 @@ public class DiscoverPostsFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
if(getActivity()==null) return;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
}).exec(accountID);

View File

@@ -33,10 +33,10 @@ public class FederatedTimelineFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if(getActivity()==null) return;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
if(!empty) maxID=result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}

View File

@@ -2,7 +2,6 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -34,10 +33,10 @@ public class LocalTimelineFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if(getActivity()==null) return;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
if(!empty) maxID=result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}

View File

@@ -23,7 +23,6 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -31,7 +30,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
@@ -97,7 +96,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
@@ -113,7 +112,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}
@Override
protected void doLoadData(int offset, int count){
protected void doLoadData(int _offset, int count){
GetSearchResults.Type type;
if(currentFilter.size()==1){
type=switch(currentFilter.iterator().next()){
@@ -128,7 +127,21 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
dataLoaded();
return;
}
currentRequest=new GetSearchResults(currentQuery, type, true)
String maxID=null;
// TODO server-side bug
/*int offset=0;
if(_offset>0){
if(type==GetSearchResults.Type.STATUSES){
if(!preloadedData.isEmpty())
maxID=preloadedData.get(preloadedData.size()-1).status.id;
else if(!data.isEmpty())
maxID=data.get(data.size()-1).status.id;
}else{
offset=_offset;
}
}*/
int offset=_offset;
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
@@ -142,12 +155,15 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
Set<String> alreadyLoadedStatuses=data.stream().filter(r->r.type==SearchResult.Type.STATUS).map(r->r.status.id).collect(Collectors.toSet());
for(Status status:result.statuses){
if(!alreadyLoadedStatuses.contains(status.id))
results.add(new SearchResult(status));
}
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
}
@Override

View File

@@ -10,7 +10,6 @@ import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -26,7 +25,7 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -38,19 +37,16 @@ import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.SearchResultViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -126,7 +122,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
recentsHeader.setVisible(!data.isEmpty());
});
}else{
currentRequest=new GetSearchResults(currentQuery, null, false)
currentRequest=new GetSearchResults(currentQuery, null, false, null, 0, 0)
.limit(2)
.setCallback(new SimpleCallback<>(this){
@Override
@@ -381,16 +377,56 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
}
private void openHashtag(SearchResult res){
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
wrapSuicideDialog(()->{
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
});
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
private void wrapSuicideDialog(Runnable r){
if(!GlobalUserPreferences.showSuicideHelp || currentQuery==null){
r.run();
return;
}
String[] terms=getContext().getString(R.string.sk_suicide_search_terms).toLowerCase().split(",");
String query=currentQuery.trim().toLowerCase();
boolean termMatches=false;
for(String term : terms){
if(query.contains(term)){
termMatches=true;
break;
}
}
if(!termMatches){
r.run();
return;
}
String url=getContext().getString(R.string.sk_suicide_helplines_url);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_search_suicide_title)
.setMessage(R.string.sk_search_suicide_message)
.setNegativeButton(R.string.sk_do_not_show_again, (dialog, which)->{
GlobalUserPreferences.showSuicideHelp = false;
GlobalUserPreferences.save();
r.run();
})
.setNeutralButton(R.string.sk_search_suicide_hotlines, (dialog, which)->UiUtils.launchWebBrowser(getContext(), url))
.setPositiveButton(R.string.ok, (dialog, which)->r.run())
.setOnDismissListener((dialog)->{})
.show();
}
private void onSearchViewEnter(){
deliverResult(currentQuery, null);
if(TextUtils.isEmpty(currentQuery) || currentQuery.trim().isEmpty())
return;
wrapSuicideDialog(()->deliverResult(currentQuery, null));
}
private void onOpenURLClick(){
@@ -398,10 +434,12 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
}
private void onGoToHashtagClick(){
String q=searchViewHelper.getQuery();
if(q.startsWith("#"))
q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q, null);
wrapSuicideDialog(()->{
String q=searchViewHelper.getQuery();
if(q.startsWith("#"))
q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q);
});
}
private void onGoToAccountClick(){
@@ -422,11 +460,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
}
private void onGoToStatusSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
wrapSuicideDialog(()->deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS));
}
private void onGoToAccountSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
wrapSuicideDialog(()->deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT));
}
private void onClearRecentClick(){

View File

@@ -105,7 +105,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
@Override
public void onClick(){
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
UiUtils.openHashtagTimeline(getActivity(), accountID, item);
}
}
}

View File

@@ -165,9 +165,7 @@ public class AccountActivationFragment extends ToolbarFragment{
private void tryGetAccount(){
if(AccountSessionManager.getInstance().tryGetAccount(accountID)==null){
uiHandler.removeCallbacks(pollRunnable);
getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
((MainActivity)getActivity()).restartHomeFragment();
return;
}
currentRequest=new GetOwnAccount()

View File

@@ -271,7 +271,7 @@ public class SignupFragment extends ToolbarFragment{
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
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);
}
}

View File

@@ -87,6 +87,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
for(Status s:result){
s.sensitive=true;
}

View File

@@ -83,7 +83,7 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
if(reportStatus!=null){
Status hiddenStatus=reportStatus.clone();
hiddenStatus.spoilerText=getString(R.string.post_hidden);
if(hiddenStatus.spoilerText==null) hiddenStatus.spoilerText=getString(R.string.post_hidden);
onDataLoaded(Collections.singletonList(hiddenStatus));
setTitle(R.string.report_title_post);
}else{
@@ -168,17 +168,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
((UsableRecyclerView)list).setIncludeMarginsInItemHitbox(false);
if(reportStatus!=null){
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
outRect.left=V.dp(16);
outRect.right=V.dp(16);
}
}
});
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
{
@@ -222,10 +211,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
if(holder instanceof StatusDisplayItem.Holder<?>){
outRect.left=outRect.right=V.dp(16);
}
int index=holder.getAbsoluteAdapterPosition()-mergeAdapter.getPositionForAdapter(adapter);
if(index==displayItems.size()){
outRect.top=V.dp(32);
}
}
});
}
@@ -251,18 +236,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
return null;
}
@Override
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
View layout=h.getLayout();
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
layout.setClipToOutline(true);
View overlay=h.getSensitiveOverlay();
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
overlay.setClipToOutline(true);
}
}
@Override
public void putRelationship(String id, Relationship rel){
super.putRelationship(id, rel);

View File

@@ -33,7 +33,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
setTitle(getString(R.string.about_app, getString(R.string.sk_app_name)));
AccountSession s=AccountSessionManager.get(accountID);
onDataLoaded(List.of(
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, ()->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag), null)),
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, ()->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag))),
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))),
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),

View File

@@ -20,6 +20,7 @@ import org.joinmastodon.android.utils.MastodonLanguage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> implements HasAccountID{
private ListItem<Void> languageItem;

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
@@ -37,7 +38,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> revealCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
// MEGALODON
private CheckableListItem<Void> trueBlackModeItem, marqueeItem, disableSwipeItem, reduceMotionItem, altIndicatorItem, noAltIndicatorItem, collapsePostsItem, spectatorModeItem, hideFabItem, translateOpenedItem, disablePillItem, showNavigationLabelsItem;
private CheckableListItem<Void> trueBlackModeItem, marqueeItem, disableSwipeItem, reduceMotionItem, altIndicatorItem, noAltIndicatorItem, collapsePostsItem, spectatorModeItem, hideFabItem, translateOpenedItem, disablePillItem, showNavigationLabelsItem, likeIconItem;
private ListItem<Void> colorItem, publishTextItem, autoRevealCWsItem;
private CheckableListItem<Void> pronounsInUserListingsItem, pronounsInTimelinesItem, pronounsInThreadsItem;
@@ -69,6 +70,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
hideFabItem=new CheckableListItem<>(R.string.sk_settings_hide_fab, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.autoHideFab, R.drawable.ic_fluent_edit_24_regular, ()->toggleCheckableItem(hideFabItem)),
translateOpenedItem=new CheckableListItem<>(R.string.sk_settings_translate_only_opened, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.translateButtonOpenedOnly, R.drawable.ic_fluent_translate_24_regular, ()->toggleCheckableItem(translateOpenedItem)),
disablePillItem=new CheckableListItem<>(R.string.sk_disable_pill_shaped_active_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableM3PillActiveIndicator, R.drawable.ic_fluent_pill_24_regular, ()->toggleCheckableItem(disablePillItem)),
likeIconItem=new CheckableListItem<>(R.string.sk_settings_like_icon, 0, CheckableListItem.Style.SWITCH, lp.likeIcon, R.drawable.ic_fluent_heart_24_regular, ()->toggleCheckableItem(likeIconItem)),
showNavigationLabelsItem=new CheckableListItem<>(R.string.sk_settings_show_labels_in_navigation_bar, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNavigationLabels, R.drawable.ic_fluent_tag_24_regular, ()->toggleCheckableItem(showNavigationLabelsItem), true),
pronounsInTimelinesItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_timelines, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInTimelines, 0, ()->toggleCheckableItem(pronounsInTimelinesItem)),
pronounsInThreadsItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_threads, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInThreads, 0, ()->toggleCheckableItem(pronounsInThreadsItem)),
@@ -96,12 +98,14 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
boolean restartPlease=
GlobalUserPreferences.disableM3PillActiveIndicator!=disablePillItem.checked ||
GlobalUserPreferences.showNavigationLabels!=showNavigationLabelsItem.checked;
GlobalUserPreferences.showNavigationLabels!=showNavigationLabelsItem.checked ||
lp.likeIcon!=likeIconItem.checked;
lp.revealCWs=revealCWsItem.checked;
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
lp.showInteractionCounts=interactionCountsItem.checked;
lp.customEmojiInNames=emojiInNamesItem.checked;
lp.likeIcon=likeIconItem.checked;
lp.save();
GlobalUserPreferences.toolbarMarquee=marqueeItem.checked;
GlobalUserPreferences.reduceMotion=reduceMotionItem.checked;
@@ -131,7 +135,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
}
private @StringRes int getColorPaletteValue(){
return switch(GlobalUserPreferences.color){
return switch(AccountSessionManager.get(accountID).getLocalPreferences().color){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
@@ -196,18 +200,18 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
}
private void onColorClick(){
int selected=GlobalUserPreferences.color.ordinal();
int selected=lp.color.ordinal();
int[] newSelected={selected};
String[] names=Arrays.stream(GlobalUserPreferences.ColorPreference.values()).map(GlobalUserPreferences.ColorPreference::getName).map(this::getString).toArray(String[]::new);
String[] names=Arrays.stream(ColorPreference.values()).map(ColorPreference::getName).map(this::getString).toArray(String[]::new);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme)
.setTitle(R.string.sk_settings_color_palette)
.setSingleChoiceItems(names,
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.ColorPreference pref=GlobalUserPreferences.ColorPreference.values()[newSelected[0]];
if(pref!=GlobalUserPreferences.color){
GlobalUserPreferences.ColorPreference prev=GlobalUserPreferences.color;
GlobalUserPreferences.color=pref;
ColorPreference pref=ColorPreference.values()[newSelected[0]];
if(pref!=lp.color){
ColorPreference prev=lp.color;
lp.color=pref;
GlobalUserPreferences.save();
colorItem.subtitleRes=getColorPaletteValue();
rebindItem(colorItem);
@@ -257,17 +261,17 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
.show();
}
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prevTheme, GlobalUserPreferences.ColorPreference prevColor, Boolean prevTrueBlack){
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prevTheme, ColorPreference prevColor, Boolean prevTrueBlack){
if(prevTheme==null) prevTheme=GlobalUserPreferences.theme;
if(prevTrueBlack==null) prevTrueBlack=GlobalUserPreferences.trueBlackTheme;
if(prevColor==null) prevColor=GlobalUserPreferences.color;
if(prevColor==null) prevColor=lp.color;
boolean isCurrentDark=prevTheme==GlobalUserPreferences.ThemePreference.DARK ||
(prevTheme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
boolean isNewBlack=GlobalUserPreferences.trueBlackTheme;
if(isCurrentDark!=isNewDark || prevColor!=GlobalUserPreferences.color || (isNewDark && prevTrueBlack!=isNewBlack)){
if(isCurrentDark!=isNewDark || prevColor!=lp.color || (isNewDark && prevTrueBlack!=isNewBlack)){
restartActivityToApplyNewTheme();
}
}

View File

@@ -55,6 +55,8 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
onDataLoaded(List.of(
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_fluent_settings_24_regular, this::onBehaviorClick),
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_fluent_color_24_regular, 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_fluent_alert_24_regular, this::onNotificationsClick),
new ListItem<>(R.string.sk_settings_instance, 0, R.drawable.ic_fluent_server_24_regular, this::onInstanceClick),
new ListItem<>(getString(R.string.about_app, getString(R.string.sk_app_name)), null, R.drawable.ic_fluent_info_24_regular, this::onAboutClick, null, 0, true),
@@ -69,7 +71,9 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_fluent_wrench_screwdriver_24_regular, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
}
account.reloadPreferences(null);
AccountSession session=AccountSessionManager.get(accountID);
session.reloadPreferences(null);
session.updateAccountInfo();
E.register(this);
}
@@ -133,6 +137,10 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
}
private void onPrivacyClick(){
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
}
private void onFiltersClick(){
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
}
@@ -155,9 +163,7 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
.setPositiveButton(R.string.log_out, (dialog, which)->account.logOut(getActivity(), ()->{
loggedOut=true;
getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
((MainActivity)getActivity()).restartActivity();
}))
.setNegativeButton(R.string.cancel, null)
.show();

View File

@@ -0,0 +1,41 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import java.util.List;
public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> discoverableItem, indexableItem;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_privacy);
Account self=AccountSessionManager.get(accountID).self;
onDataLoaded(List.of(
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_24px, ()->toggleCheckableItem(discoverableItem)),
indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_search_24px, ()->toggleCheckableItem(indexableItem))
));
if(self.source.indexable==null)
indexableItem.isEnabled=false;
}
@Override
protected void doLoadData(int offset, int count){}
@Override
public void onPause(){
super.onPause();
Account self=AccountSessionManager.get(accountID).self;
if(self.discoverable!=discoverableItem.checked || (self.source.indexable!=null && self.source.indexable!=indexableItem.checked)){
self.discoverable=discoverableItem.checked;
self.source.indexable=indexableItem.checked;
AccountSessionManager.get(accountID).savePreferencesLater();
}
}
}

View File

@@ -153,18 +153,21 @@ public class SettingsServerFragment extends AppKitFragment{
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=tabViews[viewType];
((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE);
FrameLayout view=new FrameLayout(parent.getContext());
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
FrameLayout view=tabViews[position];
if(view.getParent() instanceof ViewGroup parent)
parent.removeView(view);
view.setVisibility(View.VISIBLE);
((FrameLayout)holder.itemView).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
Fragment fragment=getFragmentForPage(position);
if(!fragment.isAdded()){
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
getChildFragmentManager().beginTransaction().add(view.getId(), fragment).commit();
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){

View File

@@ -7,12 +7,17 @@ import androidx.annotation.Nullable;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.parceler.Parcel;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
/**
* Represents a user of Mastodon and their associated profile.
*/
@@ -23,22 +28,22 @@ public class Account extends BaseModel implements Searchable{
/**
* The account id
*/
@RequiredField
// @RequiredField
public String id;
/**
* The username of the account, not including domain.
*/
@RequiredField
// @RequiredField
public String username;
/**
* The Webfinger account URI. Equal to username for local users, or username@domain for remote users.
*/
@RequiredField
// @RequiredField
public String acct;
/**
* The location of the user's profile page.
*/
@RequiredField
// @RequiredField
public String url;
// Display attributes
@@ -51,12 +56,12 @@ public class Account extends BaseModel implements Searchable{
/**
* The profile's bio / description.
*/
@RequiredField
// @RequiredField
public String note;
/**
* An image icon that is shown next to statuses and in the profile.
*/
@RequiredField
// @RequiredField
public String avatar;
/**
* A static version of the avatar. Equal to avatar if its value is a static image; different if avatar is an animated GIF.
@@ -134,6 +139,7 @@ public class Account extends BaseModel implements Searchable{
* When a timed mute will expire, if applicable.
*/
public Instant muteExpiresAt;
public boolean noindex;
public List<Role> roles;
@@ -157,16 +163,26 @@ public class Account extends BaseModel implements Searchable{
if(fields!=null){
for(AccountField f:fields)
f.postprocess();
} else {
fields = Collections.emptyList();
}
if(emojis!=null){
for(Emoji e:emojis)
e.postprocess();
} else {
emojis = Collections.emptyList();
}
if(moved!=null)
moved.postprocess();
if(TextUtils.isEmpty(displayName))
displayName=username;
if(fqn == null) fqn = getFullyQualifiedName();
if(id == null) id = "";
if(username == null) username = "";
if(TextUtils.isEmpty(displayName))
displayName = !TextUtils.isEmpty(username) ? username : "";
if(acct == null) acct = "";
if(url == null) url = "";
if(note == null) note = "";
if(avatar == null) avatar = "";
}
public boolean isLocal(){
@@ -191,6 +207,8 @@ public class Account extends BaseModel implements Searchable{
}
public String getFullyQualifiedName() {
if (TextUtils.isEmpty(acct))
return "";
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
}
@@ -221,6 +239,7 @@ public class Account extends BaseModel implements Searchable{
", source="+source+
", suspended="+suspended+
", muteExpiresAt="+muteExpiresAt+
", noindex="+noindex+
'}';
}
}

View File

@@ -46,26 +46,34 @@ public class Attachment extends BaseModel{
public int getWidth(){
if(meta==null)
return 0;
return 1920;
if(meta.width>0)
return meta.width;
if(meta.original!=null && meta.original.width>0)
return meta.original.width;
if(meta.small!=null && meta.small.width>0)
return meta.small.width;
return 0;
return 1920;
}
public int getHeight(){
if(meta==null)
return 0;
return 1080;
if(meta.height>0)
return meta.height;
if(meta.original!=null && meta.original.height>0)
return meta.original.height;
if(meta.small!=null && meta.small.height>0)
return meta.small.height;
return 0;
return 1080;
}
public boolean hasKnownDimensions(){
return meta!=null && (
(meta.height>0 && meta.width>0)
|| (meta.original!=null && meta.original.height>0 && meta.original.width>0)
|| (meta.small!=null && meta.small.height>0 && meta.small.width>0)
);
}
public double getDuration(){

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model;
import java.util.ArrayList;
import java.util.List;
public class EmojiCategory{
@@ -10,4 +11,8 @@ public class EmojiCategory{
this.title=title;
this.emojis=emojis;
}
public EmojiCategory(EmojiCategory category){
this.title = category.title;
this.emojis = new ArrayList<>(category.emojis);
}
}

View File

@@ -14,7 +14,7 @@ public class FilterResult extends BaseModel {
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if(filter!=null) filter.postprocess();
if(filter!=null) filter.postprocess();
if(keywordMatches==null) keywordMatches=List.of();
}
}

View File

@@ -23,6 +23,7 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
", following="+following+
", history="+history+
", statusesCount="+statusesCount+
", following="+following+
'}';
}
@@ -30,4 +31,19 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
public String getID(){
return name;
}
@Override
public boolean equals(Object o){
if(this==o) return true;
if(o==null || getClass()!=o.getClass()) return false;
Hashtag hashtag=(Hashtag) o;
return name.equals(hashtag.name);
}
@Override
public int hashCode(){
return name.hashCode();
}
}

View File

@@ -20,4 +20,22 @@ public class Mention extends BaseModel{
", url='"+url+'\''+
'}';
}
@Override
public boolean equals(Object o){
if(this==o) return true;
if(o==null || getClass()!=o.getClass()) return false;
Mention mention=(Mention) o;
if(!id.equals(mention.id)) return false;
return url.equals(mention.url);
}
@Override
public int hashCode(){
int result=id.hashCode();
result=31*result+url.hashCode();
return result;
}
}

View File

@@ -37,6 +37,8 @@ public class Source extends BaseModel{
* The number of pending follow requests.
*/
public int followRequestCount;
public Boolean indexable;
public boolean hideCollections;
@Override
public void postprocess() throws ObjectValidationException{
@@ -54,6 +56,8 @@ public class Source extends BaseModel{
", sensitive="+sensitive+
", language='"+language+'\''+
", followRequestCount="+followRequestCount+
", indexable="+indexable+
", hideCollections="+hideCollections+
'}';
}
}

View File

@@ -5,8 +5,21 @@ import static org.joinmastodon.android.api.MastodonAPIController.gsonWithoutDese
import android.text.TextUtils;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import androidx.annotation.NonNull;
import com.github.bottomSoftwareFoundation.bottom.Bottom;
import com.github.bottomSoftwareFoundation.bottom.TranslationError;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
@@ -15,16 +28,21 @@ import com.google.gson.JsonParseException;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.utils.StatusTextEncoder;
import org.parceler.Parcel;
import java.lang.reflect.Type;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
@Parcel
public class Status extends BaseModel implements DisplayItemsParent, Searchable{
@@ -83,9 +101,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public transient boolean sensitiveRevealed;
public transient boolean textExpanded, textExpandable;
public transient boolean hasGapAfter;
public transient TranslatedStatus translation;
public transient boolean translationShown;
private transient String strippedText;
public transient TranslationState translationState=TranslationState.HIDDEN;
public transient Translation translation;
public Status(){}
@@ -201,6 +219,38 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
return (Status) super.clone();
}
public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48");
public boolean isEligibleForTranslation(AccountSession session){
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
boolean translateEnabled = instanceInfo != null &&
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
instanceInfo.v2.configuration.translation.enabled;
try {
String bottomText = BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find()
? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN)
: null;
if(bottomText==null || bottomText.length()==0 || bottomText.equals("\u0005")) bottomText=null;
if(bottomText!=null){
translation=new Translation();
translation.content=bottomText;
translation.detectedSourceLanguage="\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48";
translation.provider="bottom-java";
return true;
}
} catch (TranslationError ignored) {}
return translateEnabled && !TextUtils.isEmpty(content) && !TextUtils.isEmpty(language)
&& !Objects.equals(Locale.getDefault().getLanguage(), language)
&& (visibility==StatusPrivacy.PUBLIC || visibility==StatusPrivacy.UNLISTED);
}
public enum TranslationState{
HIDDEN,
SHOWN,
LOADING
}
public boolean isReblogPermitted(String accountID){
return visibility.isReblogPermitted(account.id.equals(
AccountSessionManager.getInstance().getAccount(accountID).self.id

View File

@@ -219,7 +219,7 @@ public class TimelineDefinition {
args.putString("listID", listId);
args.putBoolean("listIsExclusive", listIsExclusive);
} else if (type == TimelineType.HASHTAG) {
args.putString("hashtag", hashtagName);
args.putString("hashtagName", hashtagName);
args.putBoolean("localOnly", hashtagLocalOnly);
args.putStringArrayList("any", hashtagAny == null ? new ArrayList<>() : new ArrayList<>(hashtagAny));
args.putStringArrayList("all", hashtagAll == null ? new ArrayList<>() : new ArrayList<>(hashtagAll));

View File

@@ -1,7 +0,0 @@
package org.joinmastodon.android.model;
public class TranslatedStatus extends BaseModel {
public String content;
public String detectedSourceLanguage;
public String provider;
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.AllFieldsAreRequired;
@AllFieldsAreRequired
public class Translation extends BaseModel{
public String content;
public String detectedSourceLanguage;
public String provider;
}

View File

@@ -1,8 +1,10 @@
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.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
@@ -24,9 +26,13 @@ public class AccountViewModel{
public AccountViewModel(Account account, String accountID){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
AccountSession session = AccountSessionManager.get(accountID);
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? session.getDefaultAvatarUrl() :
GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic,
V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
if(session.getLocalPreferences().customEmojiInNames)
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
else
parsedName=account.displayName;

View File

@@ -8,6 +8,7 @@ import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -39,6 +40,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -144,21 +146,9 @@ public class AccountSwitcherSheet extends BottomSheet{
}
private void logOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
onLoggedOut(accountID);
}
@Override
public void onError(ErrorResponse error){
onLoggedOut(accountID);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
AccountSessionManager.get(accountID).logOut(activity, ()->{
((MainActivity)activity).restartActivity();
});
}
private void logOutAll(){
@@ -331,10 +321,10 @@ public class AccountSwitcherSheet extends BottomSheet{
onClick.accept(item.getID(), false);
return;
}
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null)
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null){
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
((MainActivity)activity).restartActivity();
}
}
@Override

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,7 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
@@ -41,12 +42,13 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
public CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
public CharSequence parsedName, parsedBio;
public AccountCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account, Notification notification){
public AccountCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Account account, Notification notification){
super(parentID, parentFragment);
this.account=account;
this.notification=notification;
if(!TextUtils.isEmpty(account.avatar))
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? AccountSessionManager.get(accountID).getDefaultAvatarUrl() : account.avatar,
V.dp(50), V.dp(50));
if(!TextUtils.isEmpty(account.header))
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), parentFragment.getAccountID());

View File

@@ -170,12 +170,14 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
@Override
public void onBind(EmojiReactionsStatusDisplayItem item) {
if(emojiKeyboard != null) root.removeView(emojiKeyboard.getView());
addButton.setSelected(false);
AccountSession session=item.parentFragment.getSession();
item.status.reactions.forEach(r->r.request=r.getUrl(item.playGifs)!=null
? new UrlImageLoaderRequest(r.getUrl(item.playGifs), V.sp(24), V.sp(24))
: null);
emojiKeyboard=new CustomEmojiPopupKeyboard(
(Activity) item.parentFragment.getContext(),
item.accountID,
AccountSessionManager.getInstance().getCustomEmojis(session.domain),
session.domain, true);
emojiKeyboard.setListener(this);

View File

@@ -14,6 +14,8 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.StatusEditHistoryFragment;
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
@@ -74,6 +76,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status;
AccountSession session=AccountSessionManager.get(item.accountID);
boolean like=session!=null && session.getLocalPreferences().likeIcon;
favorites.setCompoundDrawablesRelativeWithIntrinsicBounds(like ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular, 0, 0, 0);
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
reblogs.setVisibility(s.visibility != StatusPrivacy.DIRECT ? View.VISIBLE : View.GONE);

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -12,10 +13,9 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -56,14 +56,14 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView replies, boosts, favorites;
private final View reply, boost, favorite, share, bookmark;
private static final Animation opacityOut, opacityIn;
private final ImageView favIcon;
private View touchingView = null;
private boolean longClickPerformed = false;
private final Runnable longClickRunnable = () -> {
longClickPerformed = touchingView != null && touchingView.performLongClick();
if (longClickPerformed && touchingView != null) {
touchingView.startAnimation(opacityIn);
UiUtils.opacityIn(touchingView);
touchingView.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
}
};
@@ -77,18 +77,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
};
private static final float ALPHA_PRESSED=0.55f;
static {
opacityOut = new AlphaAnimation(1, ALPHA_PRESSED);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(ALPHA_PRESSED, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
}
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_footer, parent);
@@ -101,6 +89,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
favorite=findViewById(R.id.favorite_btn);
share=findViewById(R.id.share_btn);
bookmark=findViewById(R.id.bookmark_btn);
favIcon=findViewById(R.id.favorite_icon);
reply.setOnTouchListener(this::onButtonTouch);
reply.setOnClickListener(this::onReplyClick);
@@ -144,6 +133,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
!nextIsWarning;
AccountSession session=AccountSessionManager.get(item.accountID);
boolean like=session!=null && session.getLocalPreferences().likeIcon;
ColorStateList color=item.parentFragment.getResources().getColorStateList(
like ? R.color.like_icon : R.color.favorite_icon, item.parentFragment.getContext().getTheme()
);
favIcon.setImageResource(like ? R.drawable.ic_fluent_heart_24_selector : R.drawable.ic_fluent_star_24_selector);
favIcon.setImageTintList(color);
favorites.setTextColor(color);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-5) : 0);
@@ -172,7 +171,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
if (!longClickPerformed) v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
if (disabled) return true;
if (action == MotionEvent.ACTION_UP && !longClickPerformed) v.performClick();
else if (!longClickPerformed) v.startAnimation(opacityIn);
else if (!longClickPerformed) UiUtils.opacityIn(v);
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
@@ -180,13 +179,13 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
v.startAnimation(opacityOut);
UiUtils.opacityOut(v);
}
return true;
}
private void onReplyClick(View v){
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("replyTo", Parcels.wrap(item.status));
@@ -210,7 +209,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onBoostClick(View v){
if (GlobalUserPreferences.confirmBoost) {
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
onBoostLongClick(v);
return;
}
@@ -219,7 +218,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
bindText(boosts, r.reblogsCount);
}
@@ -230,7 +229,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
AccountSession session = AccountSessionManager.getInstance().getAccount(item.accountID);
Consumer<StatusPrivacy> doReblog = (visibility) -> {
v.startAnimation(opacityOut);
UiUtils.opacityOut(v);
session.getStatusInteractionController()
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
dialog.dismiss();
@@ -283,7 +282,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
menu.findViewById(R.id.quote).setOnClickListener(c->{
dialog.dismiss();
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
Bundle args=new Bundle();
args.putString("account", item.accountID);
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(item.accountID);
@@ -308,7 +307,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onFavoriteClick(View v){
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
bindText(favorites, r.favouritesCount);
});
}
@@ -330,7 +329,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onBookmarkClick(View v){
bookmark.setSelected(!item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
});
}
@@ -349,7 +348,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onShareClick(View v){
v.startAnimation(opacityIn);
UiUtils.opacityIn(v);
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, item.status.url);

View File

@@ -8,14 +8,21 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.Instant;
import me.grishka.appkit.utils.V;
// Mind the gap!
public class GapStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
private Status status;
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){
super(parentID, parentFragment);
this.status=status;
}
@Override
@@ -24,25 +31,53 @@ public class GapStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<GapStatusDisplayItem>{
public final ProgressBar progress;
public final TextView text;
public final ProgressBar progressTop, progressBottom;
public final TextView textTop, gap, textBottom;
public final View top, bottom;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_gap, parent);
progress=findViewById(R.id.progress);
text=findViewById(R.id.text);
itemView.setForeground(new SawtoothTearDrawable(context));
progressTop=findViewById(R.id.progress_top);
progressBottom=findViewById(R.id.progress_bottom);
textTop=findViewById(R.id.text_top);
textBottom=findViewById(R.id.text_bottom);
top=findViewById(R.id.top);
top.setOnClickListener(this::onViewClick);
bottom=findViewById(R.id.bottom);
bottom.setOnClickListener(this::onViewClick);
gap=findViewById(R.id.gap);
gap.setForeground(new SawtoothTearDrawable(context));
}
@Override
public void onBind(GapStatusDisplayItem item){
text.setVisibility(item.loading ? View.GONE : View.VISIBLE);
progress.setVisibility(item.loading ? View.VISIBLE : View.GONE);
if(!item.loading){
progressBottom.setVisibility(View.GONE);
progressTop.setVisibility(View.GONE);
}
top.setClickable(!item.loading);
bottom.setClickable(!item.loading);
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
Instant dateBelow=next instanceof HeaderStatusDisplayItem h ? h.status.createdAt
: next instanceof ReblogOrReplyLineStatusDisplayItem l ? l.status.createdAt
: null;
String text=dateBelow!=null && item.status.createdAt!=null && dateBelow.isBefore(item.status.createdAt)
? UiUtils.formatPeriodBetween(item.parentFragment.getContext(), dateBelow, item.status.createdAt)
: null;
gap.setText(text);
int p=text==null ? V.dp(6) : V.dp(20);
gap.setPadding(p, p, p, p);
}
private void onViewClick(View v){
if(item.loading) return;
boolean isTop=v==top;
UiUtils.opacityOut(isTop ? textTop : textBottom);
V.setVisibilityAnimated((isTop ? progressTop : progressBottom), View.VISIBLE);
item.parentFragment.onGapClick(this, isTop);
}
@Override
public void onClick(){
item.parentFragment.onGapClick(this);
}
public void onClick(){}
}
}

View File

@@ -66,6 +66,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class HeaderStatusDisplayItem extends StatusDisplayItem{
@@ -76,7 +77,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private SpannableStringBuilder parsedName;
public final Status status;
private boolean hasVisibilityToggle;
public boolean hasVisibilityToggle;
boolean needBottomPadding;
private CharSequence extraText;
private Notification notification;
@@ -86,10 +87,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, CharSequence extraText, Notification notification, ScheduledStatus scheduledStatus){
super(parentID, parentFragment);
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
AccountSession session = AccountSessionManager.get(accountID);
user=scheduledStatus != null ? session.self : user;
this.user=user;
this.createdAt=createdAt;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(user.avatar) ? session.getDefaultAvatarUrl() :
GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic,
V.dp(50), V.dp(50));
this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName);
this.status=status;
@@ -290,17 +295,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
UiUtils.enablePopupMenuIcons(activity, optionsMenu);
}
private void populateAccountsMenu(Menu menu) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
sessions.stream().filter(s -> !s.getID().equals(item.accountID)).forEach(s -> {
String username = "@"+s.self.username+"@"+s.domain;
menu.add(username).setOnMenuItemClickListener(c->{
UiUtils.openURL(item.parentFragment.getActivity(), s.getID(), item.status.url, false);
return true;
});
});
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(HeaderStatusDisplayItem item){
@@ -327,23 +321,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
botIcon.setColorFilter(username.getCurrentTextColor());
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
visibility.setVisibility(item.hasVisibilityToggle ? View.VISIBLE : View.GONE);
if (item.hasVisibilityToggle){
boolean hidden = !item.status.sensitiveRevealed || (item.status.hasSpoiler() && !item.status.spoilerRevealed);
// doing this because V.setVisibilityAnimated ignores changes between INVISIBLE and GONE
int newVis=hidden ? View.INVISIBLE : View.VISIBLE;
if(newVis==View.INVISIBLE && visibility.getVisibility()==View.GONE)
visibility.setVisibility(newVis);
else
V.setVisibilityAnimated(visibility, newVis);
visibility.setEnabled(!hidden);
visibility.setContentDescription(item.parentFragment.getString(item.status.sensitiveRevealed ? R.string.spoiler_hide : R.string.spoiler_show));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
visibility.setTooltipText(visibility.getContentDescription());
}
} else {
visibility.setVisibility(View.GONE);
boolean visible = item.status.sensitiveRevealed && (!item.status.hasSpoiler() || item.status.spoilerRevealed);
visibility.setAlpha(visible ? 1 : 0f);
visibility.setScaleY(visible ? 1 : 0.8f);
visibility.setScaleX(visible ? 1 : 0.8f);
visibility.setEnabled(visible);
}
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
if(TextUtils.isEmpty(item.extraText)){
@@ -408,6 +392,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
item.inset ? V.dp(10) : V.dp(4), itemView.getPaddingBottom());
}
public void animateVisibilityToggle(boolean visible){
visibility.animate()
.alpha(visible ? 1 : 0)
.scaleX(visible ? 1 : 0.8f)
.scaleY(visible ? 1 : 0.8f)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.start();
visibility.setEnabled(visible);
}
@Override
public void setImage(int index, Drawable drawable){
if(index>0){
@@ -430,6 +424,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void onAvaClick(View v){
if (TextUtils.isEmpty(item.user.url))
return;
if (item.announcement != null) {
UiUtils.openURL(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.user.url);
return;

View File

@@ -20,6 +20,8 @@ import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.Emoji;
@@ -44,16 +46,22 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
private final String accountID;
private final CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private final CharSequence text;
private final CharSequence timestamp;
public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Notification notification, String accountID){
super(parentID, parentFragment);
this.notification=notification;
this.accountID=accountID;
this.timestamp=notification.createdAt==null ? null : UiUtils.formatRelativeTimestamp(context, notification.createdAt);
if(notification.type==Notification.Type.POLL){
text=parentFragment.getString(R.string.poll_ended);
}else{
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, V.dp(50), V.dp(50));
AccountSession session = AccountSessionManager.get(accountID);
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(notification.account.avatar) ? session.getDefaultAvatarUrl() :
GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic,
V.dp(50), V.dp(50));
SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName);
HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis);
String str = parentFragment.getString(switch(notification.type){
@@ -106,7 +114,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final ImageView icon, avatar;
private final TextView text;
private final TextView text, timestamp;
private final int selectableItemBackground;
public Holder(Activity activity, ViewGroup parent){
@@ -114,6 +122,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
icon=findViewById(R.id.icon);
avatar=findViewById(R.id.avatar);
text=findViewById(R.id.text);
timestamp=findViewById(R.id.timestamp);
avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
avatar.setClipToOutline(true);
@@ -146,6 +155,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(NotificationHeaderStatusDisplayItem item){
text.setText(item.text);
timestamp.setText(item.timestamp);
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
icon.setImageResource(switch(item.notification.type){
case FAVORITE -> R.drawable.ic_fluent_star_24_filled;

View File

@@ -68,7 +68,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private final TextView text, percent;
private final View button;
private final ImageView icon;
private final Drawable progressBg, progressBgInset;
private final Drawable progressBg;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_poll_option, parent);
@@ -77,9 +77,8 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
icon=findViewById(R.id.icon);
button=findViewById(R.id.button);
progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate();
progressBgInset=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted_inset, activity.getTheme()).mutate();
itemView.setOnClickListener(this::onButtonClick);
button.setOutlineProvider(OutlineProviders.M3_BUTTON);
button.setOutlineProvider(OutlineProviders.roundedRect(20));
button.setClipToOutline(true);
}
@@ -93,22 +92,17 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
item.showResults ? R.drawable.ic_poll_option_button : R.drawable.ic_fluent_radio_button_24_selector
));
if(item.showResults){
Drawable bg=item.inset ? progressBgInset : progressBg;
Drawable bg=progressBg;
bg.setLevel(Math.round(10000f*item.votesFraction));
button.setBackground(bg);
itemView.setSelected(item.poll.ownVotes!=null && item.poll.ownVotes.contains(item.optionIndex));
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
}else{
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
button.setBackgroundResource(item.inset ? R.drawable.bg_poll_option_clickable_inset : R.drawable.bg_poll_option_clickable);
}
if(item.inset){
text.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset));
percent.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset));
}else{
text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), android.R.attr.textColorPrimary));
percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer));
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
}
text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), android.R.attr.textColorPrimary));
percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer));
}
@Override

View File

@@ -15,8 +15,10 @@ import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
@@ -43,18 +45,21 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public boolean needBottomPadding;
ReblogOrReplyLineStatusDisplayItem extra;
CharSequence fullText;
Status status;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text);
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);
}
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, CharSequence fullText) {
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, CharSequence fullText, Status status) {
super(parentID, parentFragment);
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
HtmlParser.parseCustomEmoji(ssb, emojis);
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(ssb, emojis);
this.text=ssb;
emojiHelper.setText(ssb);
this.icon=icon;
this.status=status;
this.handleClick=handleClick;
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);

View File

@@ -6,9 +6,9 @@ import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status;
@@ -29,11 +29,13 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
private final CharSequence parsedTitle;
private final CustomEmojiHelper emojiHelper;
private final Type type;
private final int attachmentCount;
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, String title, Status statusForContent, Type type){
super(parentID, parentFragment);
this.status=statusForContent;
this.type=type;
this.attachmentCount=statusForContent.mediaAttachments.size();
if(TextUtils.isEmpty(title)){
parsedTitle=HtmlParser.parseCustomEmoji(statusForContent.spoilerText, statusForContent.emojis);
emojiHelper=new CustomEmojiHelper();
@@ -62,12 +64,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<SpoilerStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView title, action;
private final View button;
private final ImageView mediaIcon;
public Holder(Context context, ViewGroup parent, Type type){
super(context, R.layout.display_item_spoiler, parent);
title=findViewById(R.id.spoiler_title);
action=findViewById(R.id.spoiler_action);
button=findViewById(R.id.spoiler_button);
mediaIcon=findViewById(R.id.media_icon);
button.setOutlineProvider(OutlineProviders.roundedRect(8));
button.setClipToOutline(true);
@@ -94,6 +98,10 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
itemView.getPaddingRight(),
item.inset ? itemView.getPaddingTop() : 0
);
mediaIcon.setVisibility(item.attachmentCount > 0 ? View.VISIBLE : View.GONE);
mediaIcon.setImageResource(item.attachmentCount > 1
? R.drawable.ic_fluent_image_multiple_24_regular
: R.drawable.ic_fluent_image_24_regular);
}
@Override

View File

@@ -54,7 +54,7 @@ import me.grishka.appkit.views.UsableRecyclerView;
public abstract class StatusDisplayItem{
public final String parentID;
public final BaseStatusListFragment<?> parentFragment;
public boolean inset, insetPadding=true;
public boolean inset;
public int index;
public boolean
hasDescendantNeighbor = false,
@@ -135,7 +135,7 @@ public abstract class StatusDisplayItem{
: fragment.getString(R.string.in_reply_to, account.displayName);
return new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText, status
);
}
@@ -167,7 +167,7 @@ public abstract class StatusDisplayItem{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, fullText));
}, fullText, status));
} else if (!(status.tags.isEmpty() ||
fragment instanceof HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment
@@ -180,10 +180,8 @@ public abstract class StatusDisplayItem{
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, hashtag.name, List.of(),
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
i -> {
args.putString("hashtag", hashtag.name);
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
}
i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag),
status
)));
}
@@ -206,11 +204,12 @@ public abstract class StatusDisplayItem{
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, null, scheduledStatus));
}
boolean filtered=false;
LegacyFilter applyingFilter=null;
if(status.filtered!=null){
for(FilterResult filter:status.filtered){
if(filter.filter.isActive()){
filtered=true;
LegacyFilter f=filter.filter;
if(f.isActive() && filterContext != null && f.context.contains(filterContext)){
applyingFilter=f;
break;
}
}
@@ -294,7 +293,7 @@ public abstract class StatusDisplayItem{
footer.hideCounts=hideCounts;
items.add(footer);
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
items.add(new GapStatusDisplayItem(parentID, fragment, status));
}
int i=1;
boolean inset=(flags & FLAG_INSET)!=0;
@@ -317,14 +316,7 @@ public abstract class StatusDisplayItem{
}
}
LegacyFilter applyingFilter = null;
if (!statusForContent.filterRevealed) {
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, FilterAction.WARN);
statusForContent.filterRevealed = predicate.test(status);
applyingFilter = predicate.getApplyingFilter();
}
return statusForContent.filterRevealed ? items :
return applyingFilter==null ? items :
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items, applyingFilter)));
}
@@ -397,10 +389,11 @@ public abstract class StatusDisplayItem{
}
public Optional<StatusDisplayItem> getDisplayItemOffset(int offset){
int nextPos=getAbsoluteAdapterPosition() + offset;
List<StatusDisplayItem> displayItems=item.parentFragment.getDisplayItems();
return displayItems.size() > nextPos
? Optional.of(displayItems.get(nextPos))
int thisPos=displayItems.indexOf(item);
int offsetPos=thisPos + offset;
return displayItems.size() > offsetPos && thisPos >= 0 && offsetPos >= 0
? Optional.of(displayItems.get(offsetPos))
: Optional.empty();
}

View File

@@ -3,40 +3,28 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.github.bottomSoftwareFoundation.bottom.Bottom;
import com.github.bottomSoftwareFoundation.bottom.TranslationError;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.TranslatedStatus;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.utils.StatusTextEncoder;
import java.util.Locale;
import java.util.regex.Pattern;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.MovieDrawable;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -46,26 +34,19 @@ import me.grishka.appkit.utils.V;
public class TextStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private CharSequence translatedText;
private CustomEmojiHelper translationEmojiHelper=new CustomEmojiHelper();
public boolean textSelectable;
public boolean reduceTopPadding;
public boolean disableTranslate;
public final Status status;
public boolean disableTranslate, translationShown;
private AccountSession session;
public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48");
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
super(parentID, parentFragment);
this.text=text;
this.status=status;
this.disableTranslate=disableTranslate;
this.translationShown=status.translationShown;
emojiHelper.setText(text);
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
}
public void setTranslationShown(boolean translationShown) {
this.translationShown = translationShown;
status.translationShown = translationShown;
}
@Override
@@ -75,38 +56,47 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public int getImageCount(){
return emojiHelper.getImageCount();
return getCurrentEmojiHelper().getImageCount();
}
@Override
public ImageLoaderRequest getImageRequest(int index){
return emojiHelper.getImageRequest(index);
return getCurrentEmojiHelper().getImageRequest(index);
}
public void setTranslatedText(String text){
Status statusForContent=status.getContentStatus();
translatedText=HtmlParser.parse(text, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, parentFragment.getAccountID());
translationEmojiHelper.setText(translatedText);
}
private CustomEmojiHelper getCurrentEmojiHelper(){
return status.translationState==Status.TranslationState.SHOWN ? translationEmojiHelper : emojiHelper;
}
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
private final LinkedTextView text;
private final TextView translateInfo, readMore;
private final View textWrap, translateWrap, translateProgress;
private final Button translateButton;
private final ScrollView textScrollView;
private final ViewStub translationFooterStub;
private View translationFooter, translationButtonWrap;
private TextView translationInfo;
private Button translationButton;
private ProgressBar translationProgress;
private final float textMaxHeight, textCollapsedHeight;
private final float textMaxHeight;
private final LinearLayout.LayoutParams collapseParams, wrapParams;
private final ViewGroup parent;
private final TextView readMore;
private final ScrollView textScrollView;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_text, parent);
this.parent=parent;
text=findViewById(R.id.text);
textWrap = (LinearLayout) itemView;
translateWrap=findViewById(R.id.translate_wrap);
translateButton=findViewById(R.id.translate_btn);
translateInfo=findViewById(R.id.translate_info);
translateProgress=findViewById(R.id.translate_progress);
translationFooterStub=findViewById(R.id.translation_info);
textScrollView=findViewById(R.id.text_scroll_view);
readMore=findViewById(R.id.read_more);
textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height);
textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height);
float textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height);
collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight);
wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
@@ -114,83 +104,21 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(TextStatusDisplayItem item){
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
text.setText(item.translationShown
? HtmlParser.parse(item.status.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID())
: item.text);
text.setTextIsSelectable(item.textSelectable);
if (item.textSelectable) {
textScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if(item.status.translationState==Status.TranslationState.SHOWN){
if(item.translatedText==null){
item.setTranslatedText(item.status.translation.content);
}
text.setText(item.translatedText);
}else{
text.setText(item.text);
}
text.setTextIsSelectable(false);
if(item.textSelectable) itemView.post(() -> text.setTextIsSelectable(true));
text.setInvalidateOnEveryFrame(false);
itemView.setClickable(false);
itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom());
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
boolean translateEnabled = !item.disableTranslate && instanceInfo != null &&
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
instanceInfo.v2.configuration.translation.enabled;
String bottomText = null;
try {
bottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find()
? new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN)
: null;
} catch (TranslationError ignored) {}
boolean translateVisible = (bottomText != null || (
translateEnabled &&
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
item.status.language != null &&
// todo: compare to mastodon locale instead (how do i query that?!)
!item.status.language.equalsIgnoreCase(Locale.getDefault().getLanguage())))
&& (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable);
translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
translateButton.setText(item.translationShown ? R.string.sk_translate_show_original : R.string.sk_translate_post);
translateInfo.setText(item.translationShown ? itemView.getResources().getString(R.string.sk_translated_using, bottomText != null ? "bottom-java" : item.status.translation.provider) : "");
String finalBottomText = bottomText;
translateButton.setOnClickListener(v->{
if (item.status.translation == null) {
if (finalBottomText != null) {
try {
item.status.translation = new TranslatedStatus();
item.status.translation.content = finalBottomText;
item.setTranslationShown(true);
} catch (TranslationError err) {
item.status.translation = null;
Toast.makeText(itemView.getContext(), err.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
}
rebind();
return;
}
translateProgress.setVisibility(View.VISIBLE);
translateButton.setClickable(false);
translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
new TranslateStatus(item.status.id).setCallback(new Callback<>() {
@Override
public void onSuccess(TranslatedStatus translatedStatus) {
item.status.translation = translatedStatus;
item.setTranslationShown(true);
if (item.parentFragment.getActivity() == null) return;
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
rebind();
}
@Override
public void onError(ErrorResponse error) {
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
error.showToast(itemView.getContext());
}
}).exec(item.parentFragment.getAccountID());
} else {
item.setTranslationShown(!item.translationShown);
rebind();
}
});
updateTranslation(false);
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
@@ -224,18 +152,19 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
boolean expandable = tooBig && !hasSpoiler;
boolean expandable = tooBig && !item.status.hasSpoiler();
item.parentFragment.onEnableExpandable(Holder.this, expandable);
}
boolean expandButtonShown=item.status.textExpandable && !item.status.textExpanded;
translateWrap.setPadding(0, V.dp(expandButtonShown ? 0 : 4), 0, 0);
if(translationFooter!=null)
translationFooter.setPadding(0, V.dp(expandButtonShown ? 0 : 4), 0, 0);
readMore.setVisibility(expandButtonShown ? View.VISIBLE : View.GONE);
textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams);
// compensate for spoiler's bottom margin
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, item.inset && hasSpoiler ? V.dp(-16) : 0,
params.setMargins(params.leftMargin, item.inset && item.status.hasSpoiler() ? V.dp(-16) : 0,
params.rightMargin, params.bottomMargin);
}
@@ -259,5 +188,61 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
private CustomEmojiHelper getEmojiHelper(){
return item.emojiHelper;
}
public void updateTranslation(boolean updateText){
if(item.status==null)
return;
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession());
if(translationFooter==null && translateEnabled){
translationFooter=translationFooterStub.inflate();
translationInfo=findViewById(R.id.translation_info_text);
translationButton=findViewById(R.id.translation_btn);
translationButtonWrap=findViewById(R.id.translation_btn_wrap);
translationProgress=findViewById(R.id.translation_progress);
translationButton.setOnClickListener(v->item.parentFragment.togglePostTranslation(item.status, item.parentID));
}
if(translationButton!=null) translationButton.animate().cancel();
if(item.status.translationState==Status.TranslationState.HIDDEN){
if(updateText) text.setText(item.text);
if(translationFooter==null) return;
translationFooter.setVisibility(translateEnabled ? View.VISIBLE : View.GONE);
translationProgress.setVisibility(View.GONE);
Translation existingTrans=item.status.getContentStatus().translation;
String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null;
String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language;
String displayLang=Locale.forLanguageTag(lang != null ? lang
: AccountSessionManager.get(item.parentFragment.getAccountID()).preferences.postingDefaultLanguage).getDisplayLanguage();
translationButton.setText(item.parentFragment.getString(R.string.translate_post, !displayLang.isBlank() ? displayLang : lang));
translationButton.setClickable(true);
translationButton.animate().alpha(1).setDuration(100).start();
translationInfo.setVisibility(View.GONE);
UiUtils.beginLayoutTransition((ViewGroup) translationButtonWrap);
}else{
translationFooter.setVisibility(View.VISIBLE);
if(item.status.translationState==Status.TranslationState.SHOWN){
translationProgress.setVisibility(View.GONE);
translationButton.setText(R.string.translation_show_original);
translationButton.setClickable(true);
translationButton.animate().alpha(1).setDuration(200).start();
translationInfo.setVisibility(View.VISIBLE);
translationButton.setVisibility(View.VISIBLE);
String displayLang=Locale.forLanguageTag(item.status.translation.detectedSourceLanguage).getDisplayLanguage();
translationInfo.setText(translationInfo.getContext().getString(R.string.post_translated, !displayLang.isBlank() ? displayLang : item.status.translation.detectedSourceLanguage, item.status.translation.provider));
UiUtils.beginLayoutTransition((ViewGroup) translationButtonWrap);
if(updateText){
if(item.translatedText==null){
item.setTranslatedText(item.status.translation.content);
}
text.setText(item.translatedText);
}
}else{ // LOADING
translationProgress.setVisibility(View.VISIBLE);
translationButton.setClickable(false);
translationButton.animate().alpha(UiUtils.ALPHA_PRESSED).setStartDelay(50).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
translationInfo.setVisibility(View.INVISIBLE);
UiUtils.beginLayoutTransition((ViewGroup) translationButton.getParent());
}
}
}
}
}

View File

@@ -31,13 +31,11 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<WarningFilteredStatusDisplayItem>{
public final View warningWrap;
public final TextView text;
public List<StatusDisplayItem> filteredItems;
public Holder(Context context, ViewGroup parent) {
super(context, R.layout.display_item_filter_warning, parent);
warningWrap=findViewById(R.id.warning_wrap);
text=findViewById(R.id.text);
}
@@ -45,11 +43,7 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public void onBind(WarningFilteredStatusDisplayItem item) {
filteredItems = item.filteredItems;
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.applyingFilter.title));
}
@Override
public void onClick() {
item.parentFragment.onWarningClick(this);
itemView.setOnClickListener(v->item.parentFragment.onWarningClick(this));
}
}
}

View File

@@ -66,6 +66,10 @@ public class BlurhashCrossfadeDrawable extends Drawable{
public void setImageDrawable(Drawable imageDrawable){
this.imageDrawable=imageDrawable;
if(imageDrawable!=null){
width=imageDrawable.getIntrinsicWidth();
height=imageDrawable.getIntrinsicHeight();
}
invalidateSelf();
}

View File

@@ -23,9 +23,9 @@ import me.grishka.appkit.utils.V;
public class SawtoothTearDrawable extends Drawable{
private final Paint topPaint, bottomPaint;
private static final int TOP_SAWTOOTH_HEIGHT=5;
private static final int BOTTOM_SAWTOOTH_HEIGHT=3;
private static final int STROKE_WIDTH=2;
private static final int TOP_SAWTOOTH_HEIGHT=4;
private static final int BOTTOM_SAWTOOTH_HEIGHT=4;
private static final int STROKE_WIDTH=1;
private static final int SAWTOOTH_PERIOD=14;
public SawtoothTearDrawable(Context context){

View File

@@ -734,9 +734,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
public void onBind(Attachment item){
super.onBind(item);
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) imageView.getLayoutParams();
params.width=item.getWidth();
params.height=item.getHeight();
ViewImageLoader.load(this, listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()), new UrlImageLoaderRequest(item.url), false);
Drawable currentDrawable=listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition());
if(item.hasKnownDimensions()){
params.width=item.getWidth();
params.height=item.getHeight();
}else if(currentDrawable!=null){
params.width=currentDrawable.getIntrinsicWidth();
params.height=currentDrawable.getIntrinsicHeight();
}else{
params.width=1920;
params.height=1080;
}
ViewImageLoader.load(this, currentDrawable, new UrlImageLoaderRequest(item.url), false);
}
@Override
@@ -778,9 +787,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
super.onBind(item);
playerReady=false;
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) wrap.getLayoutParams();
params.width=item.getWidth();
params.height=item.getHeight();
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
Drawable currentDrawable=listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition());
if(item.hasKnownDimensions()){
params.width=item.getWidth();
params.height=item.getHeight();
}else if(currentDrawable!=null){
params.width=currentDrawable.getIntrinsicWidth();
params.height=currentDrawable.getIntrinsicHeight();
}else{
params.width=1920;
params.height=1080;
}
wrap.setBackground(currentDrawable);
progressBar.setVisibility(item.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
if(itemView.isAttachedToWindow()){
reset();
@@ -841,7 +859,9 @@ public class PhotoViewer implements ZoomPanView.Listener{
@Override
public boolean onError(MediaPlayer mp, int what, int extra){
Log.e(TAG, "video player onError() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
return false;
Toast.makeText(activity, R.string.error_playing_video, Toast.LENGTH_SHORT).show();
onStartSwipeToDismissTransition(0f);
return true;
}
public void prepareAndStartPlayer(){
@@ -862,6 +882,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
player.prepareAsync();
}catch(IOException x){
Log.w(TAG, "Error initializing gif player", x);
Toast.makeText(activity, R.string.error_playing_video, Toast.LENGTH_SHORT).show();
onStartSwipeToDismissTransition(0f);
}
}
@@ -878,6 +900,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height){
if(width<=0 || height<=0)
return;
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) wrap.getLayoutParams();
params.width=width;
params.height=height;

View File

@@ -119,6 +119,11 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
int width=right-left;
int height=bottom-top;
if(width==0 || height==0 || child.getWidth()==0 || child.getWidth()==0){
matrix.reset();
return;
}
float scale=Math.min(width/(float)child.getWidth(), height/(float)child.getHeight());
minScale=scale;
maxScale=Math.max(3f, height/(float)child.getHeight());
@@ -306,8 +311,6 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
}, 1f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_ALPHA));
}
}else{
if(animatingTransition)
Log.w(TAG, "updateViewTransform: ", new Throwable().fillInStackTrace());
child.setScaleX(matrixValues[Matrix.MSCALE_X]);
child.setScaleY(matrixValues[Matrix.MSCALE_Y]);
child.setTranslationX(matrixValues[Matrix.MTRANS_X]);

View File

@@ -1715,7 +1715,9 @@ public class TabLayout extends HorizontalScrollView implements CustomViewHelper{
child.getLayoutParams().height);
int childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}

View File

@@ -100,9 +100,10 @@ public class HtmlParser{
}
}
Map<String, String> idsByUrl=mentions.stream().collect(Collectors.toMap(m->m.url, m->m.id));
Map<String, String> idsByUrl=mentions.stream().distinct().collect(Collectors.toMap(m->m.url, m->m.id));
// Hashtags in remote posts have remote URLs, these have local URLs so they don't match.
// Map<String, String> tagsByUrl=tags.stream().collect(Collectors.toMap(t->t.url, t->t.name));
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
final SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
@@ -115,6 +116,7 @@ public class HtmlParser{
}else if(node instanceof Element el){
switch(el.nodeName()){
case "a" -> {
Object linkObject=null;
String href=el.attr("href");
LinkSpan.Type linkType;
String text=el.text();
@@ -122,6 +124,7 @@ public class HtmlParser{
if(text.startsWith("#")){
linkType=LinkSpan.Type.HASHTAG;
href=text.substring(1);
linkObject=tagsByTag.get(text.substring(1).toLowerCase());
}else{
linkType=LinkSpan.Type.URL;
}
@@ -136,7 +139,7 @@ public class HtmlParser{
}else{
linkType=LinkSpan.Type.URL;
}
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, linkObject, text), ssb.length(), el));
}
case "br" -> ssb.append('\n');
case "span" -> {
@@ -271,7 +274,7 @@ public class HtmlParser{
String url=matcher.group(3);
if(TextUtils.isEmpty(matcher.group(4)))
url="http://"+url;
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, null, null), matcher.start(3), matcher.end(3), 0);
}while(matcher.find()); // Find more URLs
return ssb;
}

View File

@@ -5,6 +5,7 @@ import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.view.View;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.utils.UiUtils;
public class LinkSpan extends CharacterStyle {
@@ -14,17 +15,15 @@ public class LinkSpan extends CharacterStyle {
private String link;
private Type type;
private String accountID;
private Object linkObject;
private String text;
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
this(link, listener, type, accountID, null);
}
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, Object linkObject, String text){
this.listener=listener;
this.link=link;
this.type=type;
this.accountID=accountID;
this.linkObject=linkObject;
this.text=text;
}
@@ -42,7 +41,12 @@ public class LinkSpan extends CharacterStyle {
switch(getType()){
case URL -> UiUtils.openURL(context, accountID, link);
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link, null);
case HASHTAG -> {
if(linkObject instanceof Hashtag ht)
UiUtils.openHashtagTimeline(context, accountID, ht);
else
UiUtils.openHashtagTimeline(context, accountID, text);
}
case CUSTOM -> listener.onLinkClick(this);
}
}

View File

@@ -1,8 +1,8 @@
package org.joinmastodon.android.ui.utils;
import static org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference.*;
import android.content.Context;
import android.content.res.Resources;
@@ -11,20 +11,21 @@ import androidx.annotation.StyleRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import java.util.Map;
public class ColorPalette {
public static final Map<GlobalUserPreferences.ColorPreference, ColorPalette> palettes = Map.of(
ColorPreference.MATERIAL3, new ColorPalette(R.style.ColorPalette_Material3)
public static final Map<AccountLocalPreferences.ColorPreference, ColorPalette> palettes = Map.of(
MATERIAL3, new ColorPalette(R.style.ColorPalette_Material3)
.dark(R.style.ColorPalette_Material3_Dark, R.style.ColorPalette_Material3_AutoLightDark),
ColorPreference.PINK, new ColorPalette(R.style.ColorPalette_Pink),
ColorPreference.PURPLE, new ColorPalette(R.style.ColorPalette_Purple),
ColorPreference.GREEN, new ColorPalette(R.style.ColorPalette_Green),
ColorPreference.BLUE, new ColorPalette(R.style.ColorPalette_Blue),
ColorPreference.BROWN, new ColorPalette(R.style.ColorPalette_Brown),
ColorPreference.RED, new ColorPalette(R.style.ColorPalette_Red),
ColorPreference.YELLOW, new ColorPalette(R.style.ColorPalette_Yellow)
PINK, new ColorPalette(R.style.ColorPalette_Pink),
PURPLE, new ColorPalette(R.style.ColorPalette_Purple),
GREEN, new ColorPalette(R.style.ColorPalette_Green),
BLUE, new ColorPalette(R.style.ColorPalette_Blue),
BROWN, new ColorPalette(R.style.ColorPalette_Brown),
RED, new ColorPalette(R.style.ColorPalette_Red),
YELLOW, new ColorPalette(R.style.ColorPalette_Yellow)
);
private @StyleRes int base;

View File

@@ -10,6 +10,7 @@ import android.widget.TextView;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.TimelineDefinition;
import java.util.EnumSet;
@@ -44,8 +45,8 @@ public class DiscoverInfoBannerHelper{
banner=((Activity)list.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
TextView text=banner.findViewById(R.id.banner_text);
text.setText(switch(type){
case TRENDING_POSTS -> list.getResources().getString(R.string.trending_posts_info_banner);
case TRENDING_LINKS -> list.getResources().getString(R.string.trending_links_info_banner);
case TRENDING_POSTS -> list.getResources().getString(R.string.sk_trending_posts_info_banner);
case TRENDING_LINKS -> list.getResources().getString(R.string.sk_trending_links_info_banner);
case FEDERATED_TIMELINE -> list.getResources().getString(R.string.sk_federated_timeline_info_banner);
case POST_NOTIFICATIONS -> list.getResources().getString(R.string.sk_notify_posts_info_banner);
case BUBBLE_TIMELINE -> list.getResources().getString(R.string.sk_bubble_timeline_info_banner);
@@ -57,8 +58,10 @@ public class DiscoverInfoBannerHelper{
case TRENDING_POSTS -> R.drawable.ic_fluent_arrow_trending_24_regular;
case TRENDING_LINKS -> R.drawable.ic_fluent_news_24_regular;
case ACCOUNTS -> R.drawable.ic_fluent_people_add_24_regular;
// no icon because those are displayed as timelines - with icon in top left
case LOCAL_TIMELINE, FEDERATED_TIMELINE, BUBBLE_TIMELINE, POST_NOTIFICATIONS -> 0;
case LOCAL_TIMELINE -> TimelineDefinition.LOCAL_TIMELINE.getDefaultIcon().iconRes;
case FEDERATED_TIMELINE -> TimelineDefinition.FEDERATED_TIMELINE.getDefaultIcon().iconRes;
case BUBBLE_TIMELINE -> TimelineDefinition.BUBBLE_TIMELINE.getDefaultIcon().iconRes;
case POST_NOTIFICATIONS -> TimelineDefinition.POSTS_TIMELINE.getDefaultIcon().iconRes;
});
adapter.addAdapter(new SingleViewRecyclerAdapter(banner));
}

View File

@@ -74,24 +74,19 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
// List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
boolean inset=sdi.getItem().inset;
int pos=holder.getAbsoluteAdapterPosition();
// int pos=holder.getAbsoluteAdapterPosition();
if(inset){
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
int pad;
// boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
// boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
// if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
pad=V.dp(16);
// else
// pad=V.dp(12);
boolean insetPadding=((StatusDisplayItem.Holder<?>) holder).getItem().insetPadding;
if(insetPadding)
outRect.left=pad;
if(insetPadding)
outRect.right=pad;
int pad=V.dp(16);
// else pad=V.dp(12);
outRect.left=pad;
outRect.right=pad;
// had to comment this out because animations with offsets aren't handled properly.
// can be worked around by manually applying top margins to items

View File

@@ -28,6 +28,7 @@ public class MediaAttachmentViewController{
private final Context context;
private boolean didClear;
private Status status;
private Attachment attachment;
public MediaAttachmentViewController(Context context, MediaGridStatusDisplayItem.GridItemType type){
view=context.getSystemService(LayoutInflater.class).inflate(switch(type){
@@ -54,6 +55,7 @@ public class MediaAttachmentViewController{
public void bind(Attachment attachment, Status status){
this.status=status;
this.attachment=attachment;
crossfadeDrawable.setSize(attachment.getWidth(), attachment.getHeight());
crossfadeDrawable.setBlurhashDrawable(attachment.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
@@ -76,6 +78,11 @@ public class MediaAttachmentViewController{
crossfadeDrawable.setImageDrawable(drawable);
if(didClear)
crossfadeDrawable.animateAlpha(0f);
// Make sure the image is not stretched if the server returned wrong dimensions
if(drawable!=null && (drawable.getIntrinsicWidth()!=attachment.getWidth() || drawable.getIntrinsicHeight()!=attachment.getHeight())){
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
}
}
public void clearImage(){

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.ui.utils;
import static android.view.Menu.NONE;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference.*;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
@@ -53,6 +54,7 @@ import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.WindowInsets;
import android.webkit.MimeTypeMap;
import android.widget.Button;
@@ -83,6 +85,7 @@ import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
@@ -96,13 +99,13 @@ import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerAboutFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
@@ -173,6 +176,8 @@ public class UiUtils {
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
public static int MAX_WIDTH, SCROLL_TO_TOP_DELTA;
public static final float ALPHA_PRESSED=0.55f;
private UiUtils() {
}
@@ -192,28 +197,42 @@ public class UiUtils {
}
public static String formatRelativeTimestamp(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
return formatPeriodBetween(context, instant, null);
}
public static String formatPeriodBetween(Context context, Instant since, Instant until) {
boolean ago = until == null;
long t = since.toEpochMilli();
long now = ago ? System.currentTimeMillis() : until.toEpochMilli();
long diff = now - t;
if(diff<1000L){
return context.getString(R.string.time_now);
}else if(diff<60_000L){
return context.getString(R.string.time_seconds_ago_short, diff/1000L);
long time = diff/1000L;
return ago ?
context.getString(R.string.time_seconds_ago_short, time) :
context.getResources().getQuantityString(R.plurals.sk_time_seconds, (int) time, time);
}else if(diff<3600_000L){
return context.getString(R.string.time_minutes_ago_short, diff/60_000L);
long time = diff/60_000L;
return ago ?
context.getString(R.string.time_minutes_ago_short, time) :
context.getResources().getQuantityString(R.plurals.sk_time_minutes, (int) time, time);
}else if(diff<3600_000L*24L){
return context.getString(R.string.time_hours_ago_short, diff/3600_000L);
long time = diff/3600_000L;
return ago ?
context.getString(R.string.time_hours_ago_short, time) :
context.getResources().getQuantityString(R.plurals.sk_time_hours, (int) time, time);
} else {
int days = (int) (diff / (3600_000L * 24L));
if (days > 30) {
ZonedDateTime dt = instant.atZone(ZoneId.systemDefault());
if (ago && days > 30) {
ZonedDateTime dt = since.atZone(ZoneId.systemDefault());
if (dt.getYear() == ZonedDateTime.now().getYear()) {
return DATE_FORMATTER_SHORT.format(dt);
} else {
return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
}
}
return context.getString(R.string.time_days_ago_short, days);
return ago ? context.getString(R.string.time_days_ago_short, days) : context.getResources().getQuantityString(R.plurals.sk_time_days, days, days);
}
}
@@ -426,12 +445,18 @@ public class UiUtils {
Nav.go((Activity) context, ProfileFragment.class, args);
}
public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following) {
Bundle args = new Bundle();
public static void openHashtagTimeline(Context context, String accountID, Hashtag hashtag){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("hashtag", hashtag);
if (following != null) args.putBoolean("following", following);
Nav.go((Activity) context, HashtagTimelineFragment.class, args);
args.putParcelable("hashtag", Parcels.wrap(hashtag));
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
}
public static void openHashtagTimeline(Context context, String accountID, String hashtag){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("hashtagName", hashtag);
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed) {
@@ -938,14 +963,20 @@ public class UiUtils {
}
public static void setUserPreferredTheme(Context context) {
context.setTheme(switch (theme) {
setUserPreferredTheme(context, null);
}
public static void setUserPreferredTheme(Context context, @Nullable AccountSession session) {
context.setTheme(switch(theme) {
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> R.style.Theme_Mastodon_Dark;
default -> R.style.Theme_Mastodon_AutoLightDark;
});
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context);
AccountLocalPreferences prefs=session != null ? session.getLocalPreferences() : null;
AccountLocalPreferences.ColorPreference color=prefs != null ? prefs.color : AccountLocalPreferences.ColorPreference.MATERIAL3;
ColorPalette palette = ColorPalette.palettes.get(color);
if (palette != null) palette.apply(context, theme);
Resources res = context.getResources();
MAX_WIDTH = (int) res.getDimension(R.dimen.layout_max_width);
@@ -962,9 +993,9 @@ public class UiUtils {
}
public static boolean isDarkTheme() {
if (theme == GlobalUserPreferences.ThemePreference.AUTO)
if (theme == AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
return theme == GlobalUserPreferences.ThemePreference.DARK;
return theme == DARK;
}
public static Optional<Pair<String, Optional<String>>> parseFediverseHandle(String maybeFediHandle) {
@@ -1138,7 +1169,7 @@ public class UiUtils {
return Optional.empty();
}
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
return Optional.of(new GetSearchResults(query.getQuery(), type, true, null, 0, 0).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
@@ -1235,7 +1266,7 @@ public class UiUtils {
}
public static MastodonAPIRequest<SearchResults> lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true, null, 0, 0)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1298,7 +1329,7 @@ public class UiUtils {
})
.execNoAuth(uri.getHost()));
} else if (looksLikeFediverseUrl(url)) {
return Optional.of(new GetSearchResults(url, null, true)
return Optional.of(new GetSearchResults(url, null, true, null, 0, 0)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1705,4 +1736,16 @@ public class UiUtils {
.filter(Objects::nonNull)
.findFirst();
}
public static void opacityIn(View v){
v.animate().alpha(1).setDuration(400).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
}
public static void opacityOut(View v){
opacityOut(v, ALPHA_PRESSED).start();
}
public static ViewPropertyAnimator opacityOut(View v, float alpha){
return v.animate().alpha(alpha).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT);
}
}

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