Compare commits

...

861 Commits

Author SHA1 Message Date
sk
92beac8dff fuck it, indented header line
closes #448
2023-03-14 15:34:05 +01:00
sk
ed1fdba9a5 Merge remote-tracking branch 'upstream/master' 2023-03-14 15:01:30 +01:00
sk
5e194e3079 display both reply and reblog
re: sk22#448
2023-03-14 14:59:11 +01:00
sk
27c2791d6c add reblogged account to known accounts 2023-03-14 14:58:21 +01:00
Grishka
d6bcc9c156 Fix button color 2023-03-14 07:31:01 +03:00
sk
4c85fd4387 use custom string for anonymous reply 2023-03-13 20:06:05 +01:00
sk
80d529d503 display reply header for unknown original poster
re: mastodon#342
2023-03-13 20:00:56 +01:00
sk
c5a19a2334 fix duplicate notification status header 2023-03-13 19:29:40 +01:00
sk
16857bebd9 add tooltip
closes sk22#436
2023-03-13 19:05:22 +01:00
sk
1c340b7c66 fix extra text padding in compose 2023-03-13 18:56:06 +01:00
sk
7d9d8f0aae Merge remote-tracking branch 'upstream/master' 2023-03-13 18:52:52 +01:00
sk
21fc35230c Merge remote-tracking branch 'upstream/master' 2023-03-13 18:51:26 +01:00
Grishka
fc67c82040 Fix #544 2023-03-13 20:46:29 +03:00
FineFindus
4d04741fe0 feat(welcome): use URI InputType (#454) 2023-03-11 13:01:11 +01:00
FineFindus
5c7fe9dcb5 fix: disable group divider on EMUI (#453)
Fixes an issues, where the forth menu item does not show up, when the divider is enabled on EMUI devices
2023-03-11 13:00:36 +01:00
Grishka
c3aa3af650 Fix #540 2023-03-08 22:46:24 +03:00
Grishka
4a695b2a83 Use a single display item for the image attachment grid 2023-03-06 02:25:13 +03:00
Grishka
a8ba50e762 Merge branch 'dev_clickable_links_hold_to_copy' 2023-03-05 22:33:35 +03:00
Grishka
f79fc66578 Fix 2023-03-05 22:33:18 +03:00
Torge Rosendahl
4144639b75 docu 2023-02-26 13:59:39 -05:00
sk22
7ea42c8403 Translated using Weblate (German)
Currently translated at 100.0% (16 of 16 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2023-02-17 12:30:41 +00:00
sk22
af9b527f35 Translated using Weblate (German)
Currently translated at 100.0% (262 of 262 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-02-17 12:30:40 +00:00
Espasant3
df58cdd86e Translated using Weblate (Galician)
Currently translated at 100.0% (15 of 15 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-02-17 12:27:57 +00:00
ihor_ck
507fcea646 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (15 of 15 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-02-17 12:27:57 +00:00
McKris
5cd1e88da9 Translated using Weblate (Polish)
Currently translated at 100.0% (15 of 15 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2023-02-17 12:27:57 +00:00
AiOO
e2293899f0 Translated using Weblate (Korean)
Currently translated at 100.0% (15 of 15 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2023-02-17 12:27:57 +00:00
gallegonovato
1c743ee3a6 Translated using Weblate (Spanish)
Currently translated at 100.0% (15 of 15 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-02-17 12:27:57 +00:00
ihor_ck
daf4c69df4 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-02-17 12:27:57 +00:00
McKris
14d3add7b3 Translated using Weblate (Polish)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-02-17 12:27:57 +00:00
Espasant3
7e3193a708 Translated using Weblate (Galician)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-02-17 12:27:57 +00:00
gallegonovato
545aa16cd3 Translated using Weblate (Spanish)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-02-17 12:27:57 +00:00
poesty
4dcf32d13a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-17 12:27:57 +00:00
AiOO
e0aba23e80 Translated using Weblate (Korean)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-02-17 12:27:57 +00:00
sk
b19ae9bb10 bump version 2023-02-17 13:27:41 +01:00
sk22
d20f8669e8 Auto-hide FAB on scroll (#435)
* feat(composeButton): hide fab on scroll
* feat(composeButton): hide when scrolling in profile fragment
* refactor(compose-fab): show fab after small scroll distance
* refactor(compose-fab): code cleanup
* feat(composeButton): hide when scrolling in profile
* fix: duplicate fab var
* feat(fab): show when scrolled to top
* add option to turn it off

---------

Co-authored-by: FineFindus <63370021+FineFindus@users.noreply.github.com>
2023-02-17 13:20:22 +01:00
sk
1567e5aba4 Merge remote-tracking branch 'upstream/master' 2023-02-17 12:45:36 +01:00
sk
6b9b6710cf enable remote-following accounts
closes sk22#431
2023-02-16 19:44:39 +01:00
sk
b07858a66d don't crash when language array empty 2023-02-16 17:04:56 +01:00
sk
c05d0b600e default role color if not provided
fixes sk22#430
2023-02-16 16:42:47 +01:00
Torge Rosendahl
0a8d73dc0b cleanup, resolved some warnings 2023-02-15 20:19:10 -05:00
Torge Rosendahl
fd99f3caa1 changed url longclick implementation to GestureListener 2023-02-15 20:16:20 -05:00
Torge Rosendahl
794c4e5227 removed longClickHandler and moved to view itself 2023-02-15 19:54:08 -05:00
Torge Rosendahl
f5df8225d1 whitespace corrections 2023-02-15 18:20:02 -05:00
Torge Rosendahl
42c6446125 refactoring: moved runnable and made it private, added copy toast localization. 2023-02-15 18:04:52 -05:00
Torge Rosendahl
e3486ebf7c added clickable link type switch for copy, to not copy hashtags and user IDs 2023-02-15 18:00:45 -05:00
Torge Rosendahl
c0115f068c implemented copy service 2023-02-15 17:40:43 -05:00
Torge Rosendahl
41682d1147 added press-and-hold listener to ClickableLinks 2023-02-15 17:30:31 -05:00
Grishka
dd582c4bee Update locales & bump version 2023-02-14 03:57:49 +03:00
Grishka
3a0d314af0 Merge branch 'l10n_master' 2023-02-14 03:49:36 +03:00
sk
a00ca599c1 add newline 2023-02-13 17:58:27 +01:00
Choukajohn
263b5b10b6 Translated using Weblate (French)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-02-13 16:50:55 +00:00
sk22
1068fa3120 Translated using Weblate (German)
Currently translated at 100.0% (15 of 15 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2023-02-13 16:50:55 +00:00
sk22
569f288c00 Translated using Weblate (German)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-02-13 16:50:54 +00:00
sk
dfd94511a5 bump version 2023-02-13 17:47:39 +01:00
sk
2271f336b0 Merge remote-tracking branch 'upstream/l10n_master' 2023-02-13 17:37:40 +01:00
Espasant3
4486feee76 Translated using Weblate (Galician)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-02-13 16:30:34 +00:00
ca
baaff2573c Translated using Weblate (Catalan)
Currently translated at 97.6% (253 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ca/
2023-02-13 16:30:34 +00:00
sk
b9c3b23757 Merge remote-tracking branch 'weblate/main' 2023-02-13 17:30:04 +01:00
sk
0c1fd22253 Merge remote-tracking branch 'upstream' 2023-02-13 17:26:26 +01:00
sk
466c489b4d get edit image drawable with theme
closes sk22#401
2023-02-13 17:07:27 +01:00
sk
625f715e26 clarify spectator mode name 2023-02-13 16:57:18 +01:00
sk
61caec4060 fix wrong icon
closes sk22#421
2023-02-13 16:53:26 +01:00
sk
df233eb1e2 fix headers not filtered in notifications list 2023-02-13 16:51:55 +01:00
sk
78225c482f fix overlapping text
closes sk22#428
2023-02-13 16:32:52 +01:00
sk
c55703f0ba fix layout inconsistency
closes sk22#427
2023-02-13 16:28:51 +01:00
Eugen Rochko
3b26dd44a0 New translations strings.xml (Arabic) 2023-02-13 09:09:52 +01:00
HudobniVolk
2a1386a87a Translated using Weblate (Slovenian)
Currently translated at 71.4% (10 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-02-12 16:56:42 +00:00
AiOO
fb32430f96 Translated using Weblate (Korean)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2023-02-12 16:56:42 +00:00
ihor_ck
2288c53adc Translated using Weblate (Ukrainian)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-02-12 16:56:41 +00:00
HudobniVolk
84b7b67045 Translated using Weblate (Slovenian)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-02-12 16:56:41 +00:00
edxkl
d233f039a3 Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.7% (248 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-02-12 16:56:41 +00:00
Choukajohn
8b01955a18 Translated using Weblate (French)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-02-12 16:56:41 +00:00
gallegonovato
427aa1722d Translated using Weblate (Spanish)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-02-12 16:56:41 +00:00
ling0412
2a7eb09998 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-12 16:56:41 +00:00
AiOO
f2f48fce79 Translated using Weblate (Korean)
Currently translated at 100.0% (259 of 259 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-02-12 16:56:41 +00:00
Grishka
634408b8cb Minor onboarding tweaks 2023-02-12 14:25:03 +03:00
Grishka
f050e3f22d Fix #500 2023-02-12 04:03:09 +03:00
Grishka
8e9531b718 Fix #528 2023-02-12 03:58:24 +03:00
HudobniVolk
00457c1edb Translated using Weblate (Slovenian)
Currently translated at 57.1% (8 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-02-11 13:13:09 +00:00
Andrewblasco
aa14986fcc Translated using Weblate (Spanish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-02-11 13:13:09 +00:00
HudobniVolk
1493cd9034 Translated using Weblate (Slovenian)
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-02-11 13:13:09 +00:00
edxkl
b2b295ee5b Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.3% (246 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-02-11 13:13:08 +00:00
Andrewblasco
58847f80fd Translated using Weblate (Spanish)
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-02-11 13:13:08 +00:00
Eyre_S
ed9db7b5fd Translated using Weblate (Chinese (Traditional))
Currently translated at 14.2% (2 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hant/
2023-02-11 13:13:08 +00:00
edxkl
433a7c6b7a Translated using Weblate (Portuguese (Brazil))
Currently translated at 92.8% (13 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2023-02-11 13:13:08 +00:00
Eyre_S
7692f587ef Translated using Weblate (Chinese (Traditional))
Currently translated at 23.2% (60 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-02-11 13:13:08 +00:00
ihor_ck
67583150b2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-02-11 13:13:08 +00:00
McKris
ed49422f76 Translated using Weblate (Polish)
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-02-11 13:13:08 +00:00
Oliebol
63078aaa3e Translated using Weblate (Dutch)
Currently translated at 96.1% (248 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-02-11 13:13:08 +00:00
Espasant3
9246c43ffe Translated using Weblate (Galician)
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-02-11 13:13:08 +00:00
Choukajohn
15f02863c0 Translated using Weblate (French)
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-02-11 13:13:08 +00:00
poesty
be1921879d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-11 13:13:08 +00:00
ling0412
61b43e0112 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (258 of 258 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-11 13:13:08 +00:00
nkufideal
b25b482630 Translated using Weblate (Belarusian)
Currently translated at 7.1% (18 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/be/
2023-02-11 13:13:08 +00:00
AiOO
1d44875a65 Translated using Weblate (Korean)
Currently translated at 98.0% (246 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-02-11 13:13:08 +00:00
edxkl
0dc5004898 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.0% (241 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-02-11 13:13:08 +00:00
Andrewblasco
0df86e315b Translated using Weblate (Spanish)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-02-11 13:13:08 +00:00
tygyh
31d3fa77de Translated using Weblate (Swedish)
Currently translated at 71.4% (10 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sv/
2023-02-11 13:13:08 +00:00
sandboiii
ec74b18c1a Translated using Weblate (Russian)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-02-11 13:13:08 +00:00
poesty
de36c31f45 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-11 13:13:08 +00:00
HudobniVolk
43bcf0008e Translated using Weblate (Slovenian)
Currently translated at 42.8% (6 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-02-11 13:13:08 +00:00
sheepnik
ac64087018 Translated using Weblate (Welsh)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/cy/
2023-02-11 13:13:08 +00:00
sheepnik
af77865a46 Translated using Weblate (Welsh)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/cy/
2023-02-11 13:13:08 +00:00
ling0412
51a4a41147 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2023-02-11 13:13:08 +00:00
HudobniVolk
987474462d Translated using Weblate (Slovenian)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-02-11 13:13:08 +00:00
Oliebol
c34ab79c6c Translated using Weblate (Dutch)
Currently translated at 96.8% (243 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-02-11 13:13:08 +00:00
ling0412
8bac664a34 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-11 13:13:08 +00:00
poesty
d77647c354 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-02-11 13:13:08 +00:00
sheepnik
e8ef6ef2c7 Translated using Weblate (Welsh)
Currently translated at 91.6% (230 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/cy/
2023-02-11 13:13:08 +00:00
ca
a87cf640dd Translated using Weblate (Catalan)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ca/
2023-02-11 13:13:08 +00:00
Daudix_UFO
bcb69f1f47 Translated using Weblate (Russian)
Currently translated at 89.6% (225 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-02-11 13:13:08 +00:00
Mannivu
0bdcc9057b Translated using Weblate (Italian)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-02-11 13:13:08 +00:00
ghose
f92977fddf Translated using Weblate (Galician)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-02-11 13:13:08 +00:00
Tribela
230a59266d Translated using Weblate (Korean)
Currently translated at 93.6% (235 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-02-11 13:13:08 +00:00
ca
d97f3ed5c8 Translated using Weblate (Catalan)
Currently translated at 98.0% (246 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ca/
2023-02-11 13:13:08 +00:00
Linerly
aae2cd2b65 Translated using Weblate (Indonesian)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2023-02-11 13:13:08 +00:00
Linerly
8b8763bffc Translated using Weblate (Indonesian)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-02-11 13:13:08 +00:00
McKris
35758e720d Translated using Weblate (Polish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2023-02-11 13:13:08 +00:00
McKris
fa48c80ab1 Translated using Weblate (Polish)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-02-11 13:13:08 +00:00
ihor_ck
1621dbc67a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-02-11 13:13:08 +00:00
Andrewblasco
4ece7b883f Translated using Weblate (Spanish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-02-11 13:13:07 +00:00
ihor_ck
66a5b749fe Translated using Weblate (Ukrainian)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-02-11 13:13:07 +00:00
Andrewblasco
49a80767a7 Translated using Weblate (Spanish)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-02-11 13:13:07 +00:00
Choukajohn
7683b464f3 Translated using Weblate (French)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-02-11 13:13:07 +00:00
Grishka
64fbbb2f07 Minor onboarding stuff 2023-02-10 21:09:06 +03:00
Eugen Rochko
9003f690d1 New translations strings.xml (French) 2023-02-10 14:16:02 +01:00
Eugen Rochko
8ffaca61bd New translations strings.xml (Portuguese, Brazilian) 2023-02-08 22:57:10 +01:00
Eugen Rochko
f6c3b10c2b New translations strings.xml (Portuguese, Brazilian) 2023-02-08 20:46:53 +01:00
sk
8933c0647e bump version 2023-02-07 16:03:18 +01:00
sk
333c38c64d add alt badge to video/gifs
closes sk22#409
2023-02-07 15:44:40 +01:00
sk
ca5827e3f8 add null check
closes sk22#414
2023-02-07 15:29:56 +01:00
sk
4884667484 generify fab button
closes sk22#380
2023-02-07 14:10:31 +01:00
sk
a2f687898c fix wrong time in edited notifications
closes sk22#387
2023-02-07 13:34:52 +01:00
sk
defd038064 display roles in profile 2023-02-06 19:41:36 +01:00
sk
f88b65f479 add spectator mode
closes sk22#264
2023-02-06 18:16:41 +01:00
sk
f65d56361f support account filter context 2023-02-06 17:59:06 +01:00
sk
255155b55a fix filters not working in lists
closes sk22#379
2023-02-06 17:52:05 +01:00
sk
ee2e39462a remove broken auto-add user to created list 2023-02-06 17:24:05 +01:00
sk
32b459ae77 hide expand/collapse to screen reader 2023-02-06 16:54:54 +01:00
sk
c1b79da4a7 tweak profile fragment 2023-02-06 15:25:12 +01:00
sk
65dfd8667d realign post header buttons 2023-02-06 15:17:05 +01:00
sk
12558c3c18 handle saving draft when attachment not uploaded
closes sk22#402
2023-02-06 14:31:53 +01:00
sk
dae347a29f fix filtered crash in scheduled posts
closes sk22#408
2023-02-06 14:00:52 +01:00
sk
c51be5f199 add missing margin
closes sk22#407
2023-02-06 13:53:28 +01:00
sk
fc1bd14f70 tweak collapse button 2023-02-06 13:44:57 +01:00
sk
bd39ed3754 fix notifications crashing with collapse button
closes sk22#410
2023-02-06 13:26:56 +01:00
sk
50029c7f73 avoid null pointer when switching tabs
closes sk22#412
2023-02-06 13:16:17 +01:00
sk
e2c907eb10 don't hide collapse button 2023-02-06 11:04:38 +01:00
sk
937747e11b add collapse button to header 2023-02-06 10:42:27 +01:00
Eugen Rochko
d68a3a6ef5 New translations strings.xml (Polish) 2023-02-05 18:37:14 +01:00
Eugen Rochko
4475bd039a New translations strings.xml (Hungarian) 2023-02-05 10:32:04 +01:00
Eugen Rochko
98dc7d0524 New translations strings.xml (Hungarian) 2023-02-05 09:17:45 +01:00
sk
85b6bc79a3 increase max text height 2023-02-04 14:21:14 +01:00
Eugen Rochko
65948030a6 New translations strings.xml (Russian) 2023-02-04 14:07:29 +01:00
Eugen Rochko
89edfaaa6d New translations strings.xml (Russian) 2023-02-04 13:11:55 +01:00
Eugen Rochko
10372804e4 New translations strings.xml (Thai) 2023-02-04 09:10:51 +01:00
sk
ec9d41fbbd collapse long posts 2023-02-03 23:40:20 +01:00
sk
847d966daa only display warning when not already revealed 2023-02-03 23:08:04 +01:00
Eugen Rochko
d1dd7d203b New translations strings.xml (German) 2023-02-03 18:47:42 +01:00
sk22
618840c76a Display filtered posts with a warning (#406)
* copy changes from @LucasGGamerM
* simplify building filter item
* fix adapter ranges
* change filter item styling

closes sk22#209 

Co-authored-by: LucasGGamerM <71328265+LucasGGamerM@users.noreply.github.com>
2023-02-03 18:33:15 +01:00
sk
33d856562d hide compose fab when editing 2023-02-03 16:54:51 +01:00
sk
9873e9ede5 fix text scaling height and margin issues 2023-02-03 16:24:30 +01:00
Eugen Rochko
c6aed0b52e New translations strings.xml (French) 2023-02-03 16:09:58 +01:00
sk
63dad42bf3 auto-orientation counters alignment
closes sk22#381
2023-02-03 15:35:36 +01:00
sk
dad58f8245 hide tab bar when editing profile 2023-02-03 15:30:10 +01:00
sk
647a7d70cd rearrange profile items 2023-02-03 15:24:21 +01:00
sk
f49c7dff00 remove about tab 2023-02-03 15:07:15 +01:00
sk
72f638c96c move metadata to profile 2023-02-03 14:50:06 +01:00
Eugen Rochko
5f902d25a9 New translations strings.xml (Basque) 2023-02-03 12:29:06 +01:00
Eugen Rochko
c43bed665d New translations strings.xml (Catalan) 2023-02-03 12:29:05 +01:00
Eugen Rochko
d70a2ae5b3 New translations strings.xml (Russian) 2023-02-03 10:03:24 +01:00
Eugen Rochko
1fdf36b4d8 New translations strings.xml (Portuguese, Brazilian) 2023-02-03 09:05:37 +01:00
sk
35e0897869 move profile counters down 2023-02-02 15:46:26 +01:00
sk
e22cb07d63 Revert "enable selecting text via alt badge"
This reverts commit 14e639aa8a.
2023-02-02 13:41:49 +01:00
Eugen Rochko
5d1cd0f4f6 New translations strings.xml (Vietnamese) 2023-02-02 07:01:44 +01:00
sk
c2df989217 fix width misalignment in header 2023-02-01 11:05:50 +01:00
sk
31d0bfb434 fix footer item hitbox sizes
closes sk22#389
2023-02-01 10:59:55 +01:00
sk
14e639aa8a enable selecting text via alt badge
re: sk22#400
2023-02-01 10:36:14 +01:00
sk
6c24e06157 change string 2023-02-01 10:30:31 +01:00
sk
53ce4276f6 Merge remote-tracking branch 'origin/main' 2023-02-01 10:25:43 +01:00
sk
423e919e16 fix crash 2023-02-01 10:25:24 +01:00
Eugen Rochko
3505460372 New translations strings.xml (Chinese Traditional) 2023-02-01 03:56:06 +01:00
Eugen Rochko
eeb91e867e New translations strings.xml (Chinese Traditional) 2023-02-01 02:54:48 +01:00
Eugen Rochko
c87062ee31 New translations strings.xml (Slovenian) 2023-01-31 21:33:54 +01:00
Eugen Rochko
fb66fa1c6f New translations strings.xml (Italian) 2023-01-31 20:07:33 +01:00
Eugen Rochko
f80af9f5bf New translations strings.xml (Italian) 2023-01-31 18:30:56 +01:00
Eugen Rochko
85157ffe25 New translations strings.xml (Thai) 2023-01-31 17:27:18 +01:00
Eugen Rochko
59f95159b7 New translations strings.xml (German) 2023-01-31 13:41:46 +01:00
aetsucore
c6cd424f30 Prefix replies with "re:" (#385)
* Prefix replies with "re:"
* Use correct quotation marks
* Avoid repeating "re: " prefix when replying to a post that already has it
2023-01-31 10:30:36 +01:00
Eugen Rochko
703dbd4c8a New translations strings.xml (Portuguese, Brazilian) 2023-01-31 07:47:05 +01:00
sk
e282d54f99 Merge remote-tracking branch 'upstream/master' 2023-01-31 00:00:56 +01:00
sk
29ad08f2ea fix crashes
closes sk22#393
closes sk22#394
2023-01-30 23:46:32 +01:00
Eugen Rochko
1e75f9f1c2 New translations strings.xml (Dutch) 2023-01-30 23:35:25 +01:00
Eugen Rochko
d0860333a9 New translations strings.xml (Dutch) 2023-01-30 22:36:37 +01:00
Eugen Rochko
64b3951c25 New translations strings.xml (German) 2023-01-30 18:52:51 +01:00
Eugen Rochko
e89e6cc3f5 New translations strings.xml (Icelandic) 2023-01-30 11:40:53 +01:00
Eugen Rochko
3c2985fa6e New translations strings.xml (Icelandic) 2023-01-30 10:39:01 +01:00
Eugen Rochko
bee01429f2 New translations full_description.txt (Portuguese, Brazilian) 2023-01-30 05:41:59 +01:00
Eugen Rochko
a96431cc00 New translations strings.xml (Portuguese, Brazilian) 2023-01-30 05:41:58 +01:00
Eugen Rochko
bf9e6f54cf New translations strings.xml (Portuguese, Brazilian) 2023-01-30 04:35:45 +01:00
Eugen Rochko
63084857a3 New translations strings.xml (Kabyle) 2023-01-30 00:02:18 +01:00
Eugen Rochko
d8b7038972 New translations strings.xml (Occitan) 2023-01-30 00:02:18 +01:00
Eugen Rochko
976e71db25 New translations strings.xml (Scottish Gaelic) 2023-01-30 00:02:17 +01:00
Eugen Rochko
2b59c2c080 New translations strings.xml (Sinhala) 2023-01-30 00:02:16 +01:00
Eugen Rochko
5929b0c6b9 New translations strings.xml (Bosnian) 2023-01-30 00:02:15 +01:00
Eugen Rochko
e160a05411 New translations strings.xml (Filipino) 2023-01-30 00:02:14 +01:00
Eugen Rochko
78d8f075a9 New translations strings.xml (Burmese) 2023-01-30 00:02:13 +01:00
Eugen Rochko
3784873cad New translations strings.xml (Hindi) 2023-01-30 00:02:12 +01:00
Eugen Rochko
528f8aaead New translations strings.xml (Croatian) 2023-01-30 00:02:11 +01:00
Eugen Rochko
4ba9f1ecaf New translations strings.xml (Thai) 2023-01-30 00:02:10 +01:00
Eugen Rochko
697a666545 New translations strings.xml (Bengali) 2023-01-30 00:02:09 +01:00
Eugen Rochko
0ee6798424 New translations strings.xml (Persian) 2023-01-30 00:02:08 +01:00
Eugen Rochko
9a95deb346 New translations strings.xml (Indonesian) 2023-01-30 00:02:07 +01:00
Eugen Rochko
0155ef2675 New translations strings.xml (Icelandic) 2023-01-30 00:02:06 +01:00
Eugen Rochko
858195f813 New translations strings.xml (Vietnamese) 2023-01-30 00:02:06 +01:00
Eugen Rochko
b681c7dfeb New translations strings.xml (Chinese Traditional) 2023-01-30 00:02:05 +01:00
Eugen Rochko
b89f931ffd New translations strings.xml (Galician) 2023-01-30 00:02:04 +01:00
Eugen Rochko
1658e56729 New translations strings.xml (Igbo) 2023-01-30 00:02:03 +01:00
Eugen Rochko
2b7d8292ed New translations strings.xml (Portuguese, Brazilian) 2023-01-30 00:02:02 +01:00
Eugen Rochko
494abdfeee New translations strings.xml (Chinese Simplified) 2023-01-30 00:02:01 +01:00
Eugen Rochko
426f3fe95b New translations strings.xml (Ukrainian) 2023-01-30 00:02:00 +01:00
Eugen Rochko
b7a96778b8 New translations strings.xml (Swedish) 2023-01-30 00:01:59 +01:00
Eugen Rochko
125cd525bf New translations strings.xml (Slovenian) 2023-01-30 00:01:58 +01:00
Eugen Rochko
ed281a4619 New translations strings.xml (Russian) 2023-01-30 00:01:58 +01:00
Eugen Rochko
f418a5a2c4 New translations strings.xml (Portuguese) 2023-01-30 00:01:57 +01:00
Eugen Rochko
6d8971df64 New translations strings.xml (Dutch) 2023-01-30 00:01:56 +01:00
Eugen Rochko
8dd3343906 New translations strings.xml (Korean) 2023-01-30 00:01:55 +01:00
Eugen Rochko
f65fc9299a New translations strings.xml (Japanese) 2023-01-30 00:01:54 +01:00
Eugen Rochko
ac00889001 New translations strings.xml (Italian) 2023-01-30 00:01:53 +01:00
Eugen Rochko
7af0a3f351 New translations strings.xml (Armenian) 2023-01-30 00:01:52 +01:00
Eugen Rochko
2734f88206 New translations strings.xml (Hebrew) 2023-01-30 00:01:51 +01:00
Eugen Rochko
c6cd8ca14b New translations strings.xml (Irish) 2023-01-30 00:01:50 +01:00
Eugen Rochko
2fd61f738f New translations strings.xml (Finnish) 2023-01-30 00:01:50 +01:00
Eugen Rochko
d3575b60fe New translations strings.xml (Basque) 2023-01-30 00:01:49 +01:00
Eugen Rochko
dce8808d62 New translations strings.xml (Greek) 2023-01-30 00:01:48 +01:00
Eugen Rochko
db97dadb25 New translations strings.xml (German) 2023-01-30 00:01:47 +01:00
Eugen Rochko
34116d9914 New translations strings.xml (Danish) 2023-01-30 00:01:46 +01:00
Eugen Rochko
bc676e6eb3 New translations strings.xml (Catalan) 2023-01-30 00:01:45 +01:00
Eugen Rochko
ef2cb31b6c New translations strings.xml (Arabic) 2023-01-30 00:01:44 +01:00
Eugen Rochko
c45dc96316 New translations strings.xml (French) 2023-01-30 00:01:43 +01:00
Eugen Rochko
f9b34b53c1 New translations strings.xml (Romanian) 2023-01-30 00:01:42 +01:00
Eugen Rochko
e128e144b1 New translations strings.xml (Turkish) 2023-01-30 00:01:41 +01:00
Eugen Rochko
c68ed6088f New translations strings.xml (Spanish) 2023-01-30 00:01:40 +01:00
Eugen Rochko
b66ad0e6f5 New translations strings.xml (Polish) 2023-01-30 00:01:39 +01:00
Eugen Rochko
02a470bd7d New translations strings.xml (Belarusian) 2023-01-30 00:01:39 +01:00
Eugen Rochko
9407bd9e86 New translations strings.xml (Hungarian) 2023-01-30 00:01:38 +01:00
Eugen Rochko
825adda664 New translations strings.xml (Czech) 2023-01-30 00:01:37 +01:00
Eugen Rochko
0a22c14eec New translations strings.xml (Norwegian) 2023-01-30 00:01:35 +01:00
Gregory K
5c2f72a706 Merge pull request #521 from FineFindus/fix/typos
fix: typos
2023-01-30 01:54:30 +03:00
Grishka
b153a64373 Signup flow redesign WIP 2023-01-30 01:54:13 +03:00
Eugen Rochko
5452da6a65 New translations strings.xml (German) 2023-01-29 22:55:32 +01:00
Eugen Rochko
ffb321e36f New translations strings.xml (German) 2023-01-29 21:52:09 +01:00
Eugen Rochko
eaecff52c9 New translations strings.xml (Portuguese, Brazilian) 2023-01-29 21:52:08 +01:00
Eugen Rochko
b141a9ac74 New translations strings.xml (Portuguese, Brazilian) 2023-01-29 18:37:47 +01:00
Eugen Rochko
a890f21ace New translations strings.xml (Polish) 2023-01-28 23:17:50 +01:00
Eugen Rochko
4e72e5c234 New translations strings.xml (Polish) 2023-01-28 22:19:28 +01:00
Eugen Rochko
1def56057a New translations strings.xml (Vietnamese) 2023-01-28 15:42:21 +01:00
Eugen Rochko
d99f6c7167 New translations strings.xml (Vietnamese) 2023-01-28 14:39:22 +01:00
Eugen Rochko
0d5d169e5f New translations strings.xml (Korean) 2023-01-28 01:46:35 +01:00
Eugen Rochko
90e55c1043 New translations strings.xml (Korean) 2023-01-28 00:36:07 +01:00
Eugen Rochko
780c5c345c New translations strings.xml (Spanish) 2023-01-27 17:31:12 +01:00
Eugen Rochko
c2bc0a4055 New translations strings.xml (Dutch) 2023-01-27 14:07:08 +01:00
FineFindus
1124486f1f fix(Instance): typo langauges => languages 2023-01-26 20:56:15 +01:00
Eugen Rochko
8032de4595 New translations strings.xml (Ukrainian) 2023-01-26 19:50:28 +01:00
Eugen Rochko
bd3f5018ed New translations strings.xml (Ukrainian) 2023-01-26 18:48:15 +01:00
sk22
c757b1ffea Translated using Weblate (German)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2023-01-26 15:52:46 +00:00
sk22
932655eeb6 Translated using Weblate (German)
Currently translated at 100.0% (251 of 251 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-01-26 15:52:46 +00:00
sk
21d57b25c9 bump version and update strings 2023-01-26 16:48:36 +01:00
sk
bed572f343 Merge remote-tracking branch 'weblate/main' 2023-01-26 16:43:13 +01:00
sk
c7483a6b20 update screenshots 2023-01-26 16:42:17 +01:00
sk
cdb1e26a4d move some settings around 2023-01-26 16:38:04 +01:00
Oliebol
ce1a450ccb Translated using Weblate (Dutch)
Currently translated at 93.9% (234 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-01-26 15:21:28 +00:00
gallegonovato
dfc244ff41 Translated using Weblate (Spanish)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-01-26 15:21:28 +00:00
gallegonovato
9c3e2f5deb Translated using Weblate (Spanish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-01-26 15:21:28 +00:00
gallegonovato
452b286352 Translated using Weblate (Spanish)
Currently translated at 95.5% (238 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-01-26 15:21:28 +00:00
McKris
6deca645de Translated using Weblate (Polish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2023-01-26 15:21:28 +00:00
ihor_ck
49fd1aba76 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-01-26 15:21:28 +00:00
tygyh
7bc951ba67 Translated using Weblate (Swedish)
Currently translated at 42.8% (6 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sv/
2023-01-26 15:21:28 +00:00
Choukajohn
a70e73a8cb Translated using Weblate (French)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2023-01-26 15:21:28 +00:00
ihor_ck
cf345356a5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-26 15:21:28 +00:00
tygyh
3da3967afa Translated using Weblate (Swedish)
Currently translated at 16.4% (41 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sv/
2023-01-26 15:21:28 +00:00
McKris
a12f09a38a Translated using Weblate (Polish)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-01-26 15:21:27 +00:00
rex07
a7302cc3e1 Translated using Weblate (Arabic)
Currently translated at 2.4% (6 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-01-26 15:21:27 +00:00
rex07
9aed2a96dc Added translation using Weblate (Arabic) 2023-01-26 15:21:27 +00:00
Linerly
dbe49134e1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2023-01-26 15:21:27 +00:00
Linerly
089d176704 Translated using Weblate (Indonesian)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-26 15:21:27 +00:00
Espasant3
4e482ef6fa Translated using Weblate (Galician)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-01-26 15:21:27 +00:00
Espasant3
c64397a613 Translated using Weblate (Galician)
Currently translated at 98.3% (245 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-26 15:21:27 +00:00
sk
6c0d4778b7 separate notification toggle for polls 2023-01-26 16:18:19 +01:00
sk
b94c1f4a82 Merge branch 'fix-notify-policy-none' 2023-01-26 16:03:54 +01:00
sk
a29a072e53 unsubscribe from notifications when policy is none
re: mastodon#520
2023-01-26 15:48:10 +01:00
sk
4f435c6957 set disabled state if policy is none
re: mastodon#520
2023-01-26 15:47:30 +01:00
sk
2a6115f6d9 add server version to settings
closes sk22#376
2023-01-26 15:01:41 +01:00
sk
4cbc1e3664 use regular lock in boost menu
closes sk22#375
2023-01-26 14:58:01 +01:00
Eugen Rochko
91c4e5e51f New translations strings.xml (Thai) 2023-01-26 14:50:19 +01:00
Eugen Rochko
ca1cb668f3 New translations strings.xml (Chinese Traditional) 2023-01-26 06:12:30 +01:00
Eugen Rochko
a12ca697ed New translations strings.xml (Arabic) 2023-01-26 06:12:29 +01:00
Choukajohn
1053d2ac0c Translated using Weblate (French)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-26 00:31:07 +00:00
sk22
0123b17602 Translated using Weblate (German)
Currently translated at 92.8% (13 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2023-01-26 00:31:06 +00:00
sk22
1dace6ead9 Translated using Weblate (German)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-01-26 00:31:06 +00:00
sk22
3287cf69c1 Translated using Weblate (English)
Currently translated at 100.0% (14 of 14 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/en/
2023-01-26 00:31:06 +00:00
sk
a2854524a9 rename string 2023-01-26 01:13:01 +01:00
sk
71c06c0762 Merge remote-tracking branch 'weblate/main' 2023-01-26 01:09:43 +01:00
ihor_ck
e30df6067d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-26 00:09:37 +00:00
edxkl
912a354b1c Translated using Weblate (Portuguese (Brazil))
Currently translated at 90.7% (226 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-01-26 00:09:36 +00:00
McKris
71f830ea82 Translated using Weblate (Polish)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-01-26 00:09:36 +00:00
Linerly
bd109a9139 Translated using Weblate (Indonesian)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-26 00:09:36 +00:00
Choukajohn
c82b4445ff Translated using Weblate (French)
Currently translated at 100.0% (249 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-26 00:09:36 +00:00
irure
9a7d149dae Translated using Weblate (Basque)
Currently translated at 93.5% (233 of 249 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/eu/
2023-01-26 00:09:36 +00:00
sk
c66e576461 adapt upstream changes 2023-01-26 01:09:08 +01:00
sk
2a47d2fe77 update comment 2023-01-26 00:58:21 +01:00
sk
331d490f4f Merge remote-tracking branch 'upstream/master' 2023-01-26 00:56:48 +01:00
sk
8d722a2130 bump version, update changelog 2023-01-26 00:54:13 +01:00
sk
6863363452 change local-only string 2023-01-26 00:40:10 +01:00
sk
10e66a58eb fix visibility radio button 2023-01-26 00:39:02 +01:00
sk
3a5c27eadc change missing icons 2023-01-26 00:28:14 +01:00
sk
8c6bce4f73 enable boosting local posts 2023-01-26 00:07:50 +01:00
sk
17e1cd1fe9 replace visibility icons 2023-01-25 23:51:42 +01:00
Eugen Rochko
3ccb629a4e New translations strings.xml (Kabyle) 2023-01-25 23:45:26 +01:00
Eugen Rochko
4db041c28f New translations strings.xml (Occitan) 2023-01-25 23:45:25 +01:00
Eugen Rochko
bdcf4a5438 New translations strings.xml (Scottish Gaelic) 2023-01-25 23:45:24 +01:00
Eugen Rochko
c042050295 New translations strings.xml (Sinhala) 2023-01-25 23:45:23 +01:00
Eugen Rochko
f87a87aba1 New translations strings.xml (Bosnian) 2023-01-25 23:45:22 +01:00
Eugen Rochko
b3a88c4a7c New translations strings.xml (Filipino) 2023-01-25 23:45:21 +01:00
Eugen Rochko
9729663cb4 New translations strings.xml (Burmese) 2023-01-25 23:45:20 +01:00
Eugen Rochko
f66e6197d3 New translations strings.xml (Hindi) 2023-01-25 23:45:19 +01:00
Eugen Rochko
4ff940030d New translations strings.xml (Croatian) 2023-01-25 23:45:18 +01:00
Eugen Rochko
da8b88dfc6 New translations strings.xml (Thai) 2023-01-25 23:45:17 +01:00
Eugen Rochko
ed13b1074d New translations strings.xml (Bengali) 2023-01-25 23:45:16 +01:00
Eugen Rochko
c5e985f6a4 New translations strings.xml (Persian) 2023-01-25 23:45:15 +01:00
Eugen Rochko
160bb4e272 New translations strings.xml (Indonesian) 2023-01-25 23:45:14 +01:00
Eugen Rochko
f89a3e644a New translations strings.xml (Icelandic) 2023-01-25 23:45:13 +01:00
Eugen Rochko
4cfd0db899 New translations strings.xml (Vietnamese) 2023-01-25 23:45:12 +01:00
Eugen Rochko
c6cb992b92 New translations strings.xml (Chinese Traditional) 2023-01-25 23:45:11 +01:00
Eugen Rochko
1b3ea6cdbe New translations strings.xml (Chinese Simplified) 2023-01-25 23:45:10 +01:00
Eugen Rochko
79323e392b New translations strings.xml (Ukrainian) 2023-01-25 23:45:08 +01:00
Eugen Rochko
c6c985c1db New translations strings.xml (Swedish) 2023-01-25 23:45:07 +01:00
Eugen Rochko
8988b22a52 New translations strings.xml (Slovenian) 2023-01-25 23:45:06 +01:00
Eugen Rochko
692ede503c New translations strings.xml (Russian) 2023-01-25 23:45:05 +01:00
Eugen Rochko
3be04343b8 New translations strings.xml (Portuguese) 2023-01-25 23:45:04 +01:00
Eugen Rochko
521157315b New translations strings.xml (Dutch) 2023-01-25 23:45:03 +01:00
Eugen Rochko
b4d7b34767 New translations strings.xml (Korean) 2023-01-25 23:45:02 +01:00
Eugen Rochko
f912e90691 New translations strings.xml (Japanese) 2023-01-25 23:45:01 +01:00
Eugen Rochko
61ff2ce7e4 New translations strings.xml (Italian) 2023-01-25 23:45:00 +01:00
Eugen Rochko
0316ec340a New translations strings.xml (Armenian) 2023-01-25 23:44:59 +01:00
Eugen Rochko
3975e8c280 New translations strings.xml (Hebrew) 2023-01-25 23:44:58 +01:00
Eugen Rochko
37c40e4a8d New translations strings.xml (Irish) 2023-01-25 23:44:57 +01:00
Eugen Rochko
7dc691deae New translations strings.xml (Finnish) 2023-01-25 23:44:56 +01:00
Eugen Rochko
ab5dfe6f62 New translations strings.xml (Basque) 2023-01-25 23:44:55 +01:00
Eugen Rochko
b445e6f79f New translations strings.xml (Greek) 2023-01-25 23:44:54 +01:00
Eugen Rochko
5f0cd72303 New translations strings.xml (German) 2023-01-25 23:44:53 +01:00
Eugen Rochko
53dfa08300 New translations strings.xml (Danish) 2023-01-25 23:44:53 +01:00
Eugen Rochko
321c23c52e New translations strings.xml (Catalan) 2023-01-25 23:44:51 +01:00
Eugen Rochko
dd6cb4af74 New translations strings.xml (Arabic) 2023-01-25 23:44:50 +01:00
Eugen Rochko
54b6aaec09 New translations strings.xml (French) 2023-01-25 23:44:50 +01:00
Eugen Rochko
624d21d18b New translations strings.xml (Romanian) 2023-01-25 23:44:49 +01:00
Eugen Rochko
5c27155507 New translations strings.xml (Galician) 2023-01-25 23:44:48 +01:00
Eugen Rochko
35552cfbef New translations strings.xml (Turkish) 2023-01-25 23:44:46 +01:00
Eugen Rochko
31bbeef24e New translations strings.xml (Spanish) 2023-01-25 23:44:46 +01:00
Eugen Rochko
5ee42c0294 New translations strings.xml (Igbo) 2023-01-25 23:44:45 +01:00
Eugen Rochko
3d08f768f8 New translations strings.xml (Polish) 2023-01-25 23:44:44 +01:00
Eugen Rochko
603e3d7d65 New translations strings.xml (Belarusian) 2023-01-25 23:44:43 +01:00
Eugen Rochko
eb8f71aa31 New translations strings.xml (Portuguese, Brazilian) 2023-01-25 23:44:42 +01:00
Eugen Rochko
c7b5b41128 New translations strings.xml (Hungarian) 2023-01-25 23:44:41 +01:00
Eugen Rochko
98dbff38ff New translations strings.xml (Czech) 2023-01-25 23:44:40 +01:00
Eugen Rochko
d9317f6eb1 New translations strings.xml (Norwegian) 2023-01-25 23:44:39 +01:00
sk
d7aceffc8f change list icon 2023-01-25 23:41:07 +01:00
Grishka
bcb3e217cd More onboarding updates 2023-01-26 01:38:29 +03:00
sk
229c19664c hopefully prevent some crashes 2023-01-25 23:23:43 +01:00
sk
8bdbb2adef dividers and alignments
nobody knows the trouble i've seen
2023-01-25 22:55:14 +01:00
Eugen Rochko
4b5dff8742 New translations short_description.txt (Danish) 2023-01-25 22:14:11 +01:00
Eugen Rochko
2256ef6232 New translations full_description.txt (Danish) 2023-01-25 22:14:10 +01:00
Eugen Rochko
182bc09023 New translations strings.xml (Danish) 2023-01-25 22:14:09 +01:00
Eugen Rochko
a60e5040ea New translations strings.xml (Portuguese, Brazilian) 2023-01-25 22:14:08 +01:00
Eugen Rochko
c2a6e17fa5 New translations strings.xml (Thai) 2023-01-25 21:08:37 +01:00
Eugen Rochko
85931e2a65 New translations strings.xml (Portuguese, Brazilian) 2023-01-25 21:08:36 +01:00
HudobniVolk
907c5a2ca1 Translated using Weblate (Slovenian)
Currently translated at 30.7% (4 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-01-25 17:32:56 +00:00
HudobniVolk
2a2bfebf48 Translated using Weblate (Slovenian)
Currently translated at 80.4% (189 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-01-25 17:32:56 +00:00
edxkl
eba59549ec Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.7% (225 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-01-25 17:32:56 +00:00
HudobniVolk
9478a71693 Translated using Weblate (Slovenian)
Currently translated at 15.3% (2 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-01-25 17:32:56 +00:00
ghose
b01d7a417a Translated using Weblate (Galician)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-01-25 17:32:56 +00:00
ihor_ck
80c77292ed Translated using Weblate (Ukrainian)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-25 17:32:56 +00:00
HudobniVolk
488e6dda04 Translated using Weblate (Slovenian)
Currently translated at 79.5% (187 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-01-25 17:32:56 +00:00
edxkl
689f676668 Translated using Weblate (Portuguese (Brazil))
Currently translated at 86.3% (203 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-01-25 17:32:56 +00:00
gicorada
a06db9a3ab Translated using Weblate (Italian)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-01-25 17:32:56 +00:00
irure
a7283cbed8 Translated using Weblate (Basque)
Currently translated at 99.1% (233 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/eu/
2023-01-25 17:32:56 +00:00
gallegonovato
bc70d5e212 Translated using Weblate (Spanish)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-01-25 17:32:56 +00:00
AiOO
441686740a Translated using Weblate (Korean)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-01-25 17:32:56 +00:00
LucasGGamerM
ac0df083f2 Added translation using Weblate (Portuguese) 2023-01-25 17:32:56 +00:00
Espasant3
e10762d5fa Translated using Weblate (Galician)
Currently translated at 92.3% (12 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-01-25 17:32:56 +00:00
McKris
0ca4663c29 Translated using Weblate (Polish)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2023-01-25 17:32:56 +00:00
edxkl
7efd9341b1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 81.7% (192 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-01-25 17:32:56 +00:00
McKris
3d8693b2bd Translated using Weblate (Polish)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-01-25 17:32:56 +00:00
Oliebol
a3b5f3c926 Translated using Weblate (Dutch)
Currently translated at 98.2% (231 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-01-25 17:32:56 +00:00
gicorada
f9f4a1d1ef Translated using Weblate (Italian)
Currently translated at 78.2% (184 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-01-25 17:32:56 +00:00
Linerly
dd536002d0 Translated using Weblate (Indonesian)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-25 17:32:56 +00:00
ghose
4f8e381c84 Translated using Weblate (Galician)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-25 17:32:56 +00:00
Choukajohn
3b6b212c9e Translated using Weblate (French)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-25 17:32:56 +00:00
gallegonovato
bf429ee263 Translated using Weblate (Spanish)
Currently translated at 98.2% (231 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-01-25 17:32:55 +00:00
Espasant3
e7b1301b71 Translated using Weblate (Spanish)
Currently translated at 98.2% (231 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-01-25 17:32:55 +00:00
ling0412
6726e9523c Translated using Weblate (Chinese (Simplified))
Currently translated at 99.1% (233 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-01-25 17:32:55 +00:00
sk22
3ffcc7cef2 Translated using Weblate (German)
Currently translated at 100.0% (235 of 235 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-01-25 17:32:55 +00:00
edxkl
e6232f6d3b Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2023-01-25 17:32:55 +00:00
ihor_ck
5efc431192 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-25 17:32:55 +00:00
HudobniVolk
c3b75782b1 Translated using Weblate (Slovenian)
Currently translated at 35.6% (81 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-01-25 17:32:55 +00:00
edxkl
ec6f3f0cc3 Translated using Weblate (Portuguese (Brazil))
Currently translated at 82.3% (187 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-01-25 17:32:55 +00:00
Linerly
136c3cfb4a Translated using Weblate (Indonesian)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-25 17:32:55 +00:00
Choukajohn
79d1dbd3b7 Translated using Weblate (French)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-25 17:32:55 +00:00
ling0412
f2e1663c41 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-01-25 17:32:55 +00:00
Oliebol
5c73f37599 Translated using Weblate (Dutch)
Currently translated at 15.3% (2 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/nl/
2023-01-25 17:32:55 +00:00
sheepnik
7f239abf2f Translated using Weblate (Welsh)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/cy/
2023-01-25 17:32:55 +00:00
sheepnik
a07f7c232a Translated using Weblate (Welsh)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/cy/
2023-01-25 17:32:55 +00:00
McKris
4a60a5190f Translated using Weblate (Polish)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2023-01-25 17:32:55 +00:00
ling0412
69986fd869 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2023-01-25 17:32:55 +00:00
ihor_ck
e2ca572d45 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-25 17:32:55 +00:00
HudobniVolk
746e41fdbc Translated using Weblate (Slovenian)
Currently translated at 23.7% (54 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-01-25 17:32:55 +00:00
McKris
091f1f1e8c Translated using Weblate (Polish)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-01-25 17:32:55 +00:00
Oliebol
844ec185a6 Translated using Weblate (Dutch)
Currently translated at 93.3% (212 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-01-25 17:32:55 +00:00
Linerly
5622eaed83 Translated using Weblate (Indonesian)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-25 17:32:55 +00:00
ghose
dbf25da1db Translated using Weblate (Galician)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-25 17:32:54 +00:00
Choukajohn
d35a416084 Translated using Weblate (French)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-25 17:32:54 +00:00
ling0412
098acb85e4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-01-25 17:32:54 +00:00
AiOO
9d67337913 Translated using Weblate (Korean)
Currently translated at 66.9% (152 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-01-25 17:32:54 +00:00
sk22
914861775a Translated using Weblate (German)
Currently translated at 100.0% (227 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-01-25 17:32:54 +00:00
sk
c12a6eaee6 support admin notifications 2023-01-25 18:32:07 +01:00
Eugen Rochko
da94cd801b New translations strings.xml (Thai) 2023-01-25 18:24:53 +01:00
sk
5de23581fe improve local visibility compatibility 2023-01-25 16:49:15 +01:00
sk
413141df1e fix null pointer exception 2023-01-25 16:24:54 +01:00
sk
5f48870a90 re-arrange settings 2023-01-25 15:52:41 +01:00
sk
86e781cdea tweak no alt indicator 2023-01-25 15:32:51 +01:00
sk
34ebd9219f fix alt indicator setting not saving 2023-01-25 15:19:30 +01:00
sk
41688c4670 update string 2023-01-25 12:54:50 +01:00
sk
0d30cd973e support akkoma local visibility 2023-01-25 12:54:35 +01:00
sk
5ed80ca40a Merge remote-tracking branch 'upstream/master' 2023-01-25 10:44:10 +01:00
sk
78958085c3 fix non-local-only posting (which i broke) 2023-01-25 10:33:04 +01:00
Eugen Rochko
594570f9a1 New translations strings.xml (Italian) 2023-01-25 10:10:05 +01:00
Eugen Rochko
548a14ab60 New translations strings.xml (Chinese Traditional) 2023-01-25 05:20:29 +01:00
Eugen Rochko
40016332ff New translations strings.xml (Chinese Traditional) 2023-01-25 04:16:16 +01:00
Eugen Rochko
2b9746232b New translations strings.xml (Kabyle) 2023-01-24 21:53:35 +01:00
Eugen Rochko
1fe31e9262 New translations strings.xml (Occitan) 2023-01-24 21:53:34 +01:00
Eugen Rochko
b2a152a728 New translations strings.xml (Scottish Gaelic) 2023-01-24 21:53:33 +01:00
Eugen Rochko
f93d9a0c35 New translations strings.xml (Sinhala) 2023-01-24 21:53:31 +01:00
Eugen Rochko
af9d9c3f48 New translations strings.xml (Bosnian) 2023-01-24 21:53:30 +01:00
Eugen Rochko
5cae41a500 New translations strings.xml (Filipino) 2023-01-24 21:53:29 +01:00
Eugen Rochko
d6c560e015 New translations strings.xml (Burmese) 2023-01-24 21:53:28 +01:00
Eugen Rochko
a2679a3841 New translations strings.xml (Hindi) 2023-01-24 21:53:27 +01:00
Eugen Rochko
d826e0172b New translations strings.xml (Croatian) 2023-01-24 21:53:26 +01:00
Eugen Rochko
f593f5eb58 New translations strings.xml (Thai) 2023-01-24 21:53:25 +01:00
Eugen Rochko
d29565af9c New translations strings.xml (Bengali) 2023-01-24 21:53:24 +01:00
Eugen Rochko
7c3cef32ed New translations strings.xml (Persian) 2023-01-24 21:53:23 +01:00
Eugen Rochko
2c15796108 New translations strings.xml (Indonesian) 2023-01-24 21:53:22 +01:00
Eugen Rochko
553a3ef7e1 New translations strings.xml (Icelandic) 2023-01-24 21:53:21 +01:00
Eugen Rochko
a7dfb671ce New translations strings.xml (Vietnamese) 2023-01-24 21:53:20 +01:00
Eugen Rochko
929218d74c New translations strings.xml (Chinese Traditional) 2023-01-24 21:53:19 +01:00
Eugen Rochko
a5ec9695df New translations strings.xml (Chinese Simplified) 2023-01-24 21:53:18 +01:00
Eugen Rochko
2784828a93 New translations strings.xml (Ukrainian) 2023-01-24 21:53:16 +01:00
Eugen Rochko
84657e9529 New translations strings.xml (Swedish) 2023-01-24 21:53:15 +01:00
Eugen Rochko
110375462e New translations strings.xml (Slovenian) 2023-01-24 21:53:14 +01:00
Eugen Rochko
09e385633e New translations strings.xml (Russian) 2023-01-24 21:53:14 +01:00
Eugen Rochko
f7410a510f New translations strings.xml (Portuguese) 2023-01-24 21:53:13 +01:00
Eugen Rochko
20511fd39d New translations strings.xml (Dutch) 2023-01-24 21:53:12 +01:00
Eugen Rochko
dbacbe0341 New translations strings.xml (Korean) 2023-01-24 21:53:11 +01:00
Eugen Rochko
bd8da39a19 New translations strings.xml (Japanese) 2023-01-24 21:53:09 +01:00
Eugen Rochko
675a353494 New translations strings.xml (Italian) 2023-01-24 21:53:08 +01:00
Eugen Rochko
7b23ca1c96 New translations strings.xml (Armenian) 2023-01-24 21:53:08 +01:00
Eugen Rochko
61e8c6f435 New translations strings.xml (Hebrew) 2023-01-24 21:53:07 +01:00
Eugen Rochko
a41b8dbb01 New translations strings.xml (Irish) 2023-01-24 21:53:06 +01:00
Eugen Rochko
fe525f9242 New translations strings.xml (Finnish) 2023-01-24 21:53:05 +01:00
Eugen Rochko
7cbae9c0a9 New translations strings.xml (Basque) 2023-01-24 21:53:04 +01:00
Eugen Rochko
93ac0a103f New translations strings.xml (Greek) 2023-01-24 21:53:03 +01:00
Eugen Rochko
3b11787984 New translations strings.xml (German) 2023-01-24 21:53:02 +01:00
Eugen Rochko
7b6fcaf3db New translations strings.xml (Danish) 2023-01-24 21:53:00 +01:00
Eugen Rochko
11b838f394 New translations strings.xml (Catalan) 2023-01-24 21:52:59 +01:00
Eugen Rochko
38bd5eb68e New translations strings.xml (Arabic) 2023-01-24 21:52:58 +01:00
Eugen Rochko
1dc8d66b3f New translations strings.xml (French) 2023-01-24 21:52:57 +01:00
Eugen Rochko
9f64e56923 New translations strings.xml (Romanian) 2023-01-24 21:52:56 +01:00
Eugen Rochko
a9e84678b3 New translations strings.xml (Galician) 2023-01-24 21:52:55 +01:00
Eugen Rochko
d7c5c0074d New translations strings.xml (Turkish) 2023-01-24 21:52:54 +01:00
Eugen Rochko
97e148f4c8 New translations strings.xml (Spanish) 2023-01-24 21:52:53 +01:00
Eugen Rochko
e83bd039b3 New translations strings.xml (Igbo) 2023-01-24 21:52:52 +01:00
Eugen Rochko
a30d288b13 New translations strings.xml (Polish) 2023-01-24 21:52:51 +01:00
Eugen Rochko
ded14711ac New translations strings.xml (Belarusian) 2023-01-24 21:52:50 +01:00
Eugen Rochko
cece9d4aa1 New translations strings.xml (Portuguese, Brazilian) 2023-01-24 21:52:49 +01:00
Eugen Rochko
f7f56c7a9b New translations strings.xml (Hungarian) 2023-01-24 21:52:48 +01:00
Eugen Rochko
613a9de40e New translations strings.xml (Czech) 2023-01-24 21:52:47 +01:00
Eugen Rochko
9ed8ad1382 New translations strings.xml (Norwegian) 2023-01-24 21:52:46 +01:00
Grishka
a1798b6666 Update onboarding 2023-01-24 23:43:56 +03:00
sk
baa7dd6302 add missing import 2023-01-24 16:05:59 +01:00
sk
ba93e5bac3 bump version 2023-01-24 16:04:30 +01:00
sk
2358d3c602 implement local-only posting 2023-01-24 16:04:17 +01:00
sk
cf61626901 use notification icon 2023-01-24 12:41:23 +01:00
sk
349a1115a6 add indicator for direct and local-only posts 2023-01-24 12:40:23 +01:00
sk
8fa4980ba5 don't apply title for hashtags/lists
closes sk22#343
2023-01-24 11:20:14 +01:00
sk
099d0ccf94 no hashtag header in list timeline
closes sk22#366
2023-01-24 11:06:35 +01:00
sk
35a1de7888 add option to disable show new posts button 2023-01-24 10:53:11 +01:00
sk
6fc850b5ba tweak no alt indicator background 2023-01-24 10:08:03 +01:00
sk
96f13defd4 Revert "bigger hitbox for alt indicator"
This reverts commit 1b04440546.

this commit just wasn't working properly. animation would have a
frame of stuttering and i don't know where that came from or how i
could fix that. also, the code was a mess anyway
2023-01-24 09:46:22 +01:00
sk
36dd07aa38 fix loading default visibility 2023-01-24 09:31:49 +01:00
sk
6a831539ad bump version, again 2023-01-24 09:31:16 +01:00
sk
c679f5529e add null check 2023-01-24 02:17:15 +01:00
sk
9c8096274a bump version 2023-01-24 01:28:49 +01:00
sk
7291b2da5a implement pre-release toggle 2023-01-24 01:27:17 +01:00
sk
4ff98140cb fix navigation bumpiness
closes sk22#347
2023-01-24 01:09:41 +01:00
sk
c2a993c5c1 don't override visibility when replying to self
closes sk22#348
2023-01-23 22:00:23 +01:00
sk
1b04440546 bigger hitbox for alt indicator
closes sk22#353
2023-01-23 20:13:01 +01:00
sk
c0c276f03e add indicator for missing alt texts
closes sk22#355
2023-01-23 19:21:21 +01:00
sk
d30b1f7bbd hide scheduling options when editing
closes sk22#364
2023-01-23 17:11:10 +01:00
sk
c0ee16cf08 fix current language getting overwritten 2023-01-23 16:57:56 +01:00
sk
a37fb33a68 prompt when saving edited draft
closes sk22#319
2023-01-23 16:11:09 +01:00
sk
59095e4ffe Merge remote-tracking branch 'upstream/master' 2023-01-23 15:32:32 +01:00
sk
626614c03d Merge branch 'improve-compose-toolbar-hitbox' 2023-01-23 15:32:02 +01:00
Gregory K
58ab0c0fc1 Merge pull request #516 from sk22/improve-compose-toolbar-hitbox
Bigger hitbox for items in compose toolbar
2023-01-23 17:17:20 +03:00
sk
32a8d38edf bigger hitbox for items in compose toolbar 2023-01-23 14:54:39 +01:00
sk
82534f7c4a change add media icon
closes sk22#351
2023-01-23 14:42:05 +01:00
sk
c6d7242043 display header for followed hashtags
closes sk22#323
2023-01-23 14:31:09 +01:00
sk
c4e23b0fe6 update hashtags/lists in home
closes sk22#312
2023-01-23 13:57:17 +01:00
Gregory K
a5c753a9f8 Merge pull request #515 from sk22/allow-notifications-toolbar-tab
Enable scrolling to top by tapping Notifications toolbar
2023-01-23 15:38:07 +03:00
Eugen Rochko
7498118800 New translations strings.xml (Belarusian) 2023-01-23 13:24:17 +01:00
sk
e3520df57e Merge branch 'allow-notifications-toolbar-tab' 2023-01-23 12:50:05 +01:00
sk
66cede567e enable scrolling to top via toolbar 2023-01-23 12:49:55 +01:00
sk
6916f435b3 improve search empty text 2023-01-23 12:41:10 +01:00
sk
dab0c560e9 fix crash when recycler view is null 2023-01-23 12:23:15 +01:00
Eugen Rochko
b894827607 New translations strings.xml (Belarusian) 2023-01-23 12:18:10 +01:00
sk
1b23ef31d5 improve header icons
* align more button to action overflow button
* use different background to better reflect hitbox
2023-01-23 12:14:05 +01:00
sk
dd7af8b5d3 use fluent more icon, correct padding
closes sk22#350
2023-01-23 11:43:19 +01:00
sk
5914ef8fad fix akkoma crash on list edit
closes sk22#352
2023-01-23 10:51:24 +01:00
sk
a26ddfe70f move edit timelines option 2023-01-23 10:25:44 +01:00
sk
cb067ca4fa clean up code 2023-01-23 10:21:19 +01:00
sk
3df9a3eecc probably fix options menu issue
closes sk22#360
2023-01-23 10:20:06 +01:00
sk
987cbc86ec fix wrong status bar color
closes sk22#363
2023-01-23 10:13:23 +01:00
sk
66dcaa9169 Merge remote-tracking branch 'origin/main' 2023-01-23 10:07:55 +01:00
sk
7162feea31 fix double-click icon button 2023-01-23 10:07:43 +01:00
sk22
1a51744807 Update blocks.tsv 2023-01-22 17:36:04 +01:00
sk22
f83a28a1b3 Adding a 12 hour option for polls (#346)
Co-authored-by: Pleclown <pleclown+github@gmail.com>

closes #346
2023-01-22 13:38:29 +01:00
sk
f5d4e2a0b5 Merge remote-tracking branch 'upstream/master' 2023-01-22 03:44:30 +01:00
sk
4aaf0c4fa4 Merge remote-tracking branch 'weblate/main' 2023-01-22 03:27:55 +01:00
sk22
38e133bee4 Translated using Weblate (German)
Currently translated at 99.1% (225 of 227 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-01-22 02:26:28 +00:00
sk
87bc01d985 remove add media string 2023-01-22 03:26:18 +01:00
sk
d5561674cd add a few icons 2023-01-22 03:15:07 +01:00
ihor_ck
48ec9e9fc6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (174 of 174 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-22 02:11:57 +00:00
Linerly
63775c6eb9 Translated using Weblate (Indonesian)
Currently translated at 100.0% (174 of 174 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-22 02:11:57 +00:00
Choukajohn
79a61f6865 Translated using Weblate (French)
Currently translated at 100.0% (174 of 174 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-22 02:11:57 +00:00
florian-obernberger
5f7e03a562 Translated using Weblate (German)
Currently translated at 91.9% (160 of 174 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-01-22 02:11:57 +00:00
sheepnik
c93c4efe1d Translated using Weblate (Welsh)
Currently translated at 15.3% (2 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/cy/
2023-01-22 02:11:57 +00:00
sheepnik
69771269fc Translated using Weblate (Welsh)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/cy/
2023-01-22 02:11:57 +00:00
gicorada
e7a28696c6 Translated using Weblate (Italian)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-01-22 02:11:57 +00:00
sk
124ad1df06 add a few icons 2023-01-22 03:11:41 +01:00
sk
3a6ace53d5 add back file opener
closes sk22#328
2023-01-22 01:06:49 +01:00
sk
1e825c979c rearrange home menus 2023-01-22 00:43:17 +01:00
Grishka
c67b2b35f3 Fix #509 2023-01-22 02:08:55 +03:00
Grishka
8588ca8ae3 Fix #510 2023-01-22 02:05:36 +03:00
Grishka
7bb280e8b8 Fix #69 (nice) 2023-01-22 02:04:12 +03:00
sk
ad1e1b112b Merge remote-tracking branch 'upstream/master' 2023-01-21 23:14:35 +01:00
sk
29139a8f4d Revert "work around crash onHidden"
This reverts commit 763c5fe2a7.
2023-01-21 23:14:24 +01:00
Grishka
ddfeaabd44 Fix #512 2023-01-22 01:12:39 +03:00
Gregory K
51219bf98a Merge pull request #513 from sk22/fix-has-spoiler-restore
Fix wrong "hasSpoiler" value on restore
2023-01-22 01:11:42 +03:00
sk
335f734698 Merge branch 'fix-has-spoiler-restore' 2023-01-21 23:09:12 +01:00
sk
512cb70347 fix wrong "hasSpoiler" value on restore
closes sk22#324
2023-01-21 23:06:31 +01:00
sk
c7e0adfbd4 Merge remote-tracking branch 'weblate/main' 2023-01-21 22:50:26 +01:00
sk
ad7a9626a4 fix typo in string 2023-01-21 22:50:11 +01:00
sk
92f37fdf16 scroll image in alt text editor
closes sk22#315
2023-01-21 22:48:00 +01:00
sk
900e8fb2e9 notifications for edited posts
closes sk22#331
2023-01-21 22:06:00 +01:00
sk
be4b032527 fix redrafting empty posts
closes sk22#325
2023-01-21 21:48:33 +01:00
sk
95cb04530f add lists to status header 2023-01-21 21:39:11 +01:00
sk
4b6a0b71a0 restart app when pinned changes 2023-01-21 19:55:51 +01:00
sk
187190c07e update bug report template 2023-01-21 19:29:48 +01:00
sk
5142851f57 remove pivot for timeline title 2023-01-21 19:24:55 +01:00
sk
763c5fe2a7 work around crash onHidden
re: mastodon#512
2023-01-21 19:24:33 +01:00
sk
7f0265fe24 work around black screen opening notifs
closes sk22#342
2023-01-21 19:22:23 +01:00
sk
f87827700b different icon for post notifs 2023-01-21 16:17:53 +01:00
sk
fb2c0c0ec2 tweak alt button 2023-01-21 02:35:46 +01:00
sk
ec40488ed1 Merge remote-tracking branch 'upstream/master' 2023-01-21 02:25:44 +01:00
sk22
88851a085e Pinnable timelines (#338)
* implement draggable list

* implement pinning timelines

* fix TimelineDefinition equals not working

* implement removing timelines

* implement pinned lists/hashtags

* per-account pinned timelines

* implement pin button

* fix issues with pinning

* improve pin button

* improve pinning timelines

* implement custom icons

* fix home switcher menu

* make hashtags pinnable

* edit timelines in options menu
2023-01-21 02:17:47 +01:00
Espasant3
87c743886e Translated using Weblate (Galician)
Currently translated at 61.5% (8 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-01-19 12:53:13 +00:00
McKris
f3cde5441b Translated using Weblate (Polish)
Currently translated at 96.6% (144 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-01-19 12:53:13 +00:00
ghose
7a9534772d Translated using Weblate (Galician)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-19 12:53:13 +00:00
Jippang
42faa62a5f Translated using Weblate (Korean)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-01-19 12:53:13 +00:00
Eugen Rochko
8788fb0b27 New translations full_description.txt (Galician) 2023-01-19 06:28:17 +01:00
Eugen Rochko
62d4c62888 New translations strings.xml (Galician) 2023-01-19 06:28:15 +01:00
Eugen Rochko
572d092f88 New translations strings.xml (Galician) 2023-01-19 05:18:11 +01:00
Grishka
6e718d6765 Save last seen home timeline post via markers API 2023-01-18 20:29:49 +03:00
Grishka
b26d491eda remove log 2023-01-18 20:10:03 +03:00
Grishka
abdbab9d7b Allow viewing alt text on images
closes #100
2023-01-18 20:09:27 +03:00
Grishka
af1c7194e6 Workaround to fix #497 2023-01-18 18:41:48 +03:00
sk
8e507e7970 move post notifications into home
closes sk22#314
2023-01-18 12:29:33 +01:00
sk
3b542730b1 fix update item margin
closes sk22#308
2023-01-18 12:10:35 +01:00
sk
b038f81718 add alt text reminder
closes sk22#103
2023-01-18 12:08:40 +01:00
sk
e1206703cf disable translating scheduled posts
closes sk22#318
2023-01-18 11:39:49 +01:00
Espasant3
924affee14 Translated using Weblate (Galician)
Currently translated at 61.5% (8 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-01-18 08:32:08 +00:00
sheepnik
0c5da34cd6 Translated using Weblate (Welsh)
Currently translated at 62.4% (93 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/cy/
2023-01-18 08:32:08 +00:00
AiOO
b44e6b9f0a Translated using Weblate (Korean)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2023-01-18 08:32:08 +00:00
gallegonovato
9d3369f601 Translated using Weblate (Spanish)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-01-18 08:32:07 +00:00
edxkl
f607ed314d Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.9% (143 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-01-18 08:32:07 +00:00
McKris
2cdf642ca3 Translated using Weblate (Polish)
Currently translated at 95.9% (143 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-01-18 08:32:07 +00:00
Oliebol
5d278eb5aa Translated using Weblate (Dutch)
Currently translated at 97.9% (146 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-01-18 08:32:07 +00:00
Espasant3
860c2826e3 Translated using Weblate (Galician)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-18 08:32:07 +00:00
ghose
3060c36cca Translated using Weblate (Galician)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-18 08:32:07 +00:00
gallegonovato
a1b0632c75 Translated using Weblate (Spanish)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-01-18 08:32:07 +00:00
ling0412
14cbb1107f Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (148 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-01-18 08:32:07 +00:00
AiOO
dd5f352f5e Translated using Weblate (Korean)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-01-18 08:32:07 +00:00
Eugen Rochko
2ff771391c New translations strings.xml (Turkish) 2023-01-17 21:00:59 +01:00
Eugen Rochko
087e55277c New translations strings.xml (Spanish) 2023-01-17 14:46:10 +01:00
Eugen Rochko
a2d45fbbc5 New translations full_description.txt (Spanish) 2023-01-17 13:50:33 +01:00
ihor_ck
d148883ab2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-01-17 11:26:44 +00:00
Linerly
cfa93424cc Translated using Weblate (Indonesian)
Currently translated at 100.0% (13 of 13 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2023-01-17 11:26:43 +00:00
ihor_ck
ff575f75c7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-01-17 11:26:43 +00:00
itslameni
6fec7a5205 Translated using Weblate (Russian)
Currently translated at 81.2% (121 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-01-17 11:26:43 +00:00
Oliebol
0693495e12 Translated using Weblate (Dutch)
Currently translated at 97.9% (146 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-01-17 11:26:43 +00:00
Linerly
04381d57f2 Translated using Weblate (Indonesian)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-01-17 11:26:43 +00:00
ghose
9f4adcab23 Translated using Weblate (Galician)
Currently translated at 46.9% (70 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-01-17 11:26:43 +00:00
Choukajohn
00dba5981c Translated using Weblate (French)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-01-17 11:26:43 +00:00
AiOO
ae838fe4d7 Translated using Weblate (Korean)
Currently translated at 100.0% (149 of 149 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-01-17 11:26:43 +00:00
Eugen Rochko
59262fe345 New translations full_description.txt (Belarusian) 2023-01-17 08:34:46 +01:00
sk
2110861f1b bump version 2023-01-17 02:20:27 +01:00
sk
4f6476c807 fix "0" reply to ID 2023-01-17 02:17:40 +01:00
Eugen Rochko
ffc36f7346 New translations short_description.txt (Belarusian) 2023-01-16 16:05:34 +01:00
Eugen Rochko
549ace65f5 New translations full_description.txt (Belarusian) 2023-01-16 16:05:33 +01:00
Eugen Rochko
c76bec2298 New translations strings.xml (Belarusian) 2023-01-16 16:05:32 +01:00
Eugen Rochko
290e47386e New translations strings.xml (Belarusian) 2023-01-16 14:43:34 +01:00
Eugen Rochko
3d96475c21 New translations strings.xml (Belarusian) 2023-01-16 12:54:15 +01:00
Eugen Rochko
b62fe06187 New translations strings.xml (Belarusian) 2023-01-16 11:49:53 +01:00
Eugen Rochko
c0dc2b8392 New translations strings.xml (Belarusian) 2023-01-16 09:21:59 +01:00
Eugen Rochko
e777bbb215 New translations strings.xml (Belarusian) 2023-01-16 08:16:21 +01:00
Eugen Rochko
014f9f4d99 New translations strings.xml (Belarusian) 2023-01-15 22:22:23 +01:00
Eugen Rochko
86bfd3d09f New translations strings.xml (Igbo) 2023-01-15 14:40:38 +01:00
Eugen Rochko
21d6f6da4c New translations title.txt (Igbo) 2023-01-15 13:36:28 +01:00
Eugen Rochko
f826e0ceef New translations short_description.txt (Igbo) 2023-01-15 13:36:28 +01:00
Eugen Rochko
458ad0f51a New translations full_description.txt (Igbo) 2023-01-15 13:36:27 +01:00
Eugen Rochko
9a0ff42ec2 New translations strings.xml (Igbo) 2023-01-15 13:36:26 +01:00
Eugen Rochko
18dae448ec New translations full_description.txt (Portuguese) 2023-01-14 14:57:31 +01:00
Eugen Rochko
fc36a8cc8f New translations full_description.txt (Portuguese) 2023-01-14 13:59:38 +01:00
Eugen Rochko
a390df2b9e New translations full_description.txt (Polish) 2023-01-14 07:31:44 +01:00
Eugen Rochko
6f61d3f0e3 New translations strings.xml (Polish) 2023-01-14 07:31:43 +01:00
Eugen Rochko
3a4e8ebdf4 New translations strings.xml (Polish) 2023-01-14 06:36:08 +01:00
Eugen Rochko
537242b277 New translations strings.xml (Czech) 2023-01-13 17:24:51 +01:00
Eugen Rochko
97eece59ea New translations strings.xml (Belarusian) 2023-01-13 15:29:36 +01:00
Eugen Rochko
fc88d42e50 New translations strings.xml (Belarusian) 2023-01-13 14:21:42 +01:00
Eugen Rochko
ec74712e55 New translations strings.xml (Belarusian) 2023-01-13 11:50:08 +01:00
Eugen Rochko
12e1ccf439 New translations strings.xml (Belarusian) 2023-01-13 10:53:08 +01:00
Eugen Rochko
24b8d5ce7c New translations strings.xml (Belarusian) 2023-01-13 07:50:35 +01:00
Eugen Rochko
46c9c83b63 New translations strings.xml (Belarusian) 2023-01-13 06:47:00 +01:00
Eugen Rochko
526a9fec03 New translations strings.xml (Belarusian) 2023-01-13 05:47:47 +01:00
Eugen Rochko
ca20f3b906 New translations strings.xml (Portuguese, Brazilian) 2023-01-13 01:19:33 +01:00
Eugen Rochko
af8c8a6248 New translations strings.xml (Portuguese, Brazilian) 2023-01-12 22:31:03 +01:00
Eugen Rochko
2940e5d3d8 New translations strings.xml (Portuguese, Brazilian) 2023-01-12 03:45:12 +01:00
Eugen Rochko
c98b001c9f New translations strings.xml (Portuguese, Brazilian) 2023-01-12 02:45:05 +01:00
Eugen Rochko
1fc1c95d6e New translations full_description.txt (Czech) 2023-01-11 12:16:25 +01:00
Eugen Rochko
e4d0c4eda5 New translations strings.xml (Czech) 2023-01-11 12:16:23 +01:00
Eugen Rochko
801d11c8e6 New translations short_description.txt (Norwegian) 2023-01-11 11:15:30 +01:00
Eugen Rochko
8143374929 New translations full_description.txt (Norwegian) 2023-01-11 11:15:29 +01:00
Eugen Rochko
df2ff9f874 New translations strings.xml (Norwegian) 2023-01-11 11:15:28 +01:00
Eugen Rochko
4bac852d37 New translations strings.xml (Norwegian) 2023-01-11 08:30:29 +01:00
Eugen Rochko
862a173392 New translations title.txt (Norwegian) 2023-01-07 17:43:49 +01:00
Eugen Rochko
bd47b31c65 New translations short_description.txt (Norwegian) 2023-01-07 17:43:48 +01:00
Eugen Rochko
aefb7f2e23 New translations full_description.txt (Norwegian) 2023-01-07 17:43:48 +01:00
Eugen Rochko
e509b8afa4 New translations strings.xml (Norwegian) 2023-01-07 17:43:47 +01:00
Eugen Rochko
7b94f7258f New translations title.txt (Danish) 2023-01-07 17:43:46 +01:00
Eugen Rochko
8d81efae4e New translations short_description.txt (Danish) 2023-01-07 17:43:45 +01:00
Eugen Rochko
5b0b80277c New translations full_description.txt (Danish) 2023-01-07 17:43:44 +01:00
Eugen Rochko
60293d5a65 New translations strings.xml (Danish) 2023-01-07 17:43:43 +01:00
Eugen Rochko
09b4aff9f5 New translations strings.xml (Filipino) 2023-01-06 11:31:21 +01:00
Eugen Rochko
7326cbeb14 New translations strings.xml (Burmese) 2023-01-06 10:13:45 +01:00
Eugen Rochko
91bd3fa4ea New translations full_description.txt (Burmese) 2023-01-06 09:04:21 +01:00
Eugen Rochko
3cc6a9905e New translations strings.xml (Burmese) 2023-01-06 09:04:20 +01:00
Eugen Rochko
f01bfcd372 New translations full_description.txt (Burmese) 2023-01-06 06:45:57 +01:00
Eugen Rochko
81b4365a14 New translations full_description.txt (Swedish) 2023-01-03 12:40:32 +01:00
Eugen Rochko
7ab28a6db6 New translations strings.xml (Swedish) 2023-01-03 12:40:31 +01:00
Eugen Rochko
3bb548cf22 New translations full_description.txt (Burmese) 2023-01-03 08:43:34 +01:00
Eugen Rochko
ba788d1b34 New translations full_description.txt (Burmese) 2023-01-03 07:13:43 +01:00
Eugen Rochko
f51b01bcd9 New translations short_description.txt (Burmese) 2023-01-03 06:17:54 +01:00
Eugen Rochko
361c97a9df New translations full_description.txt (Burmese) 2023-01-03 06:17:53 +01:00
Eugen Rochko
f34153e601 New translations title.txt (Burmese) 2023-01-02 22:49:11 +01:00
Eugen Rochko
67240acb48 New translations short_description.txt (Burmese) 2023-01-02 22:49:11 +01:00
Eugen Rochko
4395dbfa7c New translations full_description.txt (Burmese) 2023-01-02 22:49:10 +01:00
Eugen Rochko
48b0207636 New translations strings.xml (Burmese) 2023-01-02 22:49:09 +01:00
Eugen Rochko
f7b8ed519c New translations strings.xml (Filipino) 2023-01-02 20:56:03 +01:00
Eugen Rochko
eba88f2c0a New translations strings.xml (Filipino) 2023-01-02 20:00:36 +01:00
Eugen Rochko
8d95355727 New translations short_description.txt (Filipino) 2023-01-01 17:18:41 +01:00
Eugen Rochko
e05a67c4ab New translations full_description.txt (Filipino) 2023-01-01 17:18:40 +01:00
Eugen Rochko
5db91627a1 New translations strings.xml (Filipino) 2023-01-01 17:18:39 +01:00
Eugen Rochko
7e473aa8a8 New translations strings.xml (Filipino) 2023-01-01 16:17:30 +01:00
Eugen Rochko
1e1edd698d New translations strings.xml (Vietnamese) 2023-01-01 09:16:11 +01:00
Eugen Rochko
aa42a0a4c4 New translations strings.xml (Filipino) 2022-12-29 06:02:04 +01:00
Eugen Rochko
ad61596f66 New translations strings.xml (Filipino) 2022-12-29 05:04:36 +01:00
Eugen Rochko
bd518b3038 New translations strings.xml (Filipino) 2022-12-28 14:31:07 +01:00
Eugen Rochko
db3129ab11 New translations strings.xml (Arabic) 2022-12-27 21:32:08 +01:00
Eugen Rochko
57a38a83e4 New translations strings.xml (Russian) 2022-12-24 14:52:14 +01:00
Eugen Rochko
c0de43e2f3 New translations strings.xml (Vietnamese) 2022-12-24 06:25:12 +01:00
Eugen Rochko
c6e29c9ce4 New translations strings.xml (Filipino) 2022-12-23 14:24:05 +01:00
Eugen Rochko
35e8f5eddf New translations strings.xml (Filipino) 2022-12-23 11:33:45 +01:00
Eugen Rochko
40ed72aeff New translations full_description.txt (Dutch) 2022-12-22 02:02:15 +01:00
Eugen Rochko
75033cf42e New translations strings.xml (Dutch) 2022-12-22 02:02:14 +01:00
Eugen Rochko
4a29a63d50 New translations short_description.txt (Dutch) 2022-12-21 23:28:34 +01:00
Eugen Rochko
595a6847dc New translations strings.xml (Dutch) 2022-12-21 23:28:33 +01:00
Eugen Rochko
64f403b644 New translations full_description.txt (Thai) 2022-12-19 21:15:22 +01:00
Eugen Rochko
314517c378 New translations full_description.txt (Hungarian) 2022-12-19 11:42:43 +01:00
Eugen Rochko
b90fc55b3f New translations strings.xml (Hungarian) 2022-12-19 11:42:42 +01:00
Eugen Rochko
cd57966810 New translations full_description.txt (Indonesian) 2022-12-18 00:52:58 +01:00
Eugen Rochko
8c0851e2b5 New translations strings.xml (Indonesian) 2022-12-18 00:52:57 +01:00
Eugen Rochko
b9efa434d2 New translations strings.xml (Thai) 2022-12-16 22:37:47 +01:00
Eugen Rochko
adc085a313 New translations strings.xml (Thai) 2022-12-16 21:27:39 +01:00
Eugen Rochko
a2a2f67239 New translations strings.xml (Catalan) 2022-12-16 17:40:43 +01:00
Eugen Rochko
c30fba61ca New translations strings.xml (Swedish) 2022-12-16 13:09:15 +01:00
Eugen Rochko
f09b37d28f New translations strings.xml (Persian) 2022-12-16 09:28:27 +01:00
Eugen Rochko
6cbc89b01d New translations strings.xml (Persian) 2022-12-16 08:22:43 +01:00
Eugen Rochko
ffd538fbd0 New translations strings.xml (Dutch) 2022-12-15 17:36:44 +01:00
Eugen Rochko
1f27f66432 New translations full_description.txt (Dutch) 2022-12-15 16:30:46 +01:00
Eugen Rochko
25f302f62f New translations strings.xml (Basque) 2022-12-14 13:39:14 +01:00
Eugen Rochko
d54eb6ed73 New translations strings.xml (Basque) 2022-12-14 12:41:52 +01:00
Eugen Rochko
24a6d77777 New translations strings.xml (Spanish) 2022-12-13 16:47:57 +01:00
Eugen Rochko
5cbebe7ec6 New translations strings.xml (Portuguese) 2022-12-13 13:29:44 +01:00
Eugen Rochko
a31c310ffa New translations full_description.txt (German) 2022-12-13 12:26:47 +01:00
Eugen Rochko
752d0b5ca9 New translations strings.xml (Korean) 2022-12-13 02:15:40 +01:00
Eugen Rochko
170131188a New translations title.txt (Persian) 2022-12-12 14:17:48 +01:00
Eugen Rochko
3269613139 New translations short_description.txt (Persian) 2022-12-12 14:17:47 +01:00
Eugen Rochko
52cc74fb85 New translations full_description.txt (Persian) 2022-12-12 14:17:46 +01:00
Eugen Rochko
d7d09b1d56 New translations strings.xml (Persian) 2022-12-12 14:17:45 +01:00
Eugen Rochko
797f2b5929 New translations strings.xml (Spanish) 2022-12-12 13:22:18 +01:00
Eugen Rochko
fa5053fe38 New translations full_description.txt (Spanish) 2022-12-12 11:19:07 +01:00
Eugen Rochko
c682c249bd New translations strings.xml (Spanish) 2022-12-12 11:19:06 +01:00
Eugen Rochko
01c229c7c1 New translations strings.xml (Spanish) 2022-12-12 10:19:26 +01:00
Eugen Rochko
83f39d6b22 New translations strings.xml (Thai) 2022-12-11 21:27:37 +01:00
Eugen Rochko
715ec6e7c6 New translations strings.xml (Thai) 2022-12-11 20:11:30 +01:00
Eugen Rochko
b6fa34e87f New translations strings.xml (Italian) 2022-12-11 17:31:10 +01:00
Eugen Rochko
6ff14cc7a1 New translations full_description.txt (Slovenian) 2022-12-11 15:55:38 +01:00
Eugen Rochko
d606ce89e0 New translations strings.xml (Slovenian) 2022-12-11 15:55:37 +01:00
Eugen Rochko
41e80f1d24 New translations full_description.txt (Hungarian) 2022-12-11 14:42:32 +01:00
Eugen Rochko
8decd66e26 New translations strings.xml (Hungarian) 2022-12-11 14:42:31 +01:00
Eugen Rochko
c033849fb4 New translations strings.xml (Thai) 2022-12-11 14:42:30 +01:00
Eugen Rochko
14054b2198 New translations strings.xml (Hungarian) 2022-12-11 13:46:42 +01:00
Eugen Rochko
3c3a6712bd New translations strings.xml (Icelandic) 2022-12-11 10:41:24 +01:00
Eugen Rochko
6a97ed41e0 New translations strings.xml (Icelandic) 2022-12-11 09:42:40 +01:00
Eugen Rochko
bfedd6c953 New translations full_description.txt (Vietnamese) 2022-12-11 06:15:06 +01:00
Eugen Rochko
23d72346b3 New translations strings.xml (Vietnamese) 2022-12-11 06:15:05 +01:00
Eugen Rochko
e9510875ea New translations strings.xml (Vietnamese) 2022-12-11 05:13:59 +01:00
Eugen Rochko
84d7b6c48f New translations strings.xml (Italian) 2022-12-11 01:14:30 +01:00
Eugen Rochko
5f4af7024d New translations full_description.txt (Portuguese, Brazilian) 2022-12-10 20:48:20 +01:00
Eugen Rochko
3b16eb807e New translations strings.xml (Portuguese, Brazilian) 2022-12-10 20:48:19 +01:00
Eugen Rochko
ef6b52049f New translations strings.xml (German) 2022-12-10 17:34:19 +01:00
Eugen Rochko
a7b035bb8e New translations strings.xml (Chinese Simplified) 2022-12-10 16:11:51 +01:00
Eugen Rochko
645216b8eb New translations strings.xml (Chinese Simplified) 2022-12-10 15:10:07 +01:00
Eugen Rochko
2b1c18635e New translations strings.xml (Italian) 2022-12-10 11:42:34 +01:00
Eugen Rochko
bb7a76617e New translations full_description.txt (Korean) 2022-12-10 06:43:06 +01:00
Eugen Rochko
ab50e7861a New translations strings.xml (Korean) 2022-12-10 05:29:13 +01:00
Eugen Rochko
d52b88c816 New translations strings.xml (Korean) 2022-12-10 03:51:49 +01:00
Eugen Rochko
e5d0a2a14c New translations full_description.txt (Korean) 2022-12-10 02:44:45 +01:00
Eugen Rochko
11e9db7ded New translations strings.xml (Korean) 2022-12-10 02:44:44 +01:00
Eugen Rochko
23c1a78d01 New translations strings.xml (German) 2022-12-09 21:35:30 +01:00
Eugen Rochko
e524423191 New translations full_description.txt (Ukrainian) 2022-12-09 20:36:43 +01:00
Eugen Rochko
7286e71442 New translations strings.xml (Ukrainian) 2022-12-09 20:36:42 +01:00
Eugen Rochko
71681458a1 New translations strings.xml (French) 2022-12-09 19:20:25 +01:00
Eugen Rochko
4d4b3c8867 New translations strings.xml (French) 2022-12-09 17:51:26 +01:00
Eugen Rochko
681d808a74 New translations strings.xml (Kabyle) 2022-12-09 16:35:54 +01:00
Eugen Rochko
876a0b27a6 New translations strings.xml (French) 2022-12-09 16:35:53 +01:00
Eugen Rochko
9dce3b9a17 New translations full_description.txt (Arabic) 2022-12-09 14:48:38 +01:00
Eugen Rochko
f77b487520 New translations strings.xml (Arabic) 2022-12-09 14:48:37 +01:00
Eugen Rochko
f3c73a5c8a New translations strings.xml (Chinese Traditional) 2022-12-09 03:47:27 +01:00
Eugen Rochko
37502b3747 New translations strings.xml (Chinese Traditional) 2022-12-09 02:46:30 +01:00
Eugen Rochko
309e84d14c New translations strings.xml (Filipino) 2022-12-08 21:26:30 +01:00
Eugen Rochko
ff464bef9f New translations strings.xml (Hungarian) 2022-12-08 21:26:29 +01:00
Eugen Rochko
dfa5cd65f3 New translations strings.xml (Icelandic) 2022-12-08 21:26:28 +01:00
Eugen Rochko
ccba5969a5 New translations strings.xml (Belarusian) 2022-12-08 21:26:27 +01:00
Eugen Rochko
03baef713d New translations strings.xml (Slovenian) 2022-12-08 21:26:27 +01:00
Eugen Rochko
a3617349bb New translations strings.xml (Irish) 2022-12-08 21:26:26 +01:00
Eugen Rochko
e57b22d2fc New translations strings.xml (Romanian) 2022-12-08 21:26:25 +01:00
Eugen Rochko
6aabaa497d New translations strings.xml (Bengali) 2022-12-08 21:26:24 +01:00
Eugen Rochko
6caa142ead New translations strings.xml (Hindi) 2022-12-08 21:26:23 +01:00
Eugen Rochko
b01e6e30a4 New translations strings.xml (Scottish Gaelic) 2022-12-08 21:26:21 +01:00
Eugen Rochko
5e3a612828 New translations strings.xml (Sinhala) 2022-12-08 21:26:19 +01:00
Eugen Rochko
b498e7e83e New translations strings.xml (Indonesian) 2022-12-08 21:26:18 +01:00
Eugen Rochko
7508643c89 New translations strings.xml (Dutch) 2022-12-08 21:26:17 +01:00
Eugen Rochko
24d2189399 New translations strings.xml (Kabyle) 2022-12-08 21:26:17 +01:00
Eugen Rochko
4e470f34fd New translations strings.xml (Occitan) 2022-12-08 21:26:16 +01:00
Eugen Rochko
e3ca6448f2 New translations strings.xml (Bosnian) 2022-12-08 21:26:15 +01:00
Eugen Rochko
9dadac7d93 New translations strings.xml (Croatian) 2022-12-08 21:26:14 +01:00
Eugen Rochko
0715bd0aba New translations strings.xml (Thai) 2022-12-08 21:26:13 +01:00
Eugen Rochko
92772e7ee0 New translations strings.xml (Galician) 2022-12-08 21:26:12 +01:00
Eugen Rochko
f9f6c879e0 New translations strings.xml (Vietnamese) 2022-12-08 21:26:11 +01:00
Eugen Rochko
ec7623f5c5 New translations strings.xml (Chinese Simplified) 2022-12-08 21:26:10 +01:00
Eugen Rochko
0163242258 New translations strings.xml (Ukrainian) 2022-12-08 21:26:09 +01:00
Eugen Rochko
3f9c8247c6 New translations strings.xml (Turkish) 2022-12-08 21:26:08 +01:00
Eugen Rochko
20865ad202 New translations strings.xml (Swedish) 2022-12-08 21:26:06 +01:00
Eugen Rochko
b5ac895b15 New translations strings.xml (Russian) 2022-12-08 21:26:05 +01:00
Eugen Rochko
fbd550228b New translations strings.xml (Portuguese) 2022-12-08 21:26:04 +01:00
Eugen Rochko
238758fc0b New translations strings.xml (Korean) 2022-12-08 21:26:03 +01:00
Eugen Rochko
328a4339a4 New translations strings.xml (Japanese) 2022-12-08 21:26:02 +01:00
Eugen Rochko
747d958507 New translations strings.xml (Italian) 2022-12-08 21:26:01 +01:00
Eugen Rochko
8622160e62 New translations strings.xml (Armenian) 2022-12-08 21:26:00 +01:00
Eugen Rochko
df1042e87d New translations strings.xml (Hebrew) 2022-12-08 21:25:59 +01:00
Eugen Rochko
a5c6c11f09 New translations strings.xml (Finnish) 2022-12-08 21:25:58 +01:00
Eugen Rochko
7bd602cd45 New translations strings.xml (Basque) 2022-12-08 21:25:57 +01:00
Eugen Rochko
2065468f1f New translations strings.xml (Greek) 2022-12-08 21:25:56 +01:00
Eugen Rochko
7f9061d0c8 New translations strings.xml (Czech) 2022-12-08 21:25:55 +01:00
Eugen Rochko
00a638393e New translations strings.xml (Catalan) 2022-12-08 21:25:54 +01:00
Eugen Rochko
f21194f877 New translations strings.xml (Spanish) 2022-12-08 21:25:53 +01:00
Eugen Rochko
1772351fc5 New translations strings.xml (French) 2022-12-08 21:25:52 +01:00
Eugen Rochko
1030fc5e16 New translations strings.xml (Arabic) 2022-12-08 21:25:51 +01:00
Eugen Rochko
bef3ae96f6 New translations strings.xml (German) 2022-12-08 21:25:51 +01:00
Eugen Rochko
c9fa5b2104 New translations strings.xml (Chinese Traditional) 2022-12-08 21:25:50 +01:00
Eugen Rochko
79cd8c0805 New translations strings.xml (Polish) 2022-12-08 21:25:48 +01:00
Eugen Rochko
2b80420794 New translations strings.xml (Portuguese, Brazilian) 2022-12-08 21:25:47 +01:00
Eugen Rochko
b8e18613b1 New translations full_description.txt (French) 2022-12-08 18:43:26 +01:00
Eugen Rochko
87289e4804 New translations strings.xml (Ukrainian) 2022-12-08 18:43:25 +01:00
Eugen Rochko
ba3a06a782 New translations full_description.txt (Ukrainian) 2022-12-08 15:57:19 +01:00
Eugen Rochko
2764ef0417 New translations strings.xml (Ukrainian) 2022-12-08 15:57:18 +01:00
Eugen Rochko
ae0e89aa31 New translations full_description.txt (Catalan) 2022-12-08 13:36:42 +01:00
Eugen Rochko
d0e99cc517 New translations strings.xml (Catalan) 2022-12-08 13:36:41 +01:00
Eugen Rochko
604fb01d6c New translations strings.xml (Catalan) 2022-12-08 12:29:42 +01:00
Eugen Rochko
256a1687d1 New translations full_description.txt (Chinese Traditional) 2022-12-07 12:56:51 +01:00
Eugen Rochko
ea1ae58e54 New translations strings.xml (Chinese Traditional) 2022-12-07 12:56:50 +01:00
Eugen Rochko
fe9d119fe2 New translations strings.xml (Chinese Traditional) 2022-12-07 11:55:33 +01:00
Eugen Rochko
4a199533c1 New translations short_description.txt (Vietnamese) 2022-12-07 02:58:56 +01:00
Eugen Rochko
7e785f1b6c New translations strings.xml (Ukrainian) 2022-12-07 02:58:55 +01:00
Eugen Rochko
85b4824ea2 New translations strings.xml (Russian) 2022-12-07 02:58:54 +01:00
Eugen Rochko
d39af74bcc New translations strings.xml (Italian) 2022-12-07 00:52:19 +01:00
Eugen Rochko
bfb52af454 New translations full_description.txt (Italian) 2022-12-06 23:44:22 +01:00
Eugen Rochko
5630e5d488 New translations strings.xml (Italian) 2022-12-06 23:44:21 +01:00
Eugen Rochko
29780ecf22 New translations full_description.txt (Icelandic) 2022-12-06 19:46:01 +01:00
Eugen Rochko
a8b542feaa New translations full_description.txt (Kabyle) 2022-12-06 17:09:14 +01:00
Eugen Rochko
e85b182da7 New translations full_description.txt (Occitan) 2022-12-06 17:09:13 +01:00
Eugen Rochko
84e9195869 New translations full_description.txt (Scottish Gaelic) 2022-12-06 17:09:12 +01:00
Eugen Rochko
7a739457c9 New translations full_description.txt (Sinhala) 2022-12-06 17:09:11 +01:00
Eugen Rochko
30905a7c36 New translations full_description.txt (Bosnian) 2022-12-06 17:09:10 +01:00
Eugen Rochko
0326a6834a New translations full_description.txt (Hindi) 2022-12-06 17:09:09 +01:00
Eugen Rochko
7c75a67f9f New translations full_description.txt (Croatian) 2022-12-06 17:09:06 +01:00
Eugen Rochko
1aa0fbf7d5 New translations full_description.txt (Thai) 2022-12-06 17:09:04 +01:00
Eugen Rochko
9adce93645 New translations full_description.txt (Bengali) 2022-12-06 17:09:03 +01:00
Eugen Rochko
b7392ef62d New translations full_description.txt (Indonesian) 2022-12-06 17:09:02 +01:00
Eugen Rochko
6b55c90a93 New translations full_description.txt (Portuguese, Brazilian) 2022-12-06 17:09:00 +01:00
Eugen Rochko
9e9cd9ea4e New translations full_description.txt (Galician) 2022-12-06 17:08:59 +01:00
Eugen Rochko
ef91fb9e06 New translations full_description.txt (Vietnamese) 2022-12-06 17:08:57 +01:00
Eugen Rochko
2f49c525b6 New translations full_description.txt (Chinese Traditional) 2022-12-06 17:08:56 +01:00
Eugen Rochko
f78f179071 New translations full_description.txt (Chinese Simplified) 2022-12-06 17:08:55 +01:00
Eugen Rochko
6ed310f8ce New translations full_description.txt (Ukrainian) 2022-12-06 17:08:54 +01:00
Eugen Rochko
8da5a32b48 New translations full_description.txt (Turkish) 2022-12-06 17:08:53 +01:00
Eugen Rochko
74fcdaa223 New translations full_description.txt (Swedish) 2022-12-06 17:08:52 +01:00
Eugen Rochko
6b77b8fbbb New translations full_description.txt (Slovenian) 2022-12-06 17:08:51 +01:00
Eugen Rochko
9f9fdca53d New translations full_description.txt (Russian) 2022-12-06 17:08:50 +01:00
Eugen Rochko
540317017f New translations full_description.txt (Portuguese) 2022-12-06 17:08:47 +01:00
Eugen Rochko
567174fcde New translations full_description.txt (Polish) 2022-12-06 17:08:44 +01:00
Eugen Rochko
262bc1dcbe New translations full_description.txt (Dutch) 2022-12-06 17:08:43 +01:00
Eugen Rochko
b931928434 New translations full_description.txt (Korean) 2022-12-06 17:08:42 +01:00
Eugen Rochko
7e2057a847 New translations full_description.txt (Japanese) 2022-12-06 17:08:40 +01:00
Eugen Rochko
4a9b98f534 New translations full_description.txt (Italian) 2022-12-06 17:08:39 +01:00
Eugen Rochko
7bf45581e3 New translations full_description.txt (Armenian) 2022-12-06 17:08:38 +01:00
Eugen Rochko
61a7fe6217 New translations full_description.txt (Hebrew) 2022-12-06 17:08:37 +01:00
Eugen Rochko
efa1a3f14f New translations full_description.txt (Irish) 2022-12-06 17:08:36 +01:00
Eugen Rochko
925866c3f0 New translations full_description.txt (Finnish) 2022-12-06 17:08:35 +01:00
Eugen Rochko
7291ec6f88 New translations full_description.txt (Basque) 2022-12-06 17:08:34 +01:00
Eugen Rochko
16c9203956 New translations full_description.txt (Greek) 2022-12-06 17:08:33 +01:00
Eugen Rochko
b08f104663 New translations full_description.txt (Catalan) 2022-12-06 17:08:32 +01:00
Eugen Rochko
82b7c6c290 New translations full_description.txt (Arabic) 2022-12-06 17:08:31 +01:00
Eugen Rochko
63009a332f New translations full_description.txt (Spanish) 2022-12-06 17:08:29 +01:00
Eugen Rochko
95e56db159 New translations full_description.txt (French) 2022-12-06 17:08:28 +01:00
Eugen Rochko
48f981036b New translations full_description.txt (Romanian) 2022-12-06 17:08:27 +01:00
Eugen Rochko
79be77f986 New translations full_description.txt (Filipino) 2022-12-06 17:08:26 +01:00
Eugen Rochko
4fafab19fc New translations full_description.txt (Hungarian) 2022-12-06 17:08:25 +01:00
Eugen Rochko
cd71f6e858 New translations full_description.txt (Icelandic) 2022-12-06 17:08:24 +01:00
Eugen Rochko
63d5068c2c New translations full_description.txt (Belarusian) 2022-12-06 17:08:21 +01:00
Eugen Rochko
a9bc7fdeb7 New translations full_description.txt (Czech) 2022-12-06 17:08:20 +01:00
Eugen Rochko
89dc2608bc New translations full_description.txt (German) 2022-12-06 17:08:19 +01:00
430 changed files with 13226 additions and 2971 deletions

View File

@@ -8,25 +8,35 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
**To reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Does this happen in the official app?**
Does this issue also occur with the respective upstream release?
(Please test using the respective `upstream-xxxxxx.apk` provided in [Releases](https://github.com/sk22/megalodon/releases) or at least using the current Mastodon version from the Play Store)
> No / Yes
> In case it does, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead.
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
**Screenshots and screen recordings**
If applicable, add screenshots (and screen recordings, if possible) to help explain your problem.
**Version**
Megalodon version: [e.g. v1.1.4+fork.#]
**Additional context**
- Does this issue also occur with the respective upstream release? (Please test using the respective `upstream-xxxxxx.apk` provided in [Releases](https://github.com/sk22/megalodon/releases)) No / Yes (`mastodon#…`)
> In this case, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead. If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
**Crash log**
If you know your way around Android development tools, please consider attaching a crash log, if possible.

View File

@@ -152,6 +152,8 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Display server announcements](https://github.com/sk22/megalodon/commit/84179bc207d6b69cc2a770a3c28fa0a39b0b54e8)
* [Create](https://github.com/sk22/megalodon/commit/294595513a45037359b31377aafc25ae5b58d8e7), [edit](https://github.com/sk22/megalodon/commit/d47797bf7ac8cff3f9ba1cfee219a1bb2af21da6) and [delete](https://github.com/sk22/megalodon/commit/54c29fd787fc2cd0dfd2787ad796b8190f795973) lists
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
* Support for local-only posts
### Behavior
@@ -175,6 +177,8 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Preserve whitespaces in HTML](https://github.com/sk22/megalodon/commit/7d876bddc7a07d98f0fecbf62b13bdb9fcce3412)
* [Long-click to copy links](https://github.com/sk22/megalodon/commit/b32e32274923a94742a9926ef38785f746d41405)
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
* [Support admin notifications](https://github.com/sk22/megalodon/commit/c12a6eaee6b609bc53eb0a45d9199f37d5241801) and [notifications for edited reblogged posts](https://github.com/sk22/megalodon/commit/900e8fb2e9353002c16d15e06b78d2731e121601)
* [Android file opener added back in addition to image picker](https://github.com/sk22/megalodon/commit/3a6ace53d5ab01e28077c9c930cb6ed487b78031)
### Visual
@@ -190,6 +194,8 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Animations for interaction buttons](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/animate-buttons)
* [Dedicated icons for different notification types](https://github.com/sk22/megalodon/pull/178) by [@florian-obernberger](https://github.com/florian-obernberger)
* Scale text according to system settings
* Header in timeline for followed hashtags
* [Indicator for missing alt texts](https://github.com/sk22/megalodon/commit/c0c276f03e793b78c478c17dfdef24a66ef7cedb)
## Building

View File

@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
android.enableJetifier=false

View File

@@ -9,10 +9,10 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 67
versionName "1.1.5+fork.67"
versionCode 77
versionName "1.2.0+fork.77"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -70,6 +70,7 @@ dependencies {
implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'
implementation 'org.parceler:parceler-api:1.1.12'
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'

View File

@@ -14,12 +14,14 @@ import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
@@ -113,64 +115,70 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
private void actuallyCheckForUpdates(){
Request req=new Request.Builder()
.url("https://api.github.com/repos/sk22/megalodon/releases/latest")
.url("https://api.github.com/repos/sk22/megalodon/releases")
.build();
Call call=MastodonAPIController.getHttpClient().newCall(req);
try(Response resp=call.execute()){
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
String tag=obj.get("tag_name").getAsString();
String changelog=obj.get("body").getAsString();
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
return;
}
int newMajor=Integer.parseInt(matcher.group(1)),
newMinor=Integer.parseInt(matcher.group(2)),
newRevision=Integer.parseInt(matcher.group(3)),
newForkNumber=Integer.parseInt(matcher.group(4));
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
return;
}
int curMajor=Integer.parseInt(matcher.group(1)),
curMinor=Integer.parseInt(matcher.group(2)),
curRevision=Integer.parseInt(matcher.group(3)),
curForkNumber=Integer.parseInt(matcher.group(4));
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
if(newVersion>curVersion || newForkNumber>curForkNumber){
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
if("megalodon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
long size=asset.get("size").getAsLong();
String url=asset.get("browser_download_url").getAsString();
JsonArray arr=JsonParser.parseReader(resp.body().charStream()).getAsJsonArray();
for (JsonElement jsonElement : arr) {
JsonObject obj = jsonElement.getAsJsonObject();
if (obj.get("prerelease").getAsBoolean() && !GlobalUserPreferences.enablePreReleases) continue;
UpdateInfo info=new UpdateInfo();
info.size=size;
info.version=version;
info.changelog=changelog;
this.info=info;
String tag=obj.get("tag_name").getAsString();
String changelog=obj.get("body").getAsString();
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
return;
}
int newMajor=Integer.parseInt(matcher.group(1)),
newMinor=Integer.parseInt(matcher.group(2)),
newRevision=Integer.parseInt(matcher.group(3)),
newForkNumber=Integer.parseInt(matcher.group(4));
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
return;
}
int curMajor=Integer.parseInt(matcher.group(1)),
curMinor=Integer.parseInt(matcher.group(2)),
curRevision=Integer.parseInt(matcher.group(3)),
curForkNumber=Integer.parseInt(matcher.group(4));
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
if(newVersion>curVersion || newForkNumber>curForkNumber){
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
if("megalodon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
long size=asset.get("size").getAsLong();
String url=asset.get("browser_download_url").getAsString();
getPrefs().edit()
.putLong("apkSize", size)
.putString("version", version)
.putString("apkURL", url)
.putString("changelog", changelog)
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
.remove("downloadID")
.apply();
UpdateInfo info=new UpdateInfo();
info.size=size;
info.version=version;
info.changelog=changelog;
this.info=info;
break;
getPrefs().edit()
.putLong("apkSize", size)
.putString("version", version)
.putString("apkURL", url)
.putString("changelog", changelog)
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
.remove("downloadID")
.apply();
break;
}
}
}
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
break;
}
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
}catch(Exception x){
Log.w(TAG, "actuallyCheckForUpdates", x);
}finally{

View File

@@ -12,6 +12,13 @@
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
<application
android:name=".MastodonApp"
android:allowBackup="true"

View File

@@ -83,3 +83,7 @@ mirr0r.city underage
nnia.space underage
ignorelist.com malicious
repl.co malicious
# custom
pawoo.net csam
1 # lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
83
84
85
86
87
88
89

View File

@@ -8,10 +8,14 @@ import android.content.SharedPreferences;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class GlobalUserPreferences{
public static boolean playGifs;
@@ -20,7 +24,7 @@ public class GlobalUserPreferences{
public static boolean showReplies;
public static boolean showBoosts;
public static boolean loadNewPosts;
public static boolean showFederatedTimeline;
public static boolean showNewPostsButton;
public static boolean showInteractionCounts;
public static boolean alwaysExpandContentWarnings;
public static boolean disableMarquee;
@@ -31,18 +35,32 @@ public class GlobalUserPreferences{
public static boolean uniformNotificationIcon;
public static boolean reduceMotion;
public static boolean keepOnlyLatestNotification;
public static boolean disableAltTextReminder;
public static boolean showAltIndicator;
public static boolean showNoAltIndicator;
public static boolean enablePreReleases;
public static boolean prefixRepliesWithRe;
public static boolean bottomEncoding;
public static boolean collapseLongPosts;
public static boolean spectatorMode;
public static boolean autoHideFab;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> accountsInGlitchMode;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
}
private static <T> T fromJson(String json, Type type, T orElse) {
if (json == null) return orElse;
try { return gson.fromJson(json, type); }
catch (JsonSyntaxException ignored) { return orElse; }
}
@@ -55,7 +73,7 @@ public class GlobalUserPreferences{
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease"));
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
@@ -66,9 +84,21 @@ public class GlobalUserPreferences{
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
reduceMotion=prefs.getBoolean("reduceMotion", false);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false);
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false);
autoHideFab=prefs.getBoolean("autoHideFab", true);
publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>());
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -85,7 +115,7 @@ public class GlobalUserPreferences{
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)
.putBoolean("loadNewPosts", loadNewPosts)
.putBoolean("showFederatedTimeline", showFederatedTimeline)
.putBoolean("showNewPostsButton", showNewPostsButton)
.putBoolean("trueBlackTheme", trueBlackTheme)
.putBoolean("showInteractionCounts", showInteractionCounts)
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
@@ -96,10 +126,22 @@ public class GlobalUserPreferences{
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
.putBoolean("reduceMotion", reduceMotion)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("disableAltTextReminder", disableAltTextReminder)
.putBoolean("showAltIndicator", showAltIndicator)
.putBoolean("showNoAltIndicator", showNoAltIndicator)
.putBoolean("enablePreReleases", enablePreReleases)
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
.putBoolean("collapseLongPosts", collapseLongPosts)
.putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab)
.putString("publishButtonText", publishButtonText)
.putBoolean("bottomEncoding", bottomEncoding)
.putInt("theme", theme.ordinal())
.putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages))
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.apply();
}

View File

@@ -39,12 +39,13 @@ public class MainActivity extends FragmentStackActivity{
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.getBooleanExtra("fromNotification", false)){
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(!intent.hasExtra("notification"))
args.putString("tab", "notifications");
if(!hasNotification) args.putString("tab", "notifications");
}catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
@@ -54,13 +55,13 @@ public class MainActivity extends FragmentStackActivity{
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
if(fromNotification && hasNotification){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
} else if (intent.getBooleanExtra("compose", false)){
showCompose();
}else{
} else {
showFragmentClearingBackStack(fragment);
maybeRequestNotificationsPermission();
}
}
@@ -139,4 +140,31 @@ public class MainActivity extends FragmentStackActivity{
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
}
}
/**
* when opening app through a notification: if (thread) fragment "can go back", clear back stack
* and show home fragment. upstream's implementation doesn't require this as it opens home first
* and then immediately switches to the notification's ThreadFragment. this causes a black
* screen in megalodon, for some reason, so i'm working around this that way.
*/
@Override
public void onBackPressed() {
Fragment currentFragment = getFragmentManager().findFragmentById(
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
);
Bundle currentArgs = currentFragment.getArguments();
if (this.fragmentContainers.size() == 1
&& currentArgs != null
&& currentArgs.getBoolean("_can_go_back", false)
&& currentArgs.containsKey("account")) {
Bundle args = new Bundle();
args.putString("account", currentArgs.getString("account"));
args.putString("tab", "notifications");
Fragment fragment=new HomeFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
} else {
super.onBackPressed();
}
}
}

View File

@@ -144,12 +144,18 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setAutoCancel(true)
.setColor(context.getColor(R.color.primary_700));
if (!GlobalUserPreferences.uniformNotificationIcon) switch (pn.notificationType) {
case FAVORITE -> builder.setSmallIcon(R.drawable.ic_fluent_star_24_filled);
case REBLOG -> builder.setSmallIcon(R.drawable.ic_fluent_arrow_repeat_all_24_filled);
case FOLLOW -> builder.setSmallIcon(R.drawable.ic_fluent_person_add_24_filled);
case MENTION -> builder.setSmallIcon(R.drawable.ic_fluent_mention_24_filled);
case POLL -> builder.setSmallIcon(R.drawable.ic_fluent_poll_24_filled);
if (!GlobalUserPreferences.uniformNotificationIcon) {
builder.setSmallIcon(switch (pn.notificationType) {
case FAVORITE -> R.drawable.ic_fluent_star_24_filled;
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled;
case MENTION -> R.drawable.ic_fluent_mention_24_filled;
case POLL -> R.drawable.ic_fluent_poll_24_filled;
case STATUS -> R.drawable.ic_fluent_chat_24_filled;
case UPDATE -> R.drawable.ic_fluent_history_24_filled;
case REPORT -> R.drawable.ic_fluent_warning_24_filled;
case SIGN_UP -> R.drawable.ic_fluent_person_available_24_filled;
});
}
if(avatar!=null){

View File

@@ -4,6 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
this(id, followed, showReblogs, false);
}
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
if(followed)

View File

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

View File

@@ -9,7 +9,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
Request r=new Request();
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
r.data.alerts=alerts;
r.data.policy=policy;
r.policy=policy;
r.subscription.keys.p256dh=encryptionKey;
r.subscription.keys.auth=authKey;
setRequestBody(r);
@@ -18,6 +18,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Request{
public Subscription subscription=new Subscription();
public Data data=new Data();
public PushSubscription.Policy policy;
private static class Keys{
public String p256dh;
@@ -31,7 +32,6 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Data{
public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
}
}
}

View File

@@ -3,23 +3,36 @@ package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription;
import java.io.IOException;
import okhttp3.Response;
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
private final PushSubscription.Policy policy;
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
setRequestBody(new Request(alerts, policy));
this.policy=policy;
}
@Override
public void validateAndPostprocessResponse(PushSubscription respObj, Response httpResponse) throws IOException{
super.validateAndPostprocessResponse(respObj, httpResponse);
respObj.policy=policy;
}
private static class Request{
public Data data=new Data();
public PushSubscription.Policy policy;
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
this.data.alerts=alerts;
this.data.policy=policy;
this.policy=policy;
}
private static class Data{
public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
}
}
}

View File

@@ -39,6 +39,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public Poll poll;
public String inReplyToId;
public boolean sensitive;
public boolean localOnly;
public String spoilerText;
public StatusPrivacy visibility;
public Instant scheduledAt;

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class HashtagUpdatedEvent {
public final String name;
public final boolean following;
public HashtagUpdatedEvent(String name, boolean following) {
this.name = name;
this.following = following;
}
}

View File

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

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ListTimeline;
public class ListUpdatedCreatedEvent {
public final String id;
public final String title;
public final ListTimeline.RepliesPolicy repliesPolicy;
public ListUpdatedCreatedEvent(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
this.id = id;
this.title = title;
this.repliesPolicy = repliesPolicy;
}
}

View File

@@ -3,6 +3,10 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.TranslateAnimation;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -11,12 +15,15 @@ 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.Filter;
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.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
@@ -56,8 +63,8 @@ public class AccountTimelineFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null)
return;
if(getActivity()==null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})

View File

@@ -66,7 +66,7 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail;
instanceUser.emojis = List.of();
Status fakeStatus = a.toStatus();
TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus);
TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus, true);
textItem.textSelectable = true;
return List.of(
HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead),
@@ -77,12 +77,7 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
public void onMarkAsRead(String id) {
if (unreadIDs == null) return;
unreadIDs.remove(id);
if (unreadIDs.size() == 0) setResult(true, null);
}
@Override
public void onDestroy() {
super.onDestroy();
if (unreadIDs.isEmpty()) setResult(true, null);
}
@Override
@@ -97,11 +92,13 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Announcement> result){
if (getActivity() == null) return;
List<Announcement> unread = result.stream().filter(a -> !a.read).collect(toList());
List<Announcement> read = result.stream().filter(a -> a.read).collect(toList());
onDataLoaded(unread, true);
onDataLoaded(read, false);
unreadIDs = unread.stream().map(a -> a.id).collect(toList());
if (unread.isEmpty()) setResult(true, null);
else unreadIDs = unread.stream().map(a -> a.id).collect(toList());
}
})
.exec(accountID);

View File

@@ -16,6 +16,8 @@ import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
import android.widget.Toolbar;
import org.joinmastodon.android.E;
@@ -30,20 +32,21 @@ import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.TileGridLayoutManager;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
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.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout;
import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
import java.util.Collections;
@@ -54,8 +57,9 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
@@ -71,12 +75,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected DisplayItemsAdapter adapter;
protected String accountID;
protected PhotoViewer currentPhotoViewer;
protected ImageButton fab;
protected int scrollDiff = 0;
protected HashMap<String, Account> knownAccounts=new HashMap<>();
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
public BaseStatusListFragment(){
super(20);
if (withComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab);
}
protected boolean withComposeButton() {
return false;
}
@Override
@@ -90,6 +102,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
setRetainInstance(true);
}
@Override
protected RecyclerView.Adapter getAdapter(){
return adapter=new DisplayItemsAdapter();
@@ -176,21 +190,21 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
@Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){
final Status status=_status.reblog!=null ? _status.reblog : _status;
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
private ImageStatusDisplayItem.Holder<?> transitioningHolder;
private MediaAttachmentViewController transitioningHolder;
@Override
public void setPhotoViewVisibility(int index, boolean visible){
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null)
holder.photo.setAlpha(visible ? 1f : 0f);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
@@ -198,7 +212,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
view.getLocationOnScreen(pos);
outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight());
list.setClipChildren(false);
transitioningHolder.itemView.setElevation(1f);
gridHolder.setClipChildren(false);
transitioningHolder.view.setElevation(1f);
return true;
}
return false;
@@ -225,15 +240,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
view.setTranslationY(0f);
view.setScaleX(1f);
view.setScaleY(1f);
transitioningHolder.itemView.setElevation(0f);
transitioningHolder.view.setElevation(0f);
if(list!=null)
list.setClipChildren(true);
gridHolder.setClipChildren(true);
transitioningHolder=null;
}
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null)
return holder.photo.getDrawable();
return null;
@@ -249,23 +265,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
}
private ImageStatusDisplayItem.Holder<?> findPhotoViewHolder(int index){
if(list==null)
return null;
int offset=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(parentID)){
if(item instanceof ImageStatusDisplayItem){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
return imgHolder;
}
return null;
}
}
offset++;
}
return null;
private MediaAttachmentViewController findPhotoViewHolder(int index){
return gridHolder.getViewController(index);
}
});
}
@@ -273,11 +274,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(currentPhotoViewer!=null)
currentPhotoViewer.offsetView(-dx, -dy);
if (fab!=null && GlobalUserPreferences.autoHideFab) {
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
TranslateAnimation animate = new TranslateAnimation(
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
animate.setFillAfter(true);
fab.startAnimation(animate);
fab.setVisibility(View.INVISIBLE);
scrollDiff = 0;
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
if (list.getChildLayoutPosition(list.getChildAt(0)) == 0 || scrollDiff > 400) {
fab.setVisibility(View.VISIBLE);
TranslateAnimation animate = new TranslateAnimation(
0,
0,
fab.getHeight() * 2,
0);
animate.setDuration(300);
animate.setFillAfter(true);
fab.startAnimation(animate);
scrollDiff = 0;
} else {
scrollDiff += Math.abs(dy);
}
}
}
}
});
list.addItemDecoration(new StatusListItemDecoration());
@@ -313,31 +345,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
list.setItemAnimator(new BetterItemAnimator());
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
updateToolbar();
}
@Override
protected RecyclerView.LayoutManager onCreateLayoutManager(){
GridLayoutManager lm=new TileGridLayoutManager(getActivity(), 1000);
lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position){
position-=getMainAdapterOffset();
if(position>=0 && position<displayItems.size()){
StatusDisplayItem item=displayItems.get(position);
if(item instanceof ImageStatusDisplayItem imgItem){
PhotoLayoutHelper.TiledLayoutResult layout=imgItem.tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgItem.thisTile;
int spans=0;
for(int i=0;i<tile.colSpan;i++){
spans+=layout.columnSizes[tile.startCol+i];
}
return spans;
}
}
return 1000;
}
});
return lm;
if (withComposeButton()) {
fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(this::onFabLongClick);
}
}
@Override
@@ -458,7 +471,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
revealSpoiler(status, holder.getItemID());
}
public void onRevealSpoilerClick(ImageStatusDisplayItem.Holder<?> holder){
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID());
}
@@ -478,7 +491,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
Status status=holder.getItem().status;
status.spoilerRevealed=!status.spoilerRevealed;
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
TextStatusDisplayItem.Holder text = findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
@@ -487,15 +500,33 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
updateImagesSpoilerState(status, holder.getItemID());
}
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
if (holder.getItem().status.textExpandable != expandable && list != null) {
holder.getItem().status.textExpandable = expandable;
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if (header != null) header.rebind();
holder.rebind();
}
}
public void onToggleExpanded(Status status, String itemID) {
status.textExpanded = !status.textExpanded;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if (text != null) text.rebind();
if (header != null) header.rebind();
}
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(status.spoilerRevealed);
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
mediaGrid.setRevealed(status.spoilerRevealed);
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
@@ -504,6 +535,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
int startPos = warning.getAbsoluteAdapterPosition();
displayItems.remove(startPos);
displayItems.addAll(startPos, warning.filteredItems);
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
if (startPos == 0) scrollToTop();
warning.getItem().status.filterRevealed = true;
}
public String getAccountID(){
return accountID;
}
@@ -619,6 +659,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer.onPause();
}
protected void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
}
protected boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID);
}
private MediaAttachmentViewController makeNewMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){
return new MediaAttachmentViewController(getActivity(), type);
}
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> getAttachmentViewsPool(){
return attachmentViewsPool;
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){
@@ -656,16 +715,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public ImageLoaderRequest getImageRequest(int position, int image){
return displayItems.get(position).getImageRequest(image);
}
// @Override
// public void onViewDetachedFromWindow(@NonNull BindableViewHolder<StatusDisplayItem> holder){
// if(holder instanceof ImageLoaderViewHolder){
// int count=holder.getItem().getImageCount();
// for(int i=0;i<count;i++){
// ((ImageLoaderViewHolder) holder).clearImage(i);
// }
// }
// }
}
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
@@ -699,25 +748,21 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
hiddenMediaPaint.setColor(0x80000000);
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
float hGap=tile.startCol>0 ? V.dp(1) : 0;
float vGap=tile.startRow>0 ? V.dp(1) : 0;
c.drawRect(child.getX()-hGap, child.getY()-vGap, child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
}
}
}
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
if(tile.startCol==0 && tile.startRow==0 && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH));
int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH));
if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32));
c.save();
@@ -742,47 +787,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
@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 ImageStatusDisplayItem.Holder){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH));
PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem.Holder<?>) holder).getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem.Holder<?>) holder).getItem().thisTile;
if(tile.startCol+tile.colSpan<layout.columnSizes.length){
outRect.right=V.dp(1);
}
if(tile.startRow+tile.rowSpan<layout.rowSizes.length){
outRect.bottom=V.dp(1);
}
// For a view that spans rows, compensate its additional height so the row it's in stays the right height
if(tile.rowSpan>1){
outRect.bottom=-(Math.round(tile.height/1000f*width)-Math.round(layout.rowSizes[tile.startRow]/1000f*width));
}
// ...and for its siblings, offset those on rows below first to the right where they belong
if(tile.startCol>0 && layout.tiles[0].rowSpan>1 && tile.startRow>layout.tiles[0].startRow){
int xOffset=Math.round(layout.tiles[0].width/1000f*listWidth);
outRect.left=xOffset;
outRect.right=-xOffset;
}
// If the width of the media block is smaller than that of the RecyclerView, offset the views horizontally to center them
if(listWidth>width){
outRect.left+=(listWidth-V.dp(ImageAttachmentFrameLayout.MAX_WIDTH))/2;
if(tile.startCol>0){
int spanOffset=0;
for(int i=0;i<tile.startCol;i++){
spanOffset+=layout.columnSizes[i];
}
outRect.left-=Math.round(spanOffset/1000f*listWidth);
outRect.left+=Math.round(spanOffset/1000f*width);
}
}
}
}
private void rebuildMediaHiddenLayouts(int width){
currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content);

View File

@@ -25,6 +25,7 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Status> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else

View File

@@ -3,9 +3,9 @@ package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
import static org.joinmastodon.android.ui.utils.UiUtils.isPhotoPickerAvailable;
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
import static android.os.ext.SdkExtensions.getExtensionVersion;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
@@ -42,6 +42,7 @@ import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -66,6 +67,7 @@ import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.github.bottomSoftwareFoundation.bottom.Bottom;
import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E;
@@ -107,7 +109,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
import org.joinmastodon.android.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ComposeEditText;
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
@@ -115,6 +117,7 @@ import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import org.joinmastodon.android.utils.MastodonLanguage;
import org.joinmastodon.android.utils.StatusTextEncoder;
import org.parceler.Parcel;
import org.parceler.Parcels;
@@ -151,19 +154,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
private static final int MAX_ATTACHMENTS=4;
private static final String GLITCH_LOCAL_ONLY_SUFFIX = "👁";
private static final Pattern GLITCH_LOCAL_ONLY_PATTERN = Pattern.compile("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*");
private static final String TAG="ComposeFragment";
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
public static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
// from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
public static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
public static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
@SuppressLint("NewApi") // this class actually exists on 6.0
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
private SizeListenerLinearLayout contentView;
private TextView selfName, selfUsername;
private TextView selfName, selfUsername, selfExtraText, extraText;
private ImageView selfAvatar;
private Account self;
private String instanceDomain;
@@ -212,6 +217,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private View sendingOverlay;
private WindowManager wm;
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
private boolean localOnly;
private ComposeAutocompleteSpan currentAutocompleteSpan;
private FrameLayout mainEditTextWrap;
private ComposeAutocompleteViewController autocompleteViewController;
@@ -226,7 +232,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private boolean ignoreSelectionChanges=false;
private Runnable updateUploadEtaRunnable;
private String language;
private String language, encoding;
private MastodonLanguage.LanguageResolver languageResolver;
@Override
@@ -242,9 +248,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
languageResolver=new MastodonLanguage.LanguageResolver(instance);
redraftStatus=getArguments().getBoolean("redraftStatus", false);
if(getArguments().containsKey("editStatus")){
if(getArguments().containsKey("editStatus"))
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
}
if(getArguments().containsKey("replyTo"))
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
if(instance==null){
Nav.finish(this);
return;
@@ -302,6 +309,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
selfName=view.findViewById(R.id.self_name);
selfUsername=view.findViewById(R.id.self_username);
selfAvatar=view.findViewById(R.id.self_avatar);
selfExtraText=view.findViewById(R.id.self_extra_text);
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
selfUsername.setText('@'+self.username+'@'+instanceDomain);
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
@@ -327,10 +335,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
sensitiveItem=view.findViewById(R.id.sensitive_item);
replyText=view.findViewById(R.id.reply_text);
mediaBtn.setOnClickListener(v->openFilePicker());
if (isPhotoPickerAvailable()) {
PopupMenu attachPopup = new PopupMenu(getContext(), mediaBtn);
attachPopup.inflate(R.menu.attach);
attachPopup.setOnMenuItemClickListener(i -> {
openFilePicker(i.getItemId() == R.id.media);
return true;
});
UiUtils.enablePopupMenuIcons(getContext(), attachPopup);
mediaBtn.setOnClickListener(v->attachPopup.show());
mediaBtn.setOnTouchListener(attachPopup.getDragToOpenListener());
} else {
mediaBtn.setOnClickListener(v -> openFilePicker(false));
}
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
localOnly = savedInstanceState != null ? savedInstanceState.getBoolean("localOnly") :
editingStatus != null ? editingStatus.localOnly : replyTo != null && replyTo.localOnly;
buildVisibilityPopup(visibilityBtn);
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
@@ -405,6 +429,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable());
spoilerEdit.setBackground(spoilerBg);
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE);
spoilerBtn.setSelected(true);
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
@@ -448,7 +473,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private;
case LOCAL -> R.id.vis_local;
}).setChecked(true);
visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
@@ -475,6 +502,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected());
}
outState.putBoolean("sensitive", sensitive);
outState.putBoolean("localOnly", localOnly);
outState.putBoolean("hasSpoiler", hasSpoiler);
outState.putString("language", language);
if(!attachments.isEmpty()){
@@ -598,6 +626,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
});
View originalPost = view.findViewById(R.id.original_post);
extraText = view.findViewById(R.id.extra_text);
originalPost.setVisibility(View.VISIBLE);
originalPost.setOnClickListener(v->{
Bundle args=new Bundle();
@@ -630,9 +659,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
view.findViewById(R.id.visibility).setVisibility(View.GONE);
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
case LOCAL -> R.drawable.ic_fluent_eye_20_regular;
});
ImageView moreBtn = view.findViewById(R.id.more);
moreBtn.setImageDrawable(visibilityIcon);
@@ -656,6 +686,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
case LOCAL -> R.string.sk_local_only;
};
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
replyText.setOnClickListener(v->{
@@ -682,7 +713,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(!TextUtils.isEmpty(replyTo.spoilerText)){
hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE);
spoilerEdit.setText(replyTo.spoilerText);
if(GlobalUserPreferences.prefixRepliesWithRe && !replyTo.spoilerText.startsWith("re: ")){
spoilerEdit.setText("re: " + replyTo.spoilerText);
}else{
spoilerEdit.setText(replyTo.spoilerText);
}
spoilerBtn.setSelected(true);
}
if (replyTo.language != null && !replyTo.language.isEmpty()) updateLanguage(replyTo.language);
@@ -736,6 +771,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
updateSensitive();
updateHeaders();
if(editingStatus!=null){
updateCharCounter();
@@ -778,6 +814,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) {
// editing an already published post
draftsBtn.setVisibility(View.GONE);
}
}
private void navigateToUnsentPosts() {
@@ -801,9 +842,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void updateLanguage(MastodonLanguage loc) {
language = loc.getLanguage();
languageButton.setText(loc.getLanguageName());
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, loc.getDefaultName()));
updateLanguage(loc.getLanguage(), loc.getLanguageName(), loc.getDefaultName());
}
private void updateLanguage(String languageTag, String languageName, String defaultName) {
language = languageTag;
languageButton.setText(languageName);
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, defaultName));
}
@SuppressLint("ClickableViewAccessibility")
@@ -813,14 +858,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
btn.setOnClickListener(v->languagePopup.show());
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
if (language != null) updateLanguage(language);
else updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
? languageResolver.from(prefs.postingDefaultLanguage)
: languageResolver.getDefault());
Menu languageMenu = languagePopup.getMenu();
for (String recentLanguage : Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)) {
MastodonLanguage l = languageResolver.from(recentLanguage);
languageMenu.add(0, allLanguages.indexOf(l), Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
if (recentLanguage.equals("bottom")) {
addBottomLanguage(languageMenu);
} else {
MastodonLanguage l = languageResolver.from(recentLanguage);
languageMenu.add(0, allLanguages.indexOf(l), Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
}
}
SubMenu allLanguagesMenu = languageMenu.addSubMenu(R.string.sk_available_languages);
@@ -829,13 +879,33 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
allLanguagesMenu.add(0, i, Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
}
if (GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu);
btn.setOnLongClickListener(v->{
btn.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (!GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu);
return false;
});
languagePopup.setOnMenuItemClickListener(i->{
if (i.hasSubMenu()) return false;
updateLanguage(allLanguages.get(i.getItemId()));
if (i.getItemId() == allLanguages.size()) {
updateLanguage(language, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom");
encoding = "bottom";
} else {
updateLanguage(allLanguages.get(i.getItemId()));
encoding = null;
}
return true;
});
}
private void addBottomLanguage(Menu menu) {
if (menu.findItem(allLanguages.size()) == null) {
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
return true;
@@ -865,6 +935,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(hasSpoiler){
charCount+=spoilerEdit.length();
}
if (localOnly && GlobalUserPreferences.accountsInGlitchMode.contains(accountID)) {
charCount -= GLITCH_LOCAL_ONLY_SUFFIX.length();
}
charCounter.setText(String.valueOf(charLimit-charCount));
trimmedCharCount=text.toString().trim().length();
updatePublishButtonState();
@@ -898,9 +971,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void onCustomEmojiClick(Emoji emoji){
int start=mainEditText.getSelectionStart();
String prefix=start>0 && !Character.isWhitespace(mainEditText.getText().charAt(start-1)) ? " :" : ":";
mainEditText.getText().replace(start, mainEditText.getSelectionEnd(), prefix+emoji.shortcode+':');
if(getActivity().getCurrentFocus() instanceof EditText edit){
int start=edit.getSelectionStart();
String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":";
edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':');
}
}
@Override
@@ -951,15 +1026,53 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void publish(){
publish(false);
}
private void publish(boolean force){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
if ("bottom".equals(encoding)) {
text = new StatusTextEncoder(Bottom::encode).encode(text);
req.spoilerText = "bottom-encoded emoji spam";
}
if (localOnly &&
GlobalUserPreferences.accountsInGlitchMode.contains(accountID) &&
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) {
text += " " + GLITCH_LOCAL_ONLY_SUFFIX;
}
req.status=text;
req.visibility=statusVisibility;
req.localOnly=localOnly;
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
Optional<DraftMediaAttachment> withoutAltText = attachments.stream().filter(a -> a.description == null || a.description.isBlank()).findFirst();
boolean isDraft = scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT);
if (!force && !GlobalUserPreferences.disableAltTextReminder && !isDraft && withoutAltText.isPresent()) {
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_alt_text_missing_title)
.setMessage(R.string.sk_alt_text_missing)
.setPositiveButton(R.string.add_alt_text, (d, w) -> editMediaDescription(withoutAltText.get()))
.setNegativeButton(R.string.sk_publish_anyway, (d, w) -> publish(true))
.show();
return;
}
}
// ask whether to publish now when editing an existing draft
if (!force && editingStatus != null && scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_save_draft)
.setMessage(R.string.sk_save_draft_message)
.setPositiveButton(R.string.save, (d, w) -> publish(true))
.setNegativeButton(R.string.publish, (d, w) -> {
updateScheduledAt(null);
publish();
})
.show();
return;
}
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
@@ -1064,6 +1177,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
List<String> newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages));
newRecentLanguages.remove(language);
newRecentLanguages.add(0, language);
if (encoding != null) {
newRecentLanguages.remove(encoding);
newRecentLanguages.add(0, encoding);
}
if ("bottom".equals(encoding) && !GlobalUserPreferences.bottomEncoding) {
GlobalUserPreferences.bottomEncoding = true;
GlobalUserPreferences.save();
}
recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList()));
GlobalUserPreferences.save();
}
@@ -1129,7 +1250,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
boolean attachmentsPending = attachments.stream().anyMatch(att -> att.state != AttachmentUploadState.DONE);
if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_unfinished_attachments)
.setMessage(R.string.sk_unfinished_attachments_message)
.setPositiveButton(R.string.edit, (d, w) -> {})
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
.show();
else new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> {
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
@@ -1139,18 +1267,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
.show();
}
/**
* Check to see if Android platform photopicker is available on the device\
* @return whether the device supports photopicker intents.
*/
private boolean isPhotoPickerAvailable() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return true;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
} else
return false;
}
/**
* Builds the correct intent for the device version to select media.
@@ -1160,26 +1276,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
*
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
*/
private void openFilePicker(){
private void openFilePicker(boolean photoPicker){
Intent intent;
boolean usePhotoPicker = isPhotoPickerAvailable();
if (usePhotoPicker) {
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
} else {
intent = new Intent(Intent.ACTION_GET_CONTENT);
boolean usePhotoPicker=photoPicker && isPhotoPickerAvailable();
if(usePhotoPicker){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount());
}else{
intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
}
if (!usePhotoPicker && instance.configuration != null &&
instance.configuration.mediaAttachments != null &&
instance.configuration.mediaAttachments.supportedMimeTypes != null &&
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()) {
if(!usePhotoPicker && instance.configuration!=null &&
instance.configuration.mediaAttachments!=null &&
instance.configuration.mediaAttachments.supportedMimeTypes!=null &&
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
intent.putExtra(Intent.EXTRA_MIME_TYPES,
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
new String[0]));
} else {
if (!usePhotoPicker) {
}else{
if(!usePhotoPicker){
// If photo picker is being used these are the default mimetypes.
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
}
@@ -1524,6 +1640,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
if(att.serverAttachment==null)
return;
editMediaDescription(att);
}
private void editMediaDescription(DraftMediaAttachment att) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("attachment", att.serverAttachment.id);
@@ -1600,18 +1720,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
menu.getMenu().add(0, 2, 0, getResources().getQuantityString(R.plurals.x_minutes, 30, 30));
menu.getMenu().add(0, 3, 0, getResources().getQuantityString(R.plurals.x_hours, 1, 1));
menu.getMenu().add(0, 4, 0, getResources().getQuantityString(R.plurals.x_hours, 6, 6));
menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1));
menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3));
menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7));
menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_hours, 12, 12));
menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1));
menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3));
menu.getMenu().add(0, 8, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7));
menu.setOnMenuItemClickListener(item->{
pollDuration=switch(item.getItemId()){
case 1 -> 5*60;
case 2 -> 30*60;
case 3 -> 3600;
case 4 -> 6*3600;
case 5 -> 24*3600;
case 6 -> 3*24*3600;
case 7 -> 7*24*3600;
case 5 -> 12*3600;
case 6 -> 24*3600;
case 7 -> 3*24*3600;
case 8 -> 7*24*3600;
default -> throw new IllegalStateException("Unexpected value: "+item.getItemId());
};
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString()));
@@ -1705,12 +1827,33 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return attachments.size();
}
private void updateHeaders() {
UiUtils.setExtraTextInfo(getContext(), selfExtraText, statusVisibility, localOnly);
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, replyTo.visibility, replyTo.localOnly);
}
private void buildVisibilityPopup(View v){
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);
localOnlyItem.setChecked(localOnly);
Status status = editingStatus != null ? editingStatus : replyTo;
if (!prefsSaysSupported) {
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
if (GLITCH_LOCAL_ONLY_PATTERN.matcher(status.getStrippedText()).matches()) {
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
}
GlobalUserPreferences.save();
}
}
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
m.setGroupCheckable(0, true, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) m.setGroupDividerEnabled(true);
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
@Override
public boolean onMenuItemClick(MenuItem item){
@@ -1723,41 +1866,44 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT;
}else if(id==R.id.vis_local){
statusVisibility=StatusPrivacy.LOCAL;
}
if (id == R.id.local_only) {
localOnly = !item.isChecked();
item.setChecked(localOnly);
} else {
item.setChecked(true);
}
item.setChecked(true);
updateVisibilityIcon();
updateHeaders();
return true;
}
});
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
if(getArguments().containsKey("replyTo")){
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
statusVisibility = replyTo.visibility;
}
if(replyTo != null) statusVisibility = replyTo.visibility;
// A saved privacy setting from a previous compose session wins over the reply visibility
if(savedInstanceState !=null){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
AccountSessionManager asm = AccountSessionManager.getInstance();
Preferences prefs = asm.getAccount(accountID).preferences;
if (prefs != null) {
// Only override the reply visibility if our preference is more private
if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
statusVisibility = switch (prefs.postingDefaultVisibility) {
case PUBLIC -> StatusPrivacy.PUBLIC;
case UNLISTED -> StatusPrivacy.UNLISTED;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
// (and we're not replying to ourselves, or not at all)
if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility) &&
(replyTo == null || !asm.isSelf(accountID, replyTo.account))) {
statusVisibility = prefs.postingDefaultVisibility;
}
}
// A saved privacy setting from a previous compose session wins over all
if(savedInstanceState !=null){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
// A saved privacy setting from a previous compose session wins over all
if(savedInstanceState !=null){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
}
@@ -1767,9 +1913,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
visibilityBtn.setImageResource(switch(statusVisibility){
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_24_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_24_filled;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
case LOCAL -> R.drawable.ic_fluent_eye_24_regular;
});
}

View File

@@ -0,0 +1,352 @@
package org.joinmastodon.android.fragments;
import static android.view.Menu.NONE;
import static org.joinmastodon.android.ui.utils.UiUtils.makeBackItem;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends BaseRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
private String accountID;
private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper;
private Menu optionsMenu;
private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
private final List<ListTimeline> listTimelines = new ArrayList<>();
private final List<Hashtag> hashtags = new ArrayList<>();
public EditTimelinesFragment() {
super(10);
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
setTitle(R.string.sk_timelines);
accountID = getArguments().getString("account");
new GetLists().setCallback(new Callback<>() {
@Override
public void onSuccess(List<ListTimeline> result) {
listTimelines.addAll(result);
updateOptionsMenu();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID);
new GetFollowedHashtags().setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result) {
hashtags.addAll(result);
updateOptionsMenu();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
itemTouchHelper.attachToRecyclerView(list);
refreshLayout.setEnabled(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
this.optionsMenu = menu;
updateOptionsMenu();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_back) {
updateOptionsMenu();
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
return true;
}
TimelineDefinition tl = timelineByMenuItem.get(item);
if (tl != null) {
data.add(tl.copy());
adapter.notifyItemInserted(data.size());
saveTimelines();
updateOptionsMenu();
};
return true;
}
private void addTimelineToOptions(TimelineDefinition tl, Menu menu) {
if (data.contains(tl)) return;
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, tl.getTitle(getContext()));
item.setIcon(tl.getIcon().iconRes);
timelineByMenuItem.put(item, tl);
}
private void updateOptionsMenu() {
if (getActivity() == null) return;
optionsMenu.clear();
timelineByMenuItem.clear();
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
makeBackItem(timelinesMenu);
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
listsMenu.getItem().setVisible(listsMenu.size() > 0);
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0);
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
}
private void saveTimelines() {
updated = true;
GlobalUserPreferences.pinnedTimelines.put(accountID, data.size() > 0 ? data : List.of(TimelineDefinition.HOME_TIMELINE));
GlobalUserPreferences.save();
}
private void removeTimeline(int position) {
data.remove(position);
adapter.notifyItemRemoved(position);
saveTimelines();
updateOptionsMenu();
}
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
updateOptionsMenu();
}
@Override
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
return adapter = new TimelinesAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public void onDestroy() {
super.onDestroy();
if (updated) UiUtils.restartApp();
}
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
@NonNull
@Override
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new TimelineViewHolder();
}
@Override
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
private final TextView title;
private final ImageView dragger;
public TimelineViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
dragger=findViewById(R.id.dragger_thingy);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBind(TimelineDefinition item) {
title.setText(item.getTitle(getContext()));
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
dragger.setVisibility(View.VISIBLE);
dragger.setOnTouchListener((View v, MotionEvent event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(this);
return true;
}
return false;
});
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onClick() {
Context ctx = getContext();
LinearLayout view = (LinearLayout) getActivity().getLayoutInflater()
.inflate(R.layout.edit_timeline, (ViewGroup) itemView, false);
TextInputFrameLayout inputLayout = view.findViewById(R.id.input);
EditText editText = inputLayout.getEditText();
editText.setText(item.getCustomTitle());
editText.setHint(item.getDefaultTitle(ctx));
ImageButton btn = view.findViewById(R.id.button);
PopupMenu popup = new PopupMenu(ctx, btn);
TimelineDefinition.Icon currentIcon = item.getIcon();
btn.setImageResource(currentIcon.iconRes);
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
btn.setOnTouchListener(popup.getDragToOpenListener());
btn.setOnClickListener(l -> popup.show());
Menu menu = popup.getMenu();
TimelineDefinition.Icon defaultIcon = item.getDefaultIcon();
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
if (!currentIcon.equals(defaultIcon)) {
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
}
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
if (icon.hidden || icon.equals(item.getIcon())) continue;
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
}
UiUtils.enablePopupMenuIcons(ctx, popup);
popup.setOnMenuItemClickListener(menuItem -> {
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
btn.setImageResource(icon.iconRes);
btn.setContentDescription(ctx.getString(icon.nameRes));
item.setIcon(icon);
return true;
});
new M3AlertDialogBuilder(ctx)
.setTitle(R.string.sk_edit_timeline)
.setView(view)
.setPositiveButton(R.string.save, (d, which) -> {
item.setTitle(editText.getText().toString().trim());
rebind();
saveTimelines();
})
.setNeutralButton(R.string.sk_remove, (d, which) ->
removeTimeline(getAbsoluteAdapterPosition()))
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
btn.requestFocus();
}
}
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
public ItemTouchHelperCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
int toPosition = target.getAbsoluteAdapterPosition();
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
return false;
} else {
Collections.swap(data, fromPosition, toPosition);
adapter.notifyItemMoved(fromPosition, toPosition);
saveTimelines();
return true;
}
}
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
viewHolder.itemView.animate().alpha(0.65f);
}
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().alpha(1f);
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAbsoluteAdapterPosition();
removeTimeline(position);
}
}
}

View File

@@ -1,37 +0,0 @@
package org.joinmastodon.android.fragments;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.Nav;
public abstract class FabStatusListFragment extends StatusListFragment {
protected ImageButton fab;
public FabStatusListFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab = view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(this::onFabLongClick);
}
protected void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
}
protected boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID);
}
}

View File

@@ -25,6 +25,7 @@ public class FavoritedStatusListFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Status> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else

View File

@@ -80,6 +80,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else

View File

@@ -55,6 +55,7 @@ public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -10,15 +11,21 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
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.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.utils.UiUtils;
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;
@@ -26,14 +33,14 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class HashtagTimelineFragment extends StatusListFragment{
public class HashtagTimelineFragment extends PinnableStatusListFragment {
private String hashtag;
private boolean following;
private ImageButton fab;
private MenuItem followButton;
public HashtagTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@Override
protected boolean withComposeButton() {
return true;
}
@Override
@@ -41,7 +48,6 @@ public class HashtagTimelineFragment extends StatusListFragment{
super.onAttach(activity);
updateTitle(getArguments().getString("hashtag"));
following=getArguments().getBoolean("following", false);
setHasOptionsMenu(true);
}
@@ -54,35 +60,20 @@ public class HashtagTimelineFragment extends StatusListFragment{
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);
followButton.setOnMenuItemClickListener(i -> {
updateFollowingState(!following);
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag i) {
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;
});
new GetHashtag(hashtag).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag hashtag) {
if (getActivity() == null) return;
updateTitle(hashtag.name);
updateFollowingState(hashtag.following);
}
@@ -94,12 +85,45 @@ public class HashtagTimelineFragment extends StatusListFragment{
}).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;
}
return false;
}
@Override
protected TimelineDefinition makeTimelineDefinition() {
return TimelineDefinition.ofHashtag(hashtag);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
@@ -114,14 +138,12 @@ public class HashtagTimelineFragment extends StatusListFragment{
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '));
protected boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
}
private void onFabClick(View v){
@Override
protected void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", '#'+hashtag+' ');

View File

@@ -16,12 +16,10 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import org.joinmastodon.android.PushNotificationReceiver;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.discover.SearchFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.reduceMotion;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -39,21 +41,26 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -67,14 +74,12 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID;
private MenuItem announcements;
private MenuItem announcements, announcementsAction, settings, settingsAction;
// private ImageView toolbarLogo;
private Button toolbarShowNewPostsBtn;
private boolean newPostsBtnShown;
private AnimatorSet currentNewPostsAnim;
private ViewPager2 pager;
private final List<Fragment> fragments = new ArrayList<>();
private final List<FrameLayout> tabViews = new ArrayList<>();
private View switcher;
private FrameLayout toolbarFrame;
private ImageView timelineIcon;
@@ -83,11 +88,29 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private PopupMenu switcherPopup;
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
private List<TimelineDefinition> timelineDefinitions;
private int count;
private Fragment[] fragments;
private FrameLayout[] tabViews;
private TimelineDefinition[] timelines;
private final Map<Integer, TimelineDefinition> timelinesByMenuItem = new HashMap<>();
private SubMenu hashtagsMenu, listsMenu;
private PopupMenu overflowPopup;
private View overflowActionView = null;
private boolean announcementsBadged, settingsBadged;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
E.register(this);
accountID = getArguments().getString("account");
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
assert timelineDefinitions != null;
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
count = timelineDefinitions.size();
fragments = new Fragment[count];
tabViews = new FrameLayout[count];
timelines = new TimelineDefinition[count];
}
@Override
@@ -102,31 +125,40 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
pager = new ViewPager2(getContext());
toolbarFrame = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.home_toolbar, getToolbar(), false);
if (fragments.size() == 0) {
if (fragments[0] == null) {
Bundle args = new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
args.putBoolean("onlyPosts", true);
fragments.add(new HomeTimelineFragment());
fragments.add(new LocalTimelineFragment());
if (GlobalUserPreferences.showFederatedTimeline) fragments.add(new FederatedTimelineFragment());
for (int i = 0; i < timelineDefinitions.size(); i++) {
TimelineDefinition tl = timelineDefinitions.get(i);
fragments[i] = tl.getFragment();
timelines[i] = tl;
}
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
for (int i = 0; i < fragments.size(); i++) {
fragments.get(i).setArguments(args);
for (int i = 0; i < count; i++) {
fragments[i].setArguments(timelines[i].populateArguments(new Bundle(args)));
FrameLayout tabView = new FrameLayout(getActivity());
tabView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
tabView.setVisibility(View.GONE);
tabView.setId(i + 1);
transaction.add(i + 1, fragments.get(i));
transaction.add(i + 1, fragments[i]);
view.addView(tabView);
tabViews.add(tabView);
tabViews[i] = tabView;
}
transaction.commit();
}
view.addView(pager, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
overflowActionView = UiUtils.makeOverflowActionView(getContext());
overflowPopup = new PopupMenu(getContext(), overflowActionView);
overflowPopup.setOnMenuItemClickListener(this::onOptionsItemSelected);
overflowActionView.setOnClickListener(l -> overflowPopup.show());
overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener());
return view;
}
@@ -140,37 +172,36 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
collapsedChevron = toolbarFrame.findViewById(R.id.collapsed_chevron);
switcher = toolbarFrame.findViewById(R.id.switcher_btn);
switcherPopup = new PopupMenu(getContext(), switcher);
switcherPopup.inflate(R.menu.home_switcher);
switcherPopup.setOnMenuItemClickListener(this::onSwitcherItemSelected);
UiUtils.enablePopupMenuIcons(getContext(), switcherPopup);
switcher.setOnClickListener(v->{
updateSwitcherMenu();
switcherPopup.show();
});
View.OnTouchListener listener = switcherPopup.getDragToOpenListener();
switcher.setOnTouchListener((v, m)-> {
updateSwitcherMenu();
return listener.onTouch(v, m);
});
switcher.setOnClickListener(v->switcherPopup.show());
switcher.setOnTouchListener(switcherPopup.getDragToOpenListener());
updateSwitcherMenu();
UiUtils.reduceSwipeSensitivity(pager);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new HomePagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position){
if (!reduceMotion) {
// setting this here because page transformer appears to fire too late so the
// animation can appear bumpy, especially when navigating to a further-away tab
switcher.setScaleY(0.85f);
switcher.setScaleX(0.85f);
switcher.setAlpha(0.65f);
}
updateSwitcherIcon(position);
if (position==0) return;
hideNewPostsButton();
if (fragments.get(position) instanceof BaseRecyclerFragment<?> page){
if (!timelines[position].equals(TimelineDefinition.HOME_TIMELINE)) hideNewPostsButton();
if (fragments[position] instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading()) page.loadData();
}
}
});
if (!GlobalUserPreferences.reduceMotion) {
if (!reduceMotion) {
pager.setPageTransformer((v, pos) -> {
if (tabViews.get(pager.getCurrentItem()) != v) return;
if (reduceMotion || tabViews[pager.getCurrentItem()] != v) return;
float scaleFactor = Math.max(0.85f, 1 - Math.abs(pos) * 0.06f);
switcher.setScaleY(scaleFactor);
switcher.setScaleX(scaleFactor);
@@ -180,15 +211,37 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
updateToolbarLogo();
ViewTreeObserver vto = getToolbar().getViewTreeObserver();
if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(() -> {
Toolbar t = getToolbar();
if (t == null) return;
int toolbarWidth = t.getWidth();
if (toolbarWidth == 0) return;
int toolbarFrameWidth = toolbarFrame.getWidth();
int padding = toolbarWidth - toolbarFrameWidth;
FrameLayout parent = ((FrameLayout) toolbarShowNewPostsBtn.getParent());
if (padding == parent.getPaddingStart()) return;
// toolbar frame goes from screen edge to beginning of right-aligned option buttons.
// centering button by applying the same space on the left
parent.setPaddingRelative(padding, 0, 0, 0);
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth - padding * 2);
switcher.setPivotX(V.dp(28)); // padding + half of icon
switcher.setPivotY(switcher.getHeight() / 2f);
});
}
if(GithubSelfUpdater.needSelfUpdating()){
E.register(this);
updateUpdateState(GithubSelfUpdater.getInstance().getState());
}
new GetLists().setCallback(new Callback<>() {
@Override
public void onSuccess(List<ListTimeline> lists) {
addItemsToMap(lists, listItems);
updateList(lists, listItems);
}
@Override
@@ -200,7 +253,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
new GetFollowedHashtags().setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Hashtag> hashtags) {
addItemsToMap(hashtags, hashtagsItems);
updateList(hashtags, hashtagsItems);
}
@Override
@@ -208,6 +261,47 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
error.showToast(getContext());
}
}).exec(accountID);
new GetAnnouncements(false).setCallback(new Callback<>() {
@Override
public void onSuccess(List<Announcement> result) {
if (getActivity() == null) return;
if (result.stream().anyMatch(a -> !a.read)) {
announcementsBadged = true;
announcements.setVisible(false);
announcementsAction.setVisible(true);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
}
private void addListsToOverflowMenu() {
Context ctx = getContext();
listsMenu.clear();
listsMenu.getItem().setVisible(listItems.size() > 0);
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(listsMenu));
listItems.forEach((id, list) -> {
MenuItem item = listsMenu.add(Menu.NONE, id, Menu.NONE, list.title);
item.setIcon(R.drawable.ic_fluent_people_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item);
});
}
private void addHashtagsToOverflowMenu() {
Context ctx = getContext();
hashtagsMenu.clear();
hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0);
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(hashtagsMenu));
hashtagsItems.forEach((id, hashtag) -> {
MenuItem item = hashtagsMenu.add(Menu.NONE, id, Menu.NONE, hashtag.name);
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item);
});
}
public void updateToolbarLogo(){
@@ -222,11 +316,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
updateSwitcherIcon(pager.getCurrentItem());
// toolbarLogo=new ImageView(getActivity());
// toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
// toolbarLogo.setImageResource(R.drawable.logo);
// toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
toolbarShowNewPostsBtn=toolbarFrame.findViewById(R.id.show_new_posts_btn);
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N) UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
@@ -247,116 +336,90 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
toolbarShowNewPostsBtn.setScaleY(.8f);
timelineTitle.setVisibility(View.VISIBLE);
}
}
ViewTreeObserver vto = toolbar.getViewTreeObserver();
if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Toolbar t = getToolbar();
if (t == null) return;
int toolbarWidth = t.getWidth();
if (toolbarWidth == 0) return;
t.getViewTreeObserver().removeOnGlobalLayoutListener(this);
private void updateOverflowMenu() {
if (getActivity() == null) return;
Menu m = overflowPopup.getMenu();
m.clear();
overflowPopup.inflate(R.menu.home_overflow);
announcements = m.findItem(R.id.announcements);
settings = m.findItem(R.id.settings);
hashtagsMenu = m.findItem(R.id.hashtags).getSubMenu();
listsMenu = m.findItem(R.id.lists).getSubMenu();
int toolbarFrameWidth = toolbarFrame.getWidth();
int padding = toolbarWidth - toolbarFrameWidth;
// toolbar frame goes from screen edge to beginning of right-aligned option buttons.
// centering button by applying the same space on the left
((FrameLayout) toolbarShowNewPostsBtn.getParent()).setPaddingRelative(padding, 0, 0, 0);
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth - padding * 2);
announcements.setVisible(!announcementsBadged);
announcementsAction.setVisible(announcementsBadged);
settings.setVisible(!settingsBadged);
settingsAction.setVisible(settingsBadged);
switcher.setPivotX(V.dp(28)); // padding + half of icon
switcher.setPivotY(switcher.getHeight() / 2f);
timelineTitle.setPivotX(timelineTitle.getWidth() - V.dp(8));
timelineTitle.setPivotY(timelineTitle.getHeight() / 2f);
}
});
UiUtils.enablePopupMenuIcons(getContext(), overflowPopup);
addListsToOverflowMenu();
addHashtagsToOverflowMenu();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !UiUtils.isEMUI()) {
m.setGroupDividerEnabled(true);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.home, menu);
announcements = menu.findItem(R.id.announcements);
new GetAnnouncements(false).setCallback(new Callback<>() {
@Override
public void onSuccess(List<Announcement> result) {
boolean hasUnread = result.stream().anyMatch(a -> !a.read);
announcements.setIcon(hasUnread ? R.drawable.ic_announcements_24_badged : R.drawable.ic_fluent_megaphone_24_regular);
}
menu.findItem(R.id.overflow).setActionView(overflowActionView);
announcementsAction = menu.findItem(R.id.announcements_action);
settingsAction = menu.findItem(R.id.settings_action);
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
updateOverflowMenu();
}
private <T> void addItemsToMap(List<T> addItems, Map<Integer, T> items) {
if (addItems.size() == 0) return;
private <T> void updateList(List<T> addItems, Map<Integer, T> items) {
if (addItems.size() == 0 || getActivity() == null) return;
for (int i = 0; i < addItems.size(); i++) items.put(View.generateViewId(), addItems.get(i));
updateSwitcherMenu();
updateOverflowMenu();
}
private void updateSwitcherMenu() {
Context context = getContext();
switcherPopup.getMenu().findItem(R.id.federated).setVisible(GlobalUserPreferences.showFederatedTimeline);
Menu switcherMenu = switcherPopup.getMenu();
switcherMenu.clear();
timelinesByMenuItem.clear();
if (!listItems.isEmpty()) {
MenuItem listsItem = switcherPopup.getMenu().findItem(R.id.lists);
listsItem.setVisible(true);
SubMenu listsMenu = listsItem.getSubMenu();
listsMenu.clear();
listItems.forEach((id, list) -> {
MenuItem item = listsMenu.add(Menu.NONE, id, Menu.NONE, list.title);
item.setIcon(R.drawable.ic_fluent_people_list_24_regular);
UiUtils.insetPopupMenuIcon(context, item);
});
for (TimelineDefinition tl : timelines) {
int menuItemId = View.generateViewId();
timelinesByMenuItem.put(menuItemId, tl);
MenuItem item = switcherMenu.add(0, menuItemId, 0, tl.getTitle(getContext()));
item.setIcon(tl.getIcon().iconRes);
}
if (!hashtagsItems.isEmpty()) {
MenuItem hashtagsItem = switcherPopup.getMenu().findItem(R.id.followed_hashtags);
hashtagsItem.setVisible(true);
SubMenu hashtagsMenu = hashtagsItem.getSubMenu();
hashtagsMenu.clear();
hashtagsItems.forEach((id, hashtag) -> {
MenuItem item = hashtagsMenu.add(Menu.NONE, id, Menu.NONE, hashtag.name);
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
UiUtils.insetPopupMenuIcon(context, item);
});
}
UiUtils.enablePopupMenuIcons(getContext(), switcherPopup);
}
private boolean onSwitcherItemSelected(MenuItem item) {
int id = item.getItemId();
ListTimeline list;
Hashtag hashtag;
if (id == R.id.home) {
navigateTo(0);
Bundle args = new Bundle();
args.putString("account", accountID);
if (id == R.id.menu_back) {
switcher.post(() -> switcherPopup.show());
return true;
} else if (id == R.id.local) {
navigateTo(1);
return true;
} else if (id == R.id.federated) {
navigateTo(2);
return true;
} else if ((list = listItems.get(id)) != null) {
Bundle args = new Bundle();
args.putString("account", accountID);
args.putString("listID", list.id);
args.putString("listTitle", list.title);
args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
} else if ((hashtag = hashtagsItems.get(id)) != null) {
UiUtils.openHashtagTimeline(getActivity(), accountID, hashtag.name, hashtag.following);
}
TimelineDefinition tl = timelinesByMenuItem.get(id);
if (tl != null) {
for (int i = 0; i < timelines.length; i++) {
if (timelines[i] == tl) {
navigateTo(i);
return true;
}
}
}
return false;
}
private void navigateTo(int i) {
navigateTo(i, !GlobalUserPreferences.reduceMotion);
navigateTo(i, !reduceMotion);
}
private void navigateTo(int i, boolean smooth) {
@@ -365,32 +428,43 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}
private void updateSwitcherIcon(int i) {
timelineIcon.setImageResource(switch (i) {
default -> R.drawable.ic_fluent_home_24_regular;
case 1 -> R.drawable.ic_fluent_people_community_24_regular;
case 2 -> R.drawable.ic_fluent_earth_24_regular;
});
timelineTitle.setText(switch (i) {
default -> R.string.sk_timeline_home;
case 1 -> R.string.sk_timeline_local;
case 2 -> R.string.sk_timeline_federated;
});
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle();
args.putString("account", accountID);
if (item.getItemId() == R.id.settings) Nav.go(getActivity(), SettingsFragment.class, args);
if (item.getItemId() == R.id.announcements) {
int id = item.getItemId();
ListTimeline list;
Hashtag hashtag;
if (item.getItemId() == R.id.menu_back) {
getToolbar().post(() -> overflowPopup.show());
return true;
} else if (id == R.id.settings || id == R.id.settings_action) {
Nav.go(getActivity(), SettingsFragment.class, args);
} else if (id == R.id.announcements || id == R.id.announcements_action) {
Nav.goForResult(getActivity(), AnnouncementsFragment.class, args, ANNOUNCEMENTS_RESULT, this);
} else if (id == R.id.edit_timelines) {
Nav.go(getActivity(), EditTimelinesFragment.class, args);
} else if ((list = listItems.get(id)) != null) {
args.putString("listID", list.id);
args.putString("listTitle", list.title);
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);
}
return true;
}
@Override
public void scrollToTop(){
((ScrollableToTop) fragments.get(pager.getCurrentItem())).scrollToTop();
((ScrollableToTop) fragments[pager.getCurrentItem()]).scrollToTop();
}
public void hideNewPostsButton(){
@@ -411,7 +485,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f),
ObjectAnimator.ofFloat(collapsedChevron, View.ALPHA, 0f)
);
set.setDuration(GlobalUserPreferences.reduceMotion ? 0 : 300);
set.setDuration(reduceMotion ? 0 : 300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
@@ -426,7 +500,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}
public void showNewPostsButton(){
if(newPostsBtnShown || pager == null || pager.getCurrentItem() != 0)
if(newPostsBtnShown || pager == null || !timelines[pager.getCurrentItem()].equals(TimelineDefinition.HOME_TIMELINE))
return;
newPostsBtnShown=true;
if(currentNewPostsAnim!=null){
@@ -444,7 +518,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f),
ObjectAnimator.ofFloat(collapsedChevron, View.ALPHA, 1f)
);
set.setDuration(GlobalUserPreferences.reduceMotion ? 0 : 300);
set.setDuration(reduceMotion ? 0 : 300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
@@ -469,15 +543,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}
@Override
public void onFragmentResult(int reqCode, boolean noMoreUnread, Bundle result){
if (reqCode == ANNOUNCEMENTS_RESULT && noMoreUnread) {
announcements.setIcon(R.drawable.ic_fluent_megaphone_24_regular);
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if (reqCode == ANNOUNCEMENTS_RESULT && success) {
announcementsBadged = false;
announcements.setVisible(true);
announcementsAction.setVisible(false);
}
}
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING) {
settingsBadged = true;
settingsAction.setVisible(true);
settings.setVisible(false);
}
}
@Subscribe
@@ -497,11 +576,26 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override
public void onDestroyView(){
super.onDestroyView();
if (overflowPopup != null) {
overflowPopup.dismiss();
overflowPopup = null;
}
if (switcherPopup != null) {
switcherPopup.dismiss();
switcherPopup = null;
}
if(GithubSelfUpdater.needSelfUpdating()){
E.unregister(this);
}
}
@Override
protected void onShown() {
super.onShown();
Object pinnedTimelines = GlobalUserPreferences.pinnedTimelines.get(accountID);
if (pinnedTimelines != null && timelineDefinitions != pinnedTimelines) UiUtils.restartApp();
}
@Override
public void onViewStateRestored(Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
@@ -515,12 +609,61 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
outState.putInt("selectedTab", pager.getCurrentItem());
}
@Subscribe
public void onHashtagUpdatedEvent(HashtagUpdatedEvent event) {
handleListEvent(hashtagsItems, h -> h.name.equalsIgnoreCase(event.name), event.following, () -> {
Hashtag hashtag = new Hashtag();
hashtag.name = event.name;
hashtag.following = true;
return hashtag;
});
}
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
handleListEvent(listItems, l -> l.id.equals(event.id), false, null);
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
ListTimeline list = new ListTimeline();
list.id = event.id;
list.title = event.title;
list.repliesPolicy = event.repliesPolicy;
return list;
});
}
private <T> void handleListEvent(
Map<Integer, T> existingThings,
Predicate<T> matchExisting,
boolean shouldBeInList,
Supplier<T> makeNewThing
) {
Optional<Map.Entry<Integer, T>> existingThing = existingThings.entrySet().stream()
.filter(e -> matchExisting.test(e.getValue())).findFirst();
if (shouldBeInList) {
existingThings.put(existingThing.isPresent()
? existingThing.get().getKey() : View.generateViewId(), makeNewThing.get());
updateOverflowMenu();
} else if (existingThing.isPresent() && !shouldBeInList) {
existingThings.remove(existingThing.get().getKey());
updateOverflowMenu();
}
}
public Collection<Hashtag> getHashtags() {
return hashtagsItems.values();
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
FrameLayout tabView = tabViews.get(viewType % getItemCount());
((ViewGroup)tabView.getParent()).removeView(tabView);
FrameLayout tabView = tabViews[viewType % getItemCount()];
ViewGroup tabParent = (ViewGroup) tabView.getParent();
if (tabParent != null) tabParent.removeView(tabView);
tabView.setVisibility(View.VISIBLE);
return new SimpleViewHolder(tabView);
}
@@ -530,7 +673,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override
public int getItemCount(){
return fragments.size();
return count;
}
@Override

View File

@@ -8,6 +8,9 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
@@ -29,9 +32,15 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class HomeTimelineFragment extends FabStatusListFragment {
public class HomeTimelineFragment extends StatusListFragment {
private HomeTabFragment parent;
private String maxID;
private String lastSavedMarkerID;
@Override
protected boolean withComposeButton() {
return true;
}
@Override
public void onAttach(Activity activity){
@@ -54,8 +63,7 @@ public class HomeTimelineFragment extends FabStatusListFragment {
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
@Override
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
if(getActivity()==null)
return;
if (getActivity() == null) return;
List<Status> filteredItems = filterPosts(result.items);
onDataLoaded(filteredItems, !result.items.isEmpty());
maxID=result.maxID;
@@ -91,6 +99,29 @@ public class HomeTimelineFragment extends FabStatusListFragment {
}
}
@Override
protected void onHidden(){
super.onHidden();
if(!data.isEmpty()){
String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID;
if(!topPostID.equals(lastSavedMarkerID)){
lastSavedMarkerID=topPostID;
new SaveMarkers(topPostID, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SaveMarkers.Response result){
}
@Override
public void onError(ErrorResponse error){
lastSavedMarkerID=null;
}
})
.exec(accountID);
}
}
}
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
}
@@ -123,7 +154,7 @@ public class HomeTimelineFragment extends FabStatusListFragment {
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
if (parent != null) parent.showNewPostsButton();
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
}
}

View File

@@ -1,11 +1,13 @@
package org.joinmastodon.android.fragments;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
public interface IsOnTop {
boolean isOnTop();
default boolean isRecyclerViewOnTop(RecyclerView list) {
default boolean isRecyclerViewOnTop(@Nullable RecyclerView list) {
if (list == null) return true;
return !list.canScrollVertically(-1);
}
}

View File

@@ -9,17 +9,26 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.Nullable;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.CreateList;
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.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.Status;
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.ListTimelineEditor;
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;
@@ -28,14 +37,15 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ListTimelineFragment extends StatusListFragment {
public class ListTimelineFragment extends PinnableStatusListFragment {
private String listID;
private String listTitle;
@Nullable
private ListTimeline.RepliesPolicy repliesPolicy;
private ImageButton fab;
public ListTimelineFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
@Override
protected boolean withComposeButton() {
return true;
}
@Override
@@ -48,39 +58,58 @@ public class ListTimelineFragment extends StatusListFragment {
setTitle(listTitle);
setHasOptionsMenu(true);
new GetList(listID).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline listTimeline) {
if (getActivity() == null) return;
// TODO: save updated info
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
repliesPolicy = listTimeline.repliesPolicy;
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.list, menu);
super.onCreateOptionsMenu(menu, inflater);
UiUtils.enableOptionsMenuIcons(getContext(), menu, R.id.pin);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Bundle args = new Bundle();
args.putString("listID", listID);
if (super.onOptionsItemSelected(item)) return true;
if (item.getItemId() == R.id.edit) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
editor.applyList(listTitle, repliesPolicy);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_edit_list_title)
.setIcon(R.drawable.ic_fluent_people_list_28_regular)
.setIcon(R.drawable.ic_fluent_people_28_regular)
.setView(editor)
.setPositiveButton(R.string.save, (d, which) -> {
new UpdateList(listID, editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
String newTitle = editor.getTitle().trim();
setTitle(newTitle);
new UpdateList(listID, newTitle, editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
if (getActivity() == null) return;
setTitle(list.title);
listTitle = list.title;
repliesPolicy = list.repliesPolicy;
args.putString("listTitle", listTitle);
args.putInt("repliesPolicy", repliesPolicy.ordinal());
setResult(true, args);
E.post(new ListUpdatedCreatedEvent(listID, listTitle, repliesPolicy));
}
@Override
public void onError(ErrorResponse error) {
setTitle(listTitle);
error.showToast(getContext());
}
}).exec(accountID);
@@ -89,24 +118,30 @@ public class ListTimelineFragment extends StatusListFragment {
.show();
} else if (item.getItemId() == R.id.delete) {
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
args.putBoolean("deleted", true);
setResult(true, args);
E.post(new ListDeletedEvent(listID));
Nav.finish(this);
});
}
return true;
}
@Override
protected TimelineDefinition makeTimelineDefinition() {
return TimelineDefinition.ofList(listID, listTitle);
}
@Override
protected void doLoadData(int offset, int count) {
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<Status> result) {
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
.exec(accountID);
}
@Override
@@ -117,14 +152,7 @@ public class ListTimelineFragment extends StatusListFragment {
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID));
}
private void onFabClick(View v){
protected void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);

View File

@@ -12,12 +12,17 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -37,210 +42,218 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
private static final int LIST_CHANGED_RESULT = 987;
private String accountId;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
private final HashMap<String, Boolean> userInList = new HashMap<>();
private ListsAdapter adapter;
private String accountId;
private String profileAccountId;
private String profileDisplayUsername;
private HashMap<String, Boolean> userInListBefore = new HashMap<>();
private HashMap<String, Boolean> userInList = new HashMap<>();
private int inProgress = 0;
private ListsAdapter adapter;
public ListTimelinesFragment() {
super(10);
}
public ListTimelinesFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
setHasOptionsMenu(true);
E.register(this);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
setHasOptionsMenu(true);
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
String profileDisplayUsername = args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else {
setTitle(R.string.sk_your_lists);
}
}
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
profileDisplayUsername=args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else {
setTitle(R.string.sk_your_lists);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_list, menu);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) -> {
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
saveListMembership(list.id, true);
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId)
)
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
return true;
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId);
})
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
return true;
}
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {}
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(Object o) {}
}).exec(accountId);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
if (getActivity() == null) return;
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
if (getActivity() == null) return;
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
@Override
public void onFragmentResult(int reqCode, boolean listChanged, Bundle result){
if (reqCode == LIST_CHANGED_RESULT && listChanged) {
String listID = result.getString("listID");
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(listID)) {
if (result.getBoolean("deleted")) {
data.remove(i);
adapter.notifyItemRemoved(i);
} else {
item.title = result.getString("listTitle", item.title);
item.repliesPolicy = ListTimeline.RepliesPolicy.values()[result.getInt("repliesPolicy")];
adapter.notifyItemChanged(i);
}
break;
}
}
}
}
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
data.remove(i);
adapter.notifyItemRemoved(i);
break;
}
}
}
@Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
return adapter = new ListsAdapter();
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
adapter.notifyItemChanged(i);
break;
}
}
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
return adapter = new ListsAdapter();
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public int getItemCount() {
return data.size();
}
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
@Override
public int getItemCount() {
return data.size();
}
}
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_list_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
@Override
public void onClick() {
Bundle args=new Bundle();
args.putString("account", accountId);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.goForResult(getActivity(), ListTimelineFragment.class, args, LIST_CHANGED_RESULT, ListTimelinesFragment.this);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
Bundle args=new Bundle();
args.putString("account", accountId);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
}
}

View File

@@ -44,7 +44,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private String accountID;
@@ -104,13 +104,12 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
pager=view.findViewById(R.id.pager);
UiUtils.reduceSwipeSensitivity(pager);
tabViews=new FrameLayout[3];
tabViews=new FrameLayout[2];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.notifications_all;
case 1 -> R.id.notifications_mentions;
case 2 -> R.id.notifications_posts;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -120,6 +119,18 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
scrollToTop();
}
});
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
@@ -150,15 +161,9 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
mentionsFragment=new NotificationsListFragment();
mentionsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyPosts", true);
postsFragment=new NotificationsListFragment();
postsFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.notifications_all, allNotificationsFragment)
.add(R.id.notifications_mentions, mentionsFragment)
.add(R.id.notifications_posts, postsFragment)
.commit();
}
@@ -168,7 +173,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tab.setText(switch(position){
case 0 -> R.string.all_notifications;
case 1 -> R.string.mentions;
case 2 -> R.string.posts;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
@@ -183,6 +187,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Account> accounts) {
if (getActivity() == null) return;
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
}
@@ -211,13 +216,13 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
protected void updateToolbar(){
super.updateToolbar();
getToolbar().setOutlineProvider(null);
getToolbar().setOnClickListener(v->scrollToTop());
}
private NotificationsListFragment getFragmentForPage(int page){
return switch(page){
case 0 -> allNotificationsFragment;
case 1 -> mentionsFragment;
case 2 -> postsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
};
}
@@ -238,7 +243,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
@Override
public int getItemCount(){
return 3;
return 2;
}
@Override

View File

@@ -2,8 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.text.TextUtils;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -14,14 +13,18 @@ import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
@@ -35,12 +38,17 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private boolean onlyMentions;
private boolean onlyPosts;
private String maxID;
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
@Override
protected boolean withComposeButton() {
return true;
}
@Override
public void onCreate(Bundle savedInstanceState){
@@ -71,6 +79,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null :
n.report.targetAccount;
String extraText=switch(n.type){
case FOLLOW -> getString(R.string.user_followed_you);
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
@@ -78,23 +88,24 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
case REBLOG -> getString(R.string.notification_boosted);
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
case UPDATE -> getString(R.string.sk_post_edited);
case SIGN_UP -> getString(R.string.sk_signed_up);
case REPORT -> getString(R.string.sk_reported);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText, n, null) : null;
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null;
if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
if(titleItem!=null){
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem imgItem){
imgItem.horizontalInset=V.dp(32);
}
}
}
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account, n);
return Arrays.asList(titleItem, card);
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this,
reportTarget != null ? reportTarget : n.account, n);
TextStatusDisplayItem text = n.report != null && !TextUtils.isEmpty(n.report.comment) ?
new TextStatusDisplayItem(n.id, n.report.comment, this,
Status.ofFake(n.id, n.report.comment, n.createdAt), true) :
null;
return text == null ? Arrays.asList(titleItem, card) : Arrays.asList(titleItem, text, card);
}else{
return Collections.emptyList();
}
@@ -106,6 +117,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
if(s.status!=null && s.status.reblog!=null && !knownAccounts.containsKey(s.status.reblog.account.id))
knownAccounts.put(s.status.reblog.account.id, s.status.reblog.account);
}
@Override
@@ -115,8 +128,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
if(getActivity()==null)
return;
if (getActivity() == null) return;
if(refreshing)
relationships.clear();
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
@@ -163,6 +175,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else if(n.report != null){
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -175,6 +190,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
if (getParentFragment() instanceof NotificationsFragment) fab.setVisibility(View.GONE);
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
}
private Notification getNotificationByID(String id){

View File

@@ -0,0 +1,69 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.TimelineDefinition;
import java.util.ArrayList;
import java.util.List;
public abstract class PinnableStatusListFragment extends StatusListFragment {
protected List<TimelineDefinition> pinnedTimelines;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
updatePinButton(menu.findItem(R.id.pin));
}
protected boolean isPinned() {
return pinnedTimelines.contains(makeTimelineDefinition());
}
protected void updatePinButton(MenuItem pin) {
boolean pinned = isPinned();
pin.setIcon(pinned ?
R.drawable.ic_fluent_pin_24_filled :
R.drawable.ic_fluent_pin_24_regular);
pin.setTitle(pinned ? R.string.sk_unpin_timeline : R.string.sk_pin_timeline);
}
protected abstract TimelineDefinition makeTimelineDefinition();
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.pin) {
togglePin(item);
return true;
}
return super.onOptionsItemSelected(item);
}
protected void togglePin(MenuItem pin) {
onPinnedUpdated(true);
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
TimelineDefinition def = makeTimelineDefinition();
boolean pinned = isPinned();
if (pinned) pinnedTimelines.remove(def);
else pinnedTimelines.add(def);
Toast.makeText(getContext(), pinned ? R.string.sk_unpinned_timeline : R.string.sk_pinned_timeline, Toast.LENGTH_SHORT).show();
GlobalUserPreferences.pinnedTimelines.put(accountID, pinnedTimelines);
GlobalUserPreferences.save();
updatePinButton(pin);
}
public void onPinnedUpdated(boolean pinned) {}
}

View File

@@ -1,44 +1,44 @@
package org.joinmastodon.android.fragments;
import static android.content.Context.CLIPBOARD_SERVICE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -61,6 +61,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
@@ -69,8 +70,10 @@ import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.parceler.Parcels;
@@ -84,6 +87,9 @@ import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
@@ -94,10 +100,17 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
private static final int AVATAR_RESULT=722;
@@ -105,23 +118,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private View avatarBorder, nameWrap;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private ProfileAboutFragment aboutFragment;
// private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
private CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable();
private float titleTransY;
private View postsBtn, followersBtn, followingBtn;
private View postsBtn, followersBtn, followingBtn, profileCounters;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress, notifyProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
private ViewGroup rolesView;
private Account account;
private String accountID;
@@ -134,10 +148,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private Uri editNewAvatar, editNewCover;
private String profileAccountID;
private boolean refreshing;
private View fab;
private ImageButton fab;
private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
protected int scrollDiff = 0;
private static final int MAX_FIELDS=4;
// from ProfileAboutFragment
public UsableRecyclerView list;
private List<AccountField> metadataListData=Collections.emptyList();
private MetadataAdapter adapter;
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private RecyclerView.ViewHolder draggedViewHolder;
private ListImageLoaderWrapper imgLoader;
public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar);
@@ -183,8 +208,10 @@ 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);
nameWrap=content.findViewById(R.id.name_wrap);
username=content.findViewById(R.id.username);
bio=content.findViewById(R.id.bio);
profileCounters=content.findViewById(R.id.profile_counters);
followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label);
followersBtn=content.findViewById(R.id.followers_btn);
@@ -206,6 +233,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
notifyProgress=content.findViewById(R.id.notify_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
list=content.findViewById(R.id.metadata);
rolesView=content.findViewById(R.id.roles);
avatar.setOutlineProvider(new ViewOutlineProvider(){
@Override
@@ -225,7 +254,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
};
tabViews=new FrameLayout[5];
tabViews=new FrameLayout[4];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
@@ -242,7 +271,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
UiUtils.reduceSwipeSensitivity(pager);
pager.setOffscreenPageLimit(5);
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -276,6 +305,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
});
actionButton.setOnClickListener(this::onActionButtonClick);
actionButton.setOnLongClickListener(this::onActionButtonLongClick);
notifyButton.setOnClickListener(this::onNotifyButtonClick);
avatar.setOnClickListener(this::onAvatarClick);
cover.setOnClickListener(this::onCoverClick);
@@ -303,6 +333,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return true;
});
// from ProfileAboutFragment
list.setItemAnimator(new BetterItemAnimator());
list.setDrawSelectorOnTop(true);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
list.setAdapter(adapter=new MetadataAdapter());
list.setClipToPadding(false);
return sizeWrapper;
}
@@ -312,6 +350,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(Account result){
if (getActivity() == null) return;
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
@@ -357,8 +396,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
// aboutFragment=new ProfileAboutFragment();
setFields(fields);
}
pager.getAdapter().notifyDataSetChanged();
super.dataLoaded();
@@ -451,6 +490,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
name.setText(ssb);
setTitle(ssb);
if (account.roles != null && !account.roles.isEmpty()) {
rolesView.setVisibility(View.VISIBLE);
rolesView.removeAllViews();
name.setPadding(0, 0, V.dp(12), 0);
for (Account.Role role : account.roles) {
TextView roleText = new TextView(getActivity(), null, 0, R.style.role_label);
roleText.setText(role.name);
if (!TextUtils.isEmpty(role.color) && role.color.startsWith("#")) try {
GradientDrawable bg = (GradientDrawable) roleText.getBackground().mutate();
bg.setStroke(V.dp(2), Color.parseColor(role.color));
} catch (Exception ignored) {}
rolesView.addView(roleText);
}
}
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){
@@ -518,9 +572,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
fields.add(field);
}
if(aboutFragment!=null){
aboutFragment.setFields(fields);
}
setFields(fields);
}
private void updateToolbar(){
@@ -557,6 +609,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags);
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
openWithAccounts.setVisible(hasMultipleAccounts);
SubMenu accountsMenu = openWithAccounts.getSubMenu();
if (hasMultipleAccounts) {
accountsMenu.clear();
UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL(
getActivity(), s.getID(), account.url, false
));
}
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
if(isOwnProfile)
return;
@@ -679,6 +741,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void updateRelationship(){
if (getActivity() == null) return;
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
@@ -688,7 +751,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying);
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
}
public ImageButton getFab() {
return fab;
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
@@ -711,8 +778,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
coverGradient.setTopOffset(scrollY);
cover.invalidate();
titleTransY=getToolbar().getHeight();
if(scrollY>name.getTop()-topBarsH){
titleTransY=Math.max(0f, titleTransY-(scrollY-(name.getTop()-topBarsH)));
if(scrollY>nameWrap.getTop()-topBarsH){
titleTransY=Math.max(0f, titleTransY-(scrollY-(nameWrap.getTop()-topBarsH)));
}
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
@@ -721,6 +788,37 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
}
if (GlobalUserPreferences.autoHideFab) {
int dy = scrollY - oldScrollY;
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
TranslateAnimation animate = new TranslateAnimation(
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
animate.setFillAfter(true);
fab.startAnimation(animate);
fab.setVisibility(View.INVISIBLE);
scrollDiff = 0;
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
if (v.getScrollY() == 0 || scrollDiff > 400) {
fab.setVisibility(View.VISIBLE);
TranslateAnimation animate = new TranslateAnimation(
0,
0,
fab.getHeight() * 2,
0);
animate.setDuration(300);
animate.setFillAfter(true);
fab.startAnimation(animate);
scrollDiff = 0;
} else {
scrollDiff += Math.abs(dy);
}
}
}
}
private Fragment getFragmentForPage(int page){
@@ -729,7 +827,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
case 1 -> postsWithRepliesFragment;
case 2 -> pinnedPostsFragment;
case 3 -> mediaFragment;
case 4 -> aboutFragment;
// case 4 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@@ -749,6 +847,31 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private boolean onActionButtonLongClick(View v) {
if (isOwnProfile || AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickAccount(getActivity(), accountID, R.string.sk_follow_as, R.drawable.ic_fluent_person_add_28_regular, session -> {
UiUtils.lookupAccount(getActivity(), account, session.getID(), accountID, acc -> {
if (acc == null) return;
new SetAccountFollowed(acc.id, true, true).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
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;
}
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
@@ -771,8 +894,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onSuccess(Account result){
editModeLoading=false;
if(getActivity()==null)
return;
if (getActivity() == null) return;
enterEditMode(result);
setActionProgressVisible(false);
}
@@ -780,8 +902,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onError(ErrorResponse error){
editModeLoading=false;
if(getActivity()==null)
return;
if (getActivity() == null) return;
error.showToast(getActivity());
setActionProgressVisible(false);
}
@@ -796,16 +917,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.done);
pager.setCurrentItem(4);
ArrayList<Animator> animators=new ArrayList<>();
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false);
}
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay, getActivity().getTheme()).mutate();
avatar.setForeground(overlay);
animators.add(ObjectAnimator.ofInt(overlay, "alpha", 0, 255));
nameWrap.setVisibility(View.GONE);
nameEdit.setVisibility(View.VISIBLE);
nameEdit.setText(account.displayName);
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
@@ -817,10 +934,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEdit.setText(account.source.note);
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f, 1f));
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 0f));
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, .3f));
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, .3f));
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, .3f));
profileCounters.setVisibility(View.GONE);
pager.setVisibility(View.GONE);
tabbar.setVisibility(View.GONE);
AnimatorSet set=new AnimatorSet();
set.playTogether(animators);
@@ -828,7 +944,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.start();
aboutFragment.enterEditMode(account.source.fields);
// aboutFragment.enterEditMode(account.source.fields);
V.setVisibilityAnimated(fab, View.GONE);
metadataListData=account.source.fields;
adapter.notifyDataSetChanged();
dragHelper.attachToRecyclerView(list);
}
private void exitEditMode(){
@@ -839,16 +960,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
}
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f));
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f));
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 1f));
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, 1f));
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, 1f));
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, 1f));
profileCounters.setVisibility(View.VISIBLE);
pager.setVisibility(View.VISIBLE);
tabbar.setVisibility(View.VISIBLE);
V.setVisibilityAnimated(nameWrap, View.VISIBLE);
AnimatorSet set=new AnimatorSet();
set.playTogether(animators);
@@ -857,20 +976,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
for(int i=0;i<tabViews.length-1;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
nameEdit.setVisibility(View.GONE);
bioEdit.setVisibility(View.GONE);
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
lp.addRule(RelativeLayout.BELOW, R.id.name);
lp.addRule(RelativeLayout.BELOW, R.id.name_wrap);
username.getParent().requestLayout();
avatar.setForeground(null);
scrollToTop();
}
});
set.start();
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(content.getWindowToken(), 0);
V.setVisibilityAnimated(fab, View.VISIBLE);
bindHeaderView();
}
@@ -878,12 +998,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(!isInEditMode)
throw new IllegalStateException();
setActionProgressVisible(true);
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, aboutFragment.getFields())
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, metadataListData)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
account=result;
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if (getActivity() == null) return;
exitEditMode();
setActionProgressVisible(false);
}
@@ -1048,4 +1169,227 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return position;
}
}
// from ProfileAboutFragment
public void setFields(ArrayList<AccountField> fields){
metadataListData=fields;
if (isInEditMode) {
isInEditMode=false;
dragHelper.attachToRecyclerView(null);
}
if (adapter != null) adapter.notifyDataSetChanged();
}
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
public MetadataAdapter(){
super(imgLoader);
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return switch(viewType){
case 0 -> new AboutViewHolder();
case 1 -> new EditableAboutViewHolder();
case 2 -> new AddRowViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@Override
public void onBindViewHolder(BaseViewHolder holder, int position){
if(position<metadataListData.size()){
holder.bind(metadataListData.get(position));
}else{
holder.bind(null);
}
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
if(isInEditMode){
int size=metadataListData.size();
if(size<MAX_FIELDS)
size++;
return size;
}
return metadataListData.size();
}
@Override
public int getItemViewType(int position){
if(isInEditMode){
return position==metadataListData.size() ? 2 : 1;
}
return 0;
}
@Override
public int getImageCountForItem(int position){
return isInEditMode || metadataListData.get(position).emojiRequests==null
? 0 : metadataListData.get(position).emojiRequests.size();
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return metadataListData.get(position).emojiRequests.get(image);
}
}
private abstract class BaseViewHolder extends BindableViewHolder<AccountField> {
public BaseViewHolder(int layout){
super(getActivity(), layout, list);
}
}
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder {
private TextView title;
private LinkedTextView value;
public AboutViewHolder(){
super(R.layout.item_profile_about);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
}
@Override
public void onBind(AccountField item){
title.setText(item.parsedName);
value.setText(item.parsedValue);
if(item.verifiedAt!=null){
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
check.setTint(textColor);
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
}else{
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
value.setCompoundDrawables(null, null, null, null);
}
}
@Override
public void setImage(int index, Drawable image){
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
span.setDrawable(image);
title.invalidate();
value.invalidate();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
}
private class EditableAboutViewHolder extends BaseViewHolder {
private EditText title;
private EditText value;
public EditableAboutViewHolder(){
super(R.layout.item_profile_about_editable);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
dragHelper.startDrag(this);
return true;
});
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick);
}
@Override
public void onBind(AccountField item){
title.setText(item.name);
value.setText(item.value);
}
private void onRemoveRowClick(View v){
int pos=getAbsoluteAdapterPosition();
metadataListData.remove(pos);
adapter.notifyItemRemoved(pos);
for(int i=0;i<list.getChildCount();i++){
BaseViewHolder vh=(BaseViewHolder) list.getChildViewHolder(list.getChildAt(i));
vh.rebind();
}
}
}
private class AddRowViewHolder extends BaseViewHolder implements UsableRecyclerView.Clickable{
public AddRowViewHolder(){
super(R.layout.item_profile_about_add_row);
}
@Override
public void onClick(){
metadataListData.add(new AccountField());
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
adapter.notifyItemChanged(metadataListData.size()-1);
}else{
adapter.notifyItemInserted(metadataListData.size()-1);
rebind();
}
}
@Override
public void onBind(AccountField item) {}
}
private class ReorderCallback extends ItemTouchHelper.SimpleCallback{
public ReorderCallback(){
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
if(target instanceof AddRowViewHolder)
return false;
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
int toPosition=target.getAbsoluteAdapterPosition();
if (fromPosition<toPosition) {
for (int i=fromPosition;i<toPosition;i++) {
Collections.swap(metadataListData, i, i+1);
}
} else {
for (int i=fromPosition;i>toPosition;i--) {
Collections.swap(metadataListData, i, i-1);
}
}
adapter.notifyItemMoved(fromPosition, toPosition);
((BindableViewHolder)viewHolder).rebind();
((BindableViewHolder)target).rebind();
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
}
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.onSelectedChanged(viewHolder, actionState);
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=viewHolder;
}
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=null;
}
@Override
public boolean isLongPressDragEnabled(){
return false;
}
}
}

View File

@@ -28,11 +28,11 @@ import me.grishka.appkit.api.SimpleCallback;
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
private String nextMaxID;
private ImageButton fab;
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
public ScheduledStatusListFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
@Override
protected boolean withComposeButton() {
return true;
}
@Override
@@ -56,20 +56,30 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
protected void onFabClick(View v) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
fab.setOnClickListener(v -> Nav.go(getActivity(), ComposeFragment.class, args));
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args));
Nav.go(getActivity(), ComposeFragment.class, args);
}
@Override
protected boolean onFabLongClick(View v) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
return UiUtils.pickAccountForCompose(getActivity(), accountID, args);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null);
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true);
}
@Override
@@ -109,6 +119,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
if (getActivity() == null) return;
onDataLoaded(result, nextMaxID!=null);
}
})

View File

@@ -9,6 +9,8 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.LruCache;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -41,11 +43,13 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -63,7 +67,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -77,6 +80,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsButtonItem, glitchModeItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
@@ -165,11 +169,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
}));
items.add(new HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
GlobalUserPreferences.showFederatedTimeline=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
GlobalUserPreferences.playGifs=i.checked;
GlobalUserPreferences.save();
@@ -196,6 +195,64 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_settings_disable_alt_text_reminder, R.drawable.ic_fluent_image_alt_text_24_regular, GlobalUserPreferences.disableAltTextReminder, i->{
GlobalUserPreferences.disableAltTextReminder=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_single_notification, R.drawable.ic_fluent_convert_range_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.sk_timelines));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
GlobalUserPreferences.showBoosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{
GlobalUserPreferences.loadNewPosts=i.checked;
showNewPostsButtonItem.enabled = i.checked;
if (!i.checked) {
GlobalUserPreferences.showNewPostsButton = false;
showNewPostsButtonItem.checked = false;
}
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsButtonItem)) instanceof SwitchViewHolder svh) svh.rebind();
GlobalUserPreferences.save();
}));
items.add(showNewPostsButtonItem = new SwitchItem(R.string.sk_settings_see_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{
GlobalUserPreferences.showNewPostsButton=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_alt_indicator, R.drawable.ic_fluent_scan_text_24_regular, GlobalUserPreferences.showAltIndicator, i->{
GlobalUserPreferences.showAltIndicator=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_no_alt_indicator, R.drawable.ic_fluent_important_24_regular, GlobalUserPreferences.showNoAltIndicator, i->{
GlobalUserPreferences.showNoAltIndicator=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_collapse_long_posts, R.drawable.ic_fluent_chevron_down_24_regular, GlobalUserPreferences.collapseLongPosts, i->{
GlobalUserPreferences.collapseLongPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
GlobalUserPreferences.spectatorMode=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_settings_hide_fab, R.drawable.ic_fluent_edit_24_regular, GlobalUserPreferences.autoHideFab, i->{
GlobalUserPreferences.autoHideFab=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
GlobalUserPreferences.save();
@@ -206,32 +263,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
R.string.sk_settings_translation_availability_note_available :
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
items.add(new HeaderItem(R.string.home_timeline));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
GlobalUserPreferences.showBoosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
GlobalUserPreferences.loadNewPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
PushSubscription pushSubscription=getPushSubscription();
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked)));
items.add(new SwitchItem(R.string.sk_settings_single_notification, R.drawable.ic_fluent_convert_range_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
GlobalUserPreferences.save();
}));
boolean switchEnabled=pushSubscription.policy!=PushSubscription.Policy.NONE;
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked), switchEnabled));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked), switchEnabled));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked), switchEnabled));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked), switchEnabled));
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_chat_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked), switchEnabled));
items.add(new SwitchItem(R.string.sk_notify_update, R.drawable.ic_fluent_history_24_regular, pushSubscription.alerts.update, i->onNotificationsChanged(PushNotification.Type.UPDATE, i.checked), switchEnabled));
items.add(new SwitchItem(R.string.sk_notify_poll_results, R.drawable.ic_fluent_poll_24_regular, pushSubscription.alerts.poll, i->onNotificationsChanged(PushNotification.Type.POLL, i.checked), switchEnabled));
items.add(new HeaderItem(R.string.settings_account));
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
@@ -249,20 +292,65 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version)));
items.add(new HeaderItem(R.string.sk_instance_features));
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
glitchModeItem.enabled = i.checked;
if (i.checked) {
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID);
} else {
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
}
glitchModeItem.checked = GlobalUserPreferences.accountsInGlitchMode.contains(accountID);
if (list.findViewHolderForAdapterPosition(items.indexOf(glitchModeItem)) instanceof SwitchViewHolder svh) svh.rebind();
GlobalUserPreferences.save();
}));
items.add(new SmallTextItem(getString(R.string.sk_settings_local_only_explanation)));
items.add(glitchModeItem = new SwitchItem(R.string.sk_settings_glitch_instance, 0, GlobalUserPreferences.accountsInGlitchMode.contains(accountID), i->{
if (i.checked) {
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
} else {
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
}
GlobalUserPreferences.save();
}));
glitchModeItem.enabled = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
items.add(new SmallTextItem(getString(R.string.sk_settings_glitch_mode_explanation)));
items.add(new HeaderItem(R.string.sk_settings_about));
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
if (GithubSelfUpdater.needSelfUpdating()) {
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), imageCache.getDiskCache().size(), true), this::clearImageCache, 0);
LruCache<?, ?> cache = imageCache == null ? null : imageCache.getLruCache();
clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), cache != null ? cache.size() : 0, true), this::clearImageCache, 0);
items.add(clearImageCacheItem);
items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{
GlobalUserPreferences.recentLanguages.remove(accountID);
GlobalUserPreferences.save();
})));
if (GithubSelfUpdater.needSelfUpdating()) {
items.add(new SwitchItem(R.string.sk_updater_enable_pre_releases, 0, GlobalUserPreferences.enablePreReleases, i->{
GlobalUserPreferences.enablePreReleases=i.checked;
GlobalUserPreferences.save();
}));
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
if(BuildConfig.DEBUG){
items.add(new RedHeaderItem("Debug options"));
items.add(new TextItem("Test e-mail confirmation flow", ()->{
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false;
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("debug", true);
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
}));
}
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
@@ -317,11 +405,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
}
if(needAppRestart){
Intent intent = Intent.makeRestartActivityTask(MastodonApp.context.getPackageManager().getLaunchIntentForPackage(MastodonApp.context.getPackageName()).getComponent());
MastodonApp.context.startActivity(intent);
Runtime.getRuntime().exit(0);
}
if(needAppRestart) UiUtils.restartApp();
}
@Override
@@ -423,8 +507,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
case FAVORITE -> subscription.alerts.favourite=enabled;
case FOLLOW -> subscription.alerts.follow=enabled;
case REBLOG -> subscription.alerts.reblog=enabled;
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
case MENTION -> subscription.alerts.mention=enabled;
case POLL -> subscription.alerts.poll=enabled;
case STATUS -> subscription.alerts.status=enabled;
case UPDATE -> subscription.alerts.update=enabled;
}
needUpdateNotificationSettings=true;
}
@@ -443,9 +529,13 @@ public class SettingsFragment extends MastodonToolbarFragment{
list.getAdapter().notifyItemChanged(index);
}
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
boolean newState=policy!=PushSubscription.Policy.NONE;
for(PushNotification.Type value : PushNotification.Type.values()){
onNotificationsChanged(value, newState);
}
index++;
while(items.get(index) instanceof SwitchItem si){
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
si.enabled=si.checked=newState;
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
if(holder!=null)
((BindableViewHolder<?>)holder).rebind();
@@ -485,6 +575,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
private void onLoggedOut(){
if (getActivity() == null) return;
AccountSessionManager.getInstance().removeAccount(accountID);
getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
@@ -538,7 +629,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.text=getString(text);
}
public HeaderItem(String text) {
public HeaderItem(String text){
this.text=text;
}
@@ -562,7 +653,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.onChanged=onChanged;
}
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
this.text=getString(text);
this.icon=icon;
this.checked=checked;
@@ -649,6 +740,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.secondaryText = secondaryText;
}
public TextItem(String text, Runnable onClick){
this.text=text;
this.onClick=onClick;
}
@Override
public int getViewType(){
return 4;
@@ -661,6 +757,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
super(text);
}
public RedHeaderItem(String text){
super(text);
}
@Override
public int getViewType(){
return 5;
@@ -754,7 +854,12 @@ public class SettingsFragment extends MastodonToolbarFragment{
@Override
public void onBind(SwitchItem item){
text.setText(item.text);
icon.setImageResource(item.icon);
if (item.icon == 0) {
icon.setVisibility(View.GONE);
} else {
icon.setVisibility(View.VISIBLE);
icon.setImageResource(item.icon);
}
checkbox.setChecked(item.checked && item.enabled);
checkbox.setEnabled(item.enabled);
}
@@ -928,19 +1033,19 @@ public class SettingsFragment extends MastodonToolbarFragment{
private class SmallTextViewHolder extends BindableViewHolder<SmallTextItem> {
private final TextView text;
;
public SmallTextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
text.setPaddingRelative(text.getPaddingStart(), 0, text.getPaddingEnd(), text.getPaddingBottom());
}
@Override
public void onBind(SmallTextItem item){
text.setText(item.text);
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
}
}

View File

@@ -173,16 +173,7 @@ public class SplashFragment extends AppKitFragment{
TextView title=new TextView(getActivity());
title.setTextAppearance(R.style.m3_headline_medium);
title.setText(switch(page){
case 0 -> {
String src=getString(R.string.welcome_page1_title);
SpannableString ss=new SpannableString(src);
int start=src.indexOf("{logo}");
if(start!=-1){
LogoSpan span=new LogoSpan(getResources().getDrawable(R.drawable.splash_logo, getActivity().getTheme()));
ss.setSpan(span, start, start+6, 0);
}
yield ss;
}
case 0 -> getString(R.string.welcome_page1_title);
case 1 -> getString(R.string.welcome_page2_title);
case 2 -> getString(R.string.welcome_page3_title);
default -> throw new IllegalStateException("Unexpected value: "+page);
@@ -204,26 +195,4 @@ public class SplashFragment extends AppKitFragment{
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
}
private class LogoSpan extends ReplacementSpan{
private final Drawable drawable;
private LogoSpan(Drawable drawable){
this.drawable=drawable;
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
return drawable.getIntrinsicWidth();
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
canvas.save();
canvas.translate(x, y-V.dp(20));
drawable.draw(canvas);
canvas.restore();
}
}
}

View File

@@ -46,6 +46,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
public void onSuccess(List<Status> result){
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
if (getActivity() == null) return;
onDataLoaded(result, false);
}
})
@@ -139,7 +140,8 @@ public class StatusEditHistoryFragment extends StatusListFragment{
action=getString(R.string.edit_multiple_changed);
}
}
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null, null));
String sep = getString(R.string.sk_separator);
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null));
}
return items;
}

View File

@@ -6,12 +6,14 @@ import android.os.Bundle;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
@@ -30,13 +32,17 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME);
}
@Override
protected void addAccountToKnown(Status s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.reblog!=null && !knownAccounts.containsKey(s.reblog.account.id))
knownAccounts.put(s.reblog.account.id, s.reblog.account);
}
@Override
@@ -56,6 +62,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
Status status=getContentStatusByID(id);
if(status==null)
return;
status.filterRevealed = true;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));

View File

@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class ThreadFragment extends StatusListFragment{
private Status mainStatus;
protected Status mainStatus;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -61,8 +61,7 @@ public class ThreadFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if(getActivity()==null)
return;
if (getActivity() == null) return;
if(refreshing){
data.clear();
displayItems.clear();
@@ -126,4 +125,14 @@ public class ThreadFragment extends StatusListFragment{
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id);
}
@Override
public boolean wantsLightStatusBar(){
return !UiUtils.isDarkTheme();
}
@Override
public boolean wantsLightNavigationBar(){
return !UiUtils.isDarkTheme();
}
}

View File

@@ -101,6 +101,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
for(Relationship rel:result){
relationships.put(rel.id, rel);
}
if (getActivity() == null) return;
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
@@ -128,7 +129,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
super.onViewCreated(view, savedInstanceState);
// list.setPadding(0, V.dp(16), 0, V.dp(16));
list.setClipToPadding(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1,
Math.round(16f + 56f * getResources().getConfiguration().fontScale), 16));
updateToolbar();
}
@@ -370,6 +372,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
@Override
public void onSuccess(Relationship result){
relationships.put(AccountViewHolder.this.item.account.id, result);
if (getActivity() == null) return;
bindRelationship();
}

View File

@@ -23,6 +23,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
if (getActivity() == null) return;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
}
})

View File

@@ -74,6 +74,7 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
if (getActivity() == null) return;
onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false);
loadRelationships();
}
@@ -108,6 +109,7 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
public void onSuccess(List<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if (getActivity() == null) return;
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){

View File

@@ -59,6 +59,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
imageRequests=result.stream()
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150)))
.collect(Collectors.toList());
if (getActivity() == null) return;
onDataLoaded(result, false);
}
})

View File

@@ -6,10 +6,13 @@ import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
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;
@@ -22,6 +25,8 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
}).exec(accountID);

View File

@@ -5,7 +5,6 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.FabStatusListFragment;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -17,10 +16,16 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class FederatedTimelineFragment extends FabStatusListFragment {
public class FederatedTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
private String maxID;
@Override
protected boolean withComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
@@ -29,7 +34,9 @@ public class FederatedTimelineFragment extends FabStatusListFragment {
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);

View File

@@ -3,9 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.FabStatusListFragment;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -17,10 +15,16 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class LocalTimelineFragment extends FabStatusListFragment {
public class LocalTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
private String maxID;
@Override
protected boolean withComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@@ -29,7 +33,9 @@ public class LocalTimelineFragment extends FabStatusListFragment {
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);

View File

@@ -62,7 +62,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
loadData();
setEmptyText(R.string.sk_recent_searches_placeholder);
resetEmptyText();
}
@Override
@@ -71,6 +71,10 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
imm=activity.getSystemService(InputMethodManager.class);
}
private void resetEmptyText() {
setEmptyText(R.string.sk_recent_searches_placeholder);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){
return switch(s.type){
@@ -120,6 +124,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
@Override
protected void doLoadData(int offset, int count){
if (getActivity() == null) return;
resetEmptyText();
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
if(getActivity()==null)
@@ -129,11 +135,13 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
onDataLoaded(sr, false);
});
}else{
setEmptyText(R.string.sk_searching);
progressVisibilityListener.onProgressVisibilityChanged(true);
currentRequest=new GetSearchResults(currentQuery, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.sk_no_results);
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
@@ -149,11 +157,13 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
if (getActivity() == null) return;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
resetEmptyText();
currentRequest=null;
Activity a=getActivity();
if(a==null)

View File

@@ -44,6 +44,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Hashtag> result){
if (getActivity() == null) return;
onDataLoaded(result, false);
}
})

View File

@@ -193,30 +193,24 @@ public class AccountActivationFragment extends ToolbarFragment{
mgr.removeAccount(accountID);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, null);
String newID=mgr.getLastActiveAccountID();
Bundle args=new Bundle();
args.putString("account", newID);
if(session.self.avatar!=null || session.self.displayName!=null){
File avaFile=session.self.avatar!=null ? new File(session.self.avatar) : null;
new UpdateAccountCredentials(session.self.displayName, "", avaFile, null, Collections.emptyList())
accountID=newID;
if((session.self.avatar!=null || session.self.displayName!=null) && !getArguments().getBoolean("debug")){
new UpdateAccountCredentials(session.self.displayName, "", (File)null, null, Collections.emptyList())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
if(avaFile!=null)
avaFile.delete();
mgr.updateAccountInfo(newID, result);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
proceed();
}
@Override
public void onError(ErrorResponse error){
if(avaFile!=null)
avaFile.delete();
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
proceed();
}
})
.exec(newID);
}else{
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
proceed();
}
}
@@ -249,4 +243,11 @@ public class AccountActivationFragment extends ToolbarFragment{
super.onDestroyView();
resendBtn.removeCallbacks(resendTimer);
}
private void proceed(){
Bundle args=new Bundle();
args.putString("account", accountID);
// Nav.goClearingStack(getActivity(), HomeFragment.class, args);
Nav.goClearingStack(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
}
}

View File

@@ -5,6 +5,7 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -19,6 +20,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.parceler.Parcels;
@@ -42,6 +44,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Callback;
@@ -58,6 +61,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private ArrayList<Item> items=new ArrayList<>();
private Call currentRequest;
private ItemsAdapter itemsAdapter;
private ElevationOnScrollListener onScrollListener;
private static final int SIGNUP_REQUEST=722;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -72,7 +78,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
items.add(new Item("Mastodon for Android Privacy Policy", getString(R.string.privacy_policy_explanation), "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
loadServerPrivacyPolicy();
}
@@ -93,18 +99,24 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text);
text.setText(R.string.privacy_policy_subtitle);
text.setText(getString(R.string.privacy_policy_subtitle, instance.uri));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
Button backBtn=view.findViewById(R.id.btn_back);
backBtn.setText(getString(R.string.server_policy_disagree, instance.uri));
backBtn.setOnClickListener(v->{
setResult(false, null);
Nav.finish(this);
});
return view;
}
@@ -113,19 +125,32 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), SignupFragment.class, args);
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
super.onFragmentResult(reqCode, success, result);
if(reqCode==SIGNUP_REQUEST && !success){
setResult(false, null);
Nav.finish(this);
}
}
@Override
@@ -158,7 +183,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
if(!response.isSuccessful())
return;
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
final Item item=new Item(doc.title(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
final Item item=new Item(doc.title(), null, instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
Activity activity=getActivity();
if(activity!=null){
activity.runOnUiThread(()->{
@@ -192,16 +217,23 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView title;
private final TextView subtitle;
public ItemViewHolder(){
super(getActivity(), R.layout.item_privacy_policy_link, list);
title=findViewById(R.id.title);
title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
subtitle=findViewById(R.id.subtitle);
}
@Override
public void onBind(Item item){
title.setText(item.title);
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
}
}
@Override
@@ -211,10 +243,11 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
}
private static class Item{
public String title, domain, url, faviconUrl;
public String title, subtitle, domain, url, faviconUrl;
public Item(String title, String domain, String url, String faviconUrl){
public Item(String title, String subtitle, String domain, String url, String faviconUrl){
this.title=title;
this.subtitle=subtitle;
this.domain=domain;
this.url=url;
this.faviconUrl=faviconUrl;

View File

@@ -5,15 +5,12 @@ import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -23,7 +20,6 @@ import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -31,6 +27,8 @@ import org.xml.sax.InputSource;
import java.io.IOException;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -50,7 +48,6 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@@ -92,7 +89,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -106,52 +103,16 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
updateFilteredList();
loadInstanceInfo(currentSearchQuery, false);
}
protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
for(List<CatalogInstance> group:byLang.values()){
Collections.sort(group, (a, b)->{
double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers));
double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers));
return Double.compare(aa, bb);
});
}
// get the list of user-configured system languages
List<String> userLangs;
if(Build.VERSION.SDK_INT<24){
userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage());
}else{
LocaleList ll=getResources().getConfiguration().getLocales();
userLangs=new ArrayList<>(ll.size());
for(int i=0;i<ll.size();i++){
userLangs.add(ll.get(i).getLanguage());
}
}
// add instances in preferred languages to the top of the list, in the order of preference
Map<Boolean, List<CatalogInstance>> byLang=result.stream().sorted(Comparator.comparingInt((CatalogInstance ci)->ci.lastWeekUsers).reversed()).collect(Collectors.groupingBy(ci->ci.approvalRequired));
ArrayList<CatalogInstance> sortedList=new ArrayList<>();
for(String lang:userLangs){
List<CatalogInstance> langInstances=byLang.remove(lang);
if(langInstances!=null){
sortedList.addAll(langInstances);
}
}
// sort the remaining language groups by aggregate lastWeekUsers
class InstanceGroup{
public int activeUsers;
public List<CatalogInstance> instances;
}
byLang.values().stream().map(il->{
InstanceGroup group=new InstanceGroup();
group.instances=il;
for(CatalogInstance instance:il){
group.activeUsers+=instance.lastWeekUsers;
}
return group;
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
sortedList.addAll(byLang.getOrDefault(false, Collections.emptyList()));
sortedList.addAll(byLang.getOrDefault(true, Collections.emptyList()));
return sortedList;
}
@@ -208,6 +169,20 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
cancelLoadingInstanceInfo();
}
}
try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
ivh.rebind();
}
}
}
return;
}
loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance();
loadingInstanceRequest.setCallback(new Callback<>(){

View File

@@ -1,14 +1,8 @@
package org.joinmastodon.android.fragments.onboarding;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
@@ -20,7 +14,6 @@ import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.RadioButton;
@@ -38,6 +31,7 @@ import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.util.ArrayList;
@@ -56,11 +50,7 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
@@ -74,7 +64,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private List<String> languages=Collections.emptyList();
private PopupMenu langFilterMenu, speedFilterMenu;
private SignupSpeedFilter currentSignupSpeedFilter=SignupSpeedFilter.INSTANT;
private SignupSpeedFilter currentSignupSpeedFilter=SignupSpeedFilter.ANY;
private String currentLanguage=null;
private boolean searchQueryMode;
private LinearLayout filtersWrap;
@@ -85,7 +75,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private FilterChipView categoryGeneral, categorySpecialInterests;
private List<FilterChipView> regionalFilters;
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
@@ -215,47 +205,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
setStatusBarColor(0);
topBar=view.findViewById(R.id.top_bar);
LayerDrawable topBg=(LayerDrawable) topBar.getBackground().mutate();
topBar.setBackground(topBg);
Drawable topOverlay=topBg.findDrawableByLayerId(R.id.color_overlay);
topOverlay.setAlpha(0);
LayerDrawable btmBg=(LayerDrawable) buttonBar.getBackground().mutate();
buttonBar.setBackground(btmBg);
Drawable btmOverlay=btmBg.findDrawableByLayerId(R.id.color_overlay);
btmOverlay.setAlpha(0);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
private boolean isAtTop=true;
private Animator currentPanelsAnim;
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
if(newAtTop!=isAtTop){
isAtTop=newAtTop;
if(currentPanelsAnim!=null)
currentPanelsAnim.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(topOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofInt(btmOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofFloat(topBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)),
ObjectAnimator.ofFloat(buttonBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3))
);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentPanelsAnim=null;
}
});
set.start();
currentPanelsAnim=set;
}
}
});
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar));
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
@@ -366,6 +316,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
}).collect(Collectors.toList());
focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus();
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
nextButton.setEnabled(chosenInstance!=null);
}
private void onRegionFilterClick(View v){
@@ -396,22 +349,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
updateFilteredList();
}
@Override
protected void onNextClick(View v){
if(chosenInstance==null){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
}
super.onNextClick(v);
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
@@ -428,6 +365,22 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
private void onPickRandomInstanceClick(View v){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
onNextClick(v);
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
@@ -577,7 +530,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
updateFilteredList();
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder> implements ImageLoaderRecyclerAdapter{
@Override
protected void onShown(){
super.onShown();
if(!searchQueryMode){
// Prevent search view automatically getting focused when the user returns to this fragment
focusThing.requestFocus();
}
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
}
@@ -603,22 +565,11 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
public int getItemViewType(int position){
return -1;
}
@Override
public int getImageCountForItem(int position){
return filteredData.get(position).thumbnailRequest!=null ? 1 : 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return filteredData.get(position).thumbnailRequest;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable, ImageLoaderViewHolder{
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable{
private final TextView title, description;
private final RadioButton radioButton;
private final ImageView thumbnail;
private boolean enabled;
public InstanceViewHolder(){
@@ -626,15 +577,12 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
title=findViewById(R.id.title);
description=findViewById(R.id.description);
radioButton=findViewById(R.id.radiobtn);
thumbnail=findViewById(R.id.image);
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
radioButton.setChecked(chosenInstance==item);
if(item.thumbnailRequest==null)
thumbnail.setImageDrawable(null);
Instance realInstance=instancesCache.get(item.normalizedDomain);
float alpha;
if(realInstance!=null && !realInstance.registrations){
@@ -649,7 +597,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
title.setAlpha(alpha);
description.setAlpha(alpha);
radioButton.setAlpha(alpha);
thumbnail.setAlpha(alpha);
}
@Override
@@ -672,6 +619,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
adapter.notifyItemChanged(idx);
}
}
if(!nextButton.isEnabled()){
nextButton.setEnabled(true);
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
@@ -679,16 +629,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
loadInstanceInfo(chosenInstance.domain, false);
}
@Override
public void setImage(int index, Drawable image){
thumbnail.setImageDrawable(image);
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public boolean isEnabled(){
return enabled;
@@ -710,4 +650,5 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
return (this==GENERAL)==isGeneral;
}
}
}

View File

@@ -2,8 +2,14 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -17,6 +23,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import androidx.annotation.NonNull;
@@ -28,6 +35,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{
@@ -36,6 +44,9 @@ public class InstanceRulesFragment extends ToolbarFragment{
private Button btn;
private View buttonBar;
private Instance instance;
private ElevationOnScrollListener onScrollListener;
private static final int RULES_REQUEST=376;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -59,7 +70,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text);
text.setText(getString(R.string.instance_rules_subtitle, instance.uri));
text.setText(Html.fromHtml(getString(R.string.instance_rules_subtitle, "<b>"+Html.escapeHtml(instance.uri)+"</b>")));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
@@ -71,6 +82,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@@ -79,19 +92,31 @@ public class InstanceRulesFragment extends ToolbarFragment{
super.onViewCreated(view, savedInstanceState);
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args);
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
super.onFragmentResult(reqCode, success, result);
if(reqCode==RULES_REQUEST && !success){
Nav.finish(this);
}
}
@Override

View File

@@ -0,0 +1,344 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
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.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.ParsedAccount;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
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.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.popular_on_mastodon);
accountID=getArguments().getString("account");
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
@Override
protected void doLoadData(int offset, int count){
new GetFollowSuggestions(40)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
loadRelationships();
}
})
.exec(accountID);
}
private void loadRelationships(){
relationships=Collections.emptyMap();
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
relationshipsRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof SuggestionViewHolder svh)
svh.rebind();
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequest=null;
}
}).exec(accountID);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
@Override
protected RecyclerView.Adapter getAdapter(){
return new SuggestionsAdapter();
}
private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty())
return;
if(data.isEmpty()){
proceed();
return;
}
ArrayList<String> accountIdsToFollow=new ArrayList<>();
for(ParsedAccount acc:data){
Relationship rel=relationships.get(acc.account.id);
if(rel==null)
continue;
if(rel.canFollow())
accountIdsToFollow.add(acc.account.id);
}
final ProgressDialog progress=new ProgressDialog(getActivity());
progress.setIndeterminate(false);
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progress.setMax(accountIdsToFollow.size());
progress.setCancelable(false);
progress.setMessage(getString(R.string.sending_follows));
progress.show();
for(int i=0;i<Math.min(accountIdsToFollow.size(), 5);i++){ // Send up to 5 requests in parallel
followNextAccount(accountIdsToFollow, progress);
}
}
private void followNextAccount(ArrayList<String> accountIdsToFollow, ProgressDialog progress){
if(accountIdsToFollow.isEmpty()){
if(numRunningFollowRequests==0){
progress.dismiss();
proceed();
}
return;
}
numRunningFollowRequests++;
String id=accountIdsToFollow.remove(0);
new SetAccountFollowed(id, true, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(id, result);
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof SuggestionViewHolder svh && svh.getItem().account.id.equals(id)){
svh.rebind();
break;
}
}
numRunningFollowRequests--;
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
followNextAccount(accountIdsToFollow, progress);
}
@Override
public void onError(ErrorResponse error){
numRunningFollowRequests--;
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
followNextAccount(accountIdsToFollow, progress);
}
})
.exec(accountID);
}
private void proceed(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), OnboardingProfileSetupFragment.class, args);
}
private class SuggestionsAdapter extends UsableRecyclerView.Adapter<SuggestionViewHolder> implements ImageLoaderRecyclerAdapter{
public SuggestionsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new SuggestionViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public void onBindViewHolder(SuggestionViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return data.get(position).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
ParsedAccount account=data.get(position);
if(image==0)
return account.avatarRequest;
return account.emojiHelper.getImageRequest(image-1);
}
}
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, bio;
private final ImageView avatar;
private final ProgressBarButton actionButton;
private final ProgressBar actionProgress;
private final View actionWrap;
private Relationship relationship;
public SuggestionViewHolder(){
super(getActivity(), R.layout.item_user_row_m3, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
bio=findViewById(R.id.bio);
avatar=findViewById(R.id.avatar);
actionButton=findViewById(R.id.action_btn);
actionProgress=findViewById(R.id.action_progress);
actionWrap=findViewById(R.id.action_btn_wrap);
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
avatar.setClipToOutline(true);
actionButton.setOnClickListener(UiUtils.rateLimitedClickListener(this::onActionButtonClick));
}
@Override
public void onBind(ParsedAccount item){
name.setText(item.parsedName);
username.setText(item.account.getDisplayUsername());
if(TextUtils.isEmpty(item.parsedBio)){
bio.setVisibility(View.GONE);
}else{
bio.setVisibility(View.VISIBLE);
bio.setText(item.parsedBio);
}
relationship=relationships.get(item.account.id);
if(relationship==null){
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
bio.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private void onActionButtonClick(View v){
itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
rebind();
});
}
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
actionButton.setClickable(!visible);
}
}
}

View File

@@ -0,0 +1,229 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ScrollView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
private Button btn;
private View buttonBar;
private String accountID;
private ElevationOnScrollListener onScrollListener;
private ScrollView scroller;
private EditText nameEdit, bioEdit;
private ImageView avaImage, coverImage;
private Button addRow;
private ReorderableLinearLayout profileFieldsLayout;
private Uri avatarUri, coverUri;
private static final int AVATAR_RESULT=348;
private static final int COVER_RESULT=183;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
setTitle(R.string.profile_setup);
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_profile_setup, container, false);
scroller=view.findViewById(R.id.scroller);
nameEdit=view.findViewById(R.id.display_name);
bioEdit=view.findViewById(R.id.bio);
avaImage=view.findViewById(R.id.avatar);
coverImage=view.findViewById(R.id.header);
addRow=view.findViewById(R.id.add_row);
profileFieldsLayout=view.findViewById(R.id.profile_fields);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
avaImage.setOutlineProvider(OutlineProviders.roundedRect(24));
avaImage.setClipToOutline(true);
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
if(savedInstanceState==null){
nameEdit.setText(account.displayName);
makeFieldsRow();
}else{
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
for(int i=0;i<fieldTitles.size();i++){
View row=makeFieldsRow();
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
title.setText(fieldTitles.get(i));
content.setText(fieldValues.get(i));
}
if(fieldTitles.size()==4)
addRow.setVisibility(View.GONE);
}
addRow.setOnClickListener(v->{
makeFieldsRow();
if(profileFieldsLayout.getChildCount()==4){
addRow.setVisibility(View.GONE);
}
});
profileFieldsLayout.setDragListener(this);
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
scroller.setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
protected void onButtonClick(){
ArrayList<AccountField> fields=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
AccountField fld=new AccountField();
fld.name=title.getText().toString();
fld.value=content.getText().toString();
fields.add(fld);
}
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
AccountSessionManager.getInstance().updateAccountInfo(accountID, result);
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.saving, true)
.exec(accountID);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private View makeFieldsRow(){
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
profileFieldsLayout.addView(view);
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
profileFieldsLayout.startDragging(view);
return true;
});
view.findViewById(R.id.delete).setOnClickListener(v->{
profileFieldsLayout.removeView(view);
if(addRow.getVisibility()==View.GONE)
addRow.setVisibility(View.VISIBLE);
});
return view;
}
@Override
public void onSwapItems(int oldIndex, int newIndex){}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
fieldTitles.add(title.getText().toString());
fieldValues.add(content.getText().toString());
}
outState.putStringArrayList("fieldTitles", fieldTitles);
outState.putStringArrayList("fieldValues", fieldValues);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode!=Activity.RESULT_OK)
return;
ImageView img;
Uri uri=data.getData();
int size;
if(requestCode==AVATAR_RESULT){
img=avaImage;
avatarUri=uri;
size=V.dp(100);
}else{
img=coverImage;
coverUri=uri;
size=V.dp(1000);
}
img.setForeground(null);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
}
}

View File

@@ -1,14 +1,18 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -16,11 +20,9 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
@@ -31,18 +33,22 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
@@ -51,12 +57,10 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends ToolbarFragment{
private static final int AVATAR_RESULT=198;
private static final String TAG="SignupFragment";
private Instance instance;
@@ -73,6 +77,7 @@ public class SignupFragment extends ToolbarFragment{
private boolean submitAfterGettingToken;
private ProgressDialog progressDialog;
private HashSet<EditText> errorFields=new HashSet<>();
private ElevationOnScrollListener onScrollListener;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -145,19 +150,22 @@ public class SignupFragment extends ToolbarFragment{
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
private void onButtonClick(){
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
passwordConfirmWrap.setErrorState();
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
return;
}
showProgressDialog();
@@ -212,8 +220,22 @@ public class SignupFragment extends ToolbarFragment{
anyFieldsSkipped=true;
continue;
}
field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n")));
getFieldWrapByName(fieldName).setErrorState();
List<MastodonDetailedErrorResponse.FieldError> errors=Objects.requireNonNull(fieldErrors.get(fieldName));
if(errors.size()==1){
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
}else{
SpannableStringBuilder ssb=new SpannableStringBuilder();
boolean firstErr=true;
for(MastodonDetailedErrorResponse.FieldError err:errors){
if(firstErr){
firstErr=false;
}else{
ssb.append('\n');
}
ssb.append(getErrorDescription(err, fieldName));
}
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
}
errorFields.add(field);
if(first){
first=false;
@@ -231,6 +253,40 @@ public class SignupFragment extends ToolbarFragment{
.exec(instance.uri, apiToken);
}
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
return switch(fieldName){
case "email" -> switch(error.error){
case "ERR_BLOCKED" -> {
String emailAddr=email.getText().toString();
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
yield ssb;
}
default -> error.description;
};
default -> error.description;
};
}
private EditText getFieldByName(String name){
return switch(name){
case "email" -> email;
@@ -323,6 +379,11 @@ public class SignupFragment extends ToolbarFragment{
}
}
private void onGoBackLinkClick(LinkSpan span){
setResult(false, null);
Nav.finish(this);
}
private class ErrorClearingListener implements TextWatcher{
public final EditText editText;

View File

@@ -22,10 +22,8 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -89,6 +87,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if (getActivity() == null) return;
onDataLoaded(result, !result.isEmpty());
}
})
@@ -131,22 +130,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
if(holder.getAbsoluteAdapterPosition()==0)
return;
outRect.left=V.dp(40);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
PhotoLayoutHelper.TiledLayoutResult layout=imgHolder.getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
String siblingID;
if(holder.getAbsoluteAdapterPosition()<parent.getAdapter().getItemCount()-1){
siblingID=displayItems.get(holder.getAbsoluteAdapterPosition()-getMainAdapterOffset()+1).parentID;
}else{
siblingID=null;
}
if(tile.startCol>0)
outRect.left=0;
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
if(!imgHolder.getItemID().equals(siblingID) || tile.startRow+tile.rowSpan==layout.rowSizes.length)
outRect.bottom=V.dp(16);
}else if(holder instanceof AudioStatusDisplayItem.Holder){
if(holder instanceof AudioStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
@@ -165,10 +149,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
parent.getDecoratedBoundsWithMargins(child, tmpRect);
String id=sdiHolder.getItemID();
int height=tmpRect.height();
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(imgHolder.getItem().thisTile.startCol+imgHolder.getItem().thisTile.colSpan<imgHolder.getItem().tiledLayout.columnSizes.length)
height=0;
}
if(!(holder instanceof HeaderStatusDisplayItem.Holder) && !(holder instanceof ReblogOrReplyLineStatusDisplayItem.Holder))
postsWithKnownNonHeaderHeights.add(id);
knownDisplayItemHeights.put(holder.getAbsoluteAdapterPosition(), height);
@@ -235,17 +215,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
return adapter;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem isdi){
isdi.horizontalInset=V.dp(40+32);
}
}
return items;
}
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
tmpRect.offset(0, Math.round(child.getTranslationY()));

View File

@@ -14,7 +14,7 @@ import java.util.List;
* Represents a user of Mastodon and their associated profile.
*/
@Parcel
public class Account extends BaseModel{
public class Account extends BaseModel implements Searchable{
// Base attributes
/**
@@ -133,6 +133,19 @@ public class Account extends BaseModel{
*/
public Instant muteExpiresAt;
public List<Role> roles;
@Override
public String getQuery() {
return url;
}
@Parcel
public static class Role {
public String name;
/** #rrggbb */
public String color;
}
@Override
public void postprocess() throws ObjectValidationException{

View File

@@ -42,17 +42,9 @@ public class Announcement extends BaseModel implements DisplayItemsParent {
}
public Status toStatus() {
Status s = new Status();
s.id = id;
s.mediaAttachments = List.of();
Status s = Status.ofFake(id, content, publishedAt);
s.createdAt = startsAt != null ? startsAt : publishedAt;
if (updatedAt != null) s.editedAt = updatedAt;
s.content = s.text = content;
s.spoilerText = "";
s.visibility = StatusPrivacy.PUBLIC;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
return s;
}

View File

@@ -47,26 +47,26 @@ 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 double getDuration(){

View File

@@ -19,6 +19,7 @@ public class Filter extends BaseModel{
public String id;
@RequiredField
public String phrase;
public String title;
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
public Instant expiresAt;
public boolean irreversible;
@@ -50,6 +51,7 @@ public class Filter extends BaseModel{
else
pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
}
if (title == null) title = phrase;
return pattern.matcher(text).find();
}
@@ -61,6 +63,7 @@ public class Filter extends BaseModel{
public String toString(){
return "Filter{"+
"id='"+id+'\''+
", title='"+title+'\''+
", phrase='"+phrase+'\''+
", context="+context+
", expiresAt="+expiresAt+
@@ -77,7 +80,9 @@ public class Filter extends BaseModel{
@SerializedName("public")
PUBLIC,
@SerializedName("thread")
THREAD
THREAD,
@SerializedName("account")
ACCOUNT
}
public enum FilterAction{

View File

@@ -45,7 +45,7 @@ public class Instance extends BaseModel{
@RequiredField
public String version;
/**
* Primary langauges of the website and its staff.
* Primary languages of the website and its staff.
*/
// @RequiredField
public List<String> languages;
@@ -84,6 +84,8 @@ public class Instance extends BaseModel{
public V2 v2;
public Pleroma pleroma;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@@ -119,7 +121,7 @@ public class Instance extends BaseModel{
ci.domain=uri;
ci.normalizedDomain=IDN.toUnicode(uri);
ci.description=Html.fromHtml(shortDescription).toString().trim();
if(languages!=null){
if(languages!=null && languages.size() > 0){
ci.language=languages.get(0);
ci.languages=languages;
}else{
@@ -193,4 +195,9 @@ public class Instance extends BaseModel{
public boolean enabled;
}
}
@Parcel
public static class Pleroma extends BaseModel {
// metadata etc
}
}

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.model;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.RequiredField;
@@ -11,9 +13,9 @@ public class ListTimeline extends BaseModel {
public String id;
@RequiredField
public String title;
@RequiredField
public RepliesPolicy repliesPolicy;
@NonNull
@Override
public String toString() {
return "List{" +

View File

@@ -18,8 +18,8 @@ public class Notification extends BaseModel implements DisplayItemsParent{
public Instant createdAt;
@RequiredField
public Account account;
public Status status;
public Report report;
@Override
public void postprocess() throws ObjectValidationException{
@@ -48,6 +48,19 @@ public class Notification extends BaseModel implements DisplayItemsParent{
@SerializedName("poll")
POLL,
@SerializedName("status")
STATUS
STATUS,
@SerializedName("update")
UPDATE,
@SerializedName("admin.sign_up")
SIGN_UP,
@SerializedName("admin.report")
REPORT
}
@Parcel
public static class Report {
public String id;
public String comment;
public Account targetAccount;
}
}

View File

@@ -0,0 +1,33 @@
package org.joinmastodon.android.model;
import android.text.SpannableStringBuilder;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import java.util.Collections;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ParsedAccount{
public Account account;
public CharSequence parsedName, parsedBio;
public CustomEmojiHelper emojiHelper;
public ImageLoaderRequest avatarRequest;
public ParsedAccount(Account account, String accountID){
this.account=account;
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
emojiHelper=new CustomEmojiHelper();
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
ssb.append(parsedBio);
emojiHelper.setText(ssb);
avatarRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(40), V.dp(40));
}
}

View File

@@ -16,6 +16,7 @@ public class Poll extends BaseModel{
private boolean expired;
public boolean multiple;
public int votersCount;
public int votesCount;
public boolean voted;
@RequiredField
public List<Integer> ownVotes;
@@ -41,10 +42,12 @@ public class Poll extends BaseModel{
", expired="+expired+
", multiple="+multiple+
", votersCount="+votersCount+
", votesCount="+votesCount+
", voted="+voted+
", ownVotes="+ownVotes+
", options="+options+
", emojis="+emojis+
", selectedOptions="+selectedOptions+
'}';
}

View File

@@ -45,7 +45,13 @@ public class PushNotification extends BaseModel{
@SerializedName("poll")
POLL(R.string.notification_type_poll),
@SerializedName("status")
STATUS(R.string.sk_notification_type_status);
STATUS(R.string.sk_notification_type_status),
@SerializedName("update")
UPDATE(R.string.sk_notification_type_update),
@SerializedName("admin.sign_up")
SIGN_UP(R.string.sk_sign_ups),
@SerializedName("admin.report")
REPORT(R.string.sk_new_reports);
@StringRes
public final int localizedName;

View File

@@ -23,6 +23,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
", endpoint='"+endpoint+'\''+
", alerts="+alerts+
", serverKey='"+serverKey+'\''+
", policy="+policy+
'}';
}
@@ -44,10 +45,19 @@ public class PushSubscription extends BaseModel implements Cloneable{
public boolean mention;
public boolean poll;
public boolean status;
public boolean update;
// set to true here because i didn't add any items for those to the settings
// (so i don't have to determine whether the user is an admin to show the items or not, and
// admins can still disable those through the android notifications settings)
@SerializedName("admin.sign_up")
public boolean adminSignUp = true;
@SerializedName("admin.report")
public boolean adminReport = true;
public static Alerts ofAll(){
Alerts alerts=new Alerts();
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=alerts.status=true;
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=alerts.status=alerts.update=true;
return alerts;
}
@@ -60,6 +70,9 @@ public class PushSubscription extends BaseModel implements Cloneable{
", mention="+mention+
", poll="+poll+
", status="+status+
", update="+update+
", adminSignUp="+adminSignUp+
", adminReport="+adminReport+
'}';
}

View File

@@ -18,6 +18,10 @@ public class Relationship extends BaseModel{
public boolean blockedBy;
public String note;
public boolean canFollow(){
return !(following || blocking || blockedBy || domainBlocking);
}
@Override
public String toString(){
return "Relationship{"+

View File

@@ -62,19 +62,13 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
}
public Status toStatus() {
Status s = new Status();
s.id = id;
Status s = Status.ofFake(id, params.text, scheduledAt);
s.mediaAttachments = mediaAttachments;
s.createdAt = scheduledAt;
s.inReplyToId = "" + params.inReplyToId;
s.content = s.text = params.text;
s.inReplyToId = params.inReplyToId > 0 ? "" + params.inReplyToId : null;
s.spoilerText = params.spoilerText;
s.visibility = params.visibility;
s.language = params.language;
s.sensitive = params.sensitive;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
if (params.poll != null) s.poll = params.poll.toPoll();
return s;
}

View File

@@ -0,0 +1,5 @@
package org.joinmastodon.android.model;
public interface Searchable {
String getQuery();
}

View File

@@ -11,7 +11,7 @@ import java.time.Instant;
import java.util.List;
@Parcel
public class Status extends BaseModel implements DisplayItemsParent{
public class Status extends BaseModel implements DisplayItemsParent, Searchable{
@RequiredField
public String id;
@RequiredField
@@ -50,6 +50,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public Card card;
public String language;
public String text;
public boolean localOnly;
public boolean favourited;
public boolean reblogged;
@@ -57,7 +58,9 @@ public class Status extends BaseModel implements DisplayItemsParent{
public boolean bookmarked;
public boolean pinned;
public transient boolean filterRevealed;
public transient boolean spoilerRevealed;
public transient boolean textExpanded, textExpandable;
public transient boolean hasGapAfter;
private transient String strippedText;
@@ -83,6 +86,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
reblog.postprocess();
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
}
@Override
@@ -144,4 +148,24 @@ public class Status extends BaseModel implements DisplayItemsParent{
strippedText=HtmlParser.strip(content);
return strippedText;
}
public static Status ofFake(String id, String text, Instant createdAt) {
Status s = new Status();
s.id = id;
s.mediaAttachments = List.of();
s.createdAt = createdAt;
s.content = s.text = text;
s.spoilerText = "";
s.visibility = StatusPrivacy.PUBLIC;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
s.filtered = List.of();
return s;
}
@Override
public String getQuery() {
return url;
}
}

View File

@@ -10,7 +10,9 @@ public enum StatusPrivacy{
@SerializedName("private")
PRIVATE(2),
@SerializedName("direct")
DIRECT(3);
DIRECT(3),
@SerializedName("local")
LOCAL(4); // akkoma
private int privacy;

View File

@@ -0,0 +1,252 @@
package org.joinmastodon.android.model;
import android.app.Fragment;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
import java.util.List;
import java.util.Objects;
public class TimelineDefinition {
private TimelineType type;
private String title;
private @Nullable Icon icon;
private @Nullable String listId;
private @Nullable String listTitle;
private @Nullable String hashtagName;
public static TimelineDefinition ofList(String listId, String listTitle) {
TimelineDefinition def = new TimelineDefinition(TimelineType.LIST);
def.listId = listId;
def.listTitle = listTitle;
return def;
}
public static TimelineDefinition ofList(ListTimeline list) {
return ofList(list.id, list.title);
}
public static TimelineDefinition ofHashtag(String hashtag) {
TimelineDefinition def = new TimelineDefinition(TimelineType.HASHTAG);
def.hashtagName = hashtag;
return def;
}
public static TimelineDefinition ofHashtag(Hashtag hashtag) {
return ofHashtag(hashtag.name);
}
@SuppressWarnings("unused")
public TimelineDefinition() {}
public TimelineDefinition(TimelineType type) {
this.type = type;
}
public String getTitle(Context ctx) {
return title != null ? title : getDefaultTitle(ctx);
}
public String getCustomTitle() {
return title;
}
public void setTitle(String title) {
this.title = title == null || title.isBlank() ? null : title;
}
public String getDefaultTitle(Context ctx) {
return switch (type) {
case HOME -> ctx.getString(R.string.sk_timeline_home);
case LOCAL -> ctx.getString(R.string.sk_timeline_local);
case FEDERATED -> ctx.getString(R.string.sk_timeline_federated);
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
case LIST -> listTitle;
case HASHTAG -> hashtagName;
};
}
public Icon getDefaultIcon() {
return switch (type) {
case HOME -> Icon.HOME;
case LOCAL -> Icon.LOCAL;
case FEDERATED -> Icon.FEDERATED;
case POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
case LIST -> Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
};
}
public Fragment getFragment() {
return switch (type) {
case HOME -> new HomeTimelineFragment();
case LOCAL -> new LocalTimelineFragment();
case FEDERATED -> new FederatedTimelineFragment();
case LIST -> new ListTimelineFragment();
case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment();
};
}
@Nullable
public Icon getIcon() {
return icon == null ? getDefaultIcon() : icon;
}
public void setIcon(@Nullable Icon icon) {
this.icon = icon;
}
public TimelineType getType() {
return type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TimelineDefinition that = (TimelineDefinition) o;
if (type != that.type) return false;
if (type == TimelineType.LIST) return Objects.equals(listId, that.listId);
if (type == TimelineType.HASHTAG) return Objects.equals(hashtagName.toLowerCase(), that.hashtagName.toLowerCase());
return true;
}
@Override
public int hashCode() {
int result = type.ordinal();
result = 31 * result + (listId != null ? listId.hashCode() : 0);
result = 31 * result + (hashtagName.toLowerCase() != null ? hashtagName.toLowerCase().hashCode() : 0);
return result;
}
public TimelineDefinition copy() {
TimelineDefinition def = new TimelineDefinition(type);
def.title = title;
def.listId = listId;
def.listTitle = listTitle;
def.hashtagName = hashtagName;
def.icon = icon == null ? null : Icon.values()[icon.ordinal()];
return def;
}
public Bundle populateArguments(Bundle args) {
if (type == TimelineType.LIST) {
args.putString("listTitle", title);
args.putString("listID", listId);
} else if (type == TimelineType.HASHTAG) {
args.putString("hashtag", hashtagName);
}
return args;
}
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG }
public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
STAR(R.drawable.ic_fluent_star_24_regular, R.string.sk_icon_star),
PEOPLE(R.drawable.ic_fluent_people_24_regular, R.string.sk_icon_people),
CITY(R.drawable.ic_fluent_city_24_regular, R.string.sk_icon_city),
IMAGE(R.drawable.ic_fluent_image_24_regular, R.string.sk_icon_image),
NEWS(R.drawable.ic_fluent_news_24_regular, R.string.sk_icon_news),
COLOR_PALETTE(R.drawable.ic_fluent_color_24_regular, R.string.sk_icon_color_palette),
CAT(R.drawable.ic_fluent_animal_cat_24_regular, R.string.sk_icon_cat),
DOG(R.drawable.ic_fluent_animal_dog_24_regular, R.string.sk_icon_dog),
RABBIT(R.drawable.ic_fluent_animal_rabbit_24_regular, R.string.sk_icon_rabbit),
TURTLE(R.drawable.ic_fluent_animal_turtle_24_regular, R.string.sk_icon_turtle),
ACADEMIC_CAP(R.drawable.ic_fluent_hat_graduation_24_regular, R.string.sk_icon_academic_cap),
BOT(R.drawable.ic_fluent_bot_24_regular, R.string.sk_icon_bot),
IMPORTANT(R.drawable.ic_fluent_important_24_regular, R.string.sk_icon_important),
PIN(R.drawable.ic_fluent_pin_24_regular, R.string.sk_icon_pin),
SHIELD(R.drawable.ic_fluent_shield_24_regular, R.string.sk_icon_shield),
CHAT(R.drawable.ic_fluent_chat_multiple_24_regular, R.string.sk_icon_chat),
TAG(R.drawable.ic_fluent_tag_24_regular, R.string.sk_icon_tag),
TRAIN(R.drawable.ic_fluent_vehicle_subway_24_regular, R.string.sk_icon_train),
BICYCLE(R.drawable.ic_fluent_vehicle_bicycle_24_regular, R.string.sk_icon_bicycle),
MAP(R.drawable.ic_fluent_map_24_regular, R.string.sk_icon_map),
BACKPACK(R.drawable.ic_fluent_backpack_24_regular, R.string.sk_icon_backpack),
BRIEFCASE(R.drawable.ic_fluent_briefcase_24_regular, R.string.sk_icon_briefcase),
BOOK(R.drawable.ic_fluent_book_open_24_regular, R.string.sk_icon_book),
LANGUAGE(R.drawable.ic_fluent_local_language_24_regular, R.string.sk_icon_language),
WEATHER(R.drawable.ic_fluent_weather_rain_showers_day_24_regular, R.string.sk_icon_weather),
APERTURE(R.drawable.ic_fluent_scan_24_regular, R.string.sk_icon_aperture),
MUSIC(R.drawable.ic_fluent_music_note_2_24_regular, R.string.sk_icon_music),
LOCATION(R.drawable.ic_fluent_location_24_regular, R.string.sk_icon_location),
GLOBE(R.drawable.ic_fluent_globe_24_regular, R.string.sk_icon_globe),
MEGAPHONE(R.drawable.ic_fluent_megaphone_loud_24_regular, R.string.sk_icon_megaphone),
MICROPHONE(R.drawable.ic_fluent_mic_24_regular, R.string.sk_icon_microphone),
MICROSCOPE(R.drawable.ic_fluent_microscope_24_regular, R.string.sk_icon_microscope),
STETHOSCOPE(R.drawable.ic_fluent_stethoscope_24_regular, R.string.sk_icon_stethoscope),
KEYBOARD(R.drawable.ic_fluent_midi_24_regular, R.string.sk_icon_keyboard),
COFFEE(R.drawable.ic_fluent_drink_coffee_24_regular, R.string.sk_icon_coffee),
CLAPPER_BOARD(R.drawable.ic_fluent_movies_and_tv_24_regular, R.string.sk_icon_clapper_board),
LAUGH(R.drawable.ic_fluent_emoji_laugh_24_regular, R.string.sk_icon_laugh),
BALLOON(R.drawable.ic_fluent_balloon_24_regular, R.string.sk_icon_balloon),
PI(R.drawable.ic_fluent_pi_24_regular, R.string.sk_icon_pi),
MATH_FORMULA(R.drawable.ic_fluent_math_formula_24_regular, R.string.sk_icon_math_formula),
GAMES(R.drawable.ic_fluent_games_24_regular, R.string.sk_icon_games),
CODE(R.drawable.ic_fluent_code_24_regular, R.string.sk_icon_code),
BUG(R.drawable.ic_fluent_bug_24_regular, R.string.sk_icon_bug),
LIGHT_BULB(R.drawable.ic_fluent_lightbulb_24_regular, R.string.sk_icon_light_bulb),
FIRE(R.drawable.ic_fluent_fire_24_regular, R.string.sk_icon_fire),
LEAVES(R.drawable.ic_fluent_leaf_three_24_regular, R.string.sk_icon_leaves),
SPORT(R.drawable.ic_fluent_sport_24_regular, R.string.sk_icon_sport),
HEALTH(R.drawable.ic_fluent_heart_pulse_24_regular, R.string.sk_icon_health),
PIZZA(R.drawable.ic_fluent_food_pizza_24_regular, R.string.sk_icon_pizza),
GAVEL(R.drawable.ic_fluent_gavel_24_regular, R.string.sk_icon_gavel),
GAUGE(R.drawable.ic_fluent_gauge_24_regular, R.string.sk_icon_gauge),
HEADPHONES(R.drawable.ic_fluent_headphones_sound_wave_24_regular, R.string.sk_icon_headphones),
HUMAN(R.drawable.ic_fluent_accessibility_24_regular, R.string.sk_icon_human),
HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true),
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true);
public final int iconRes, nameRes;
public final boolean hidden;
Icon(@DrawableRes int iconRes, @StringRes int nameRes) {
this(iconRes, nameRes, false);
}
Icon(@DrawableRes int iconRes, @StringRes int nameRes, boolean hidden) {
this.iconRes = iconRes;
this.nameRes = nameRes;
this.hidden = hidden;
}
}
public static final TimelineDefinition HOME_TIMELINE = new TimelineDefinition(TimelineType.HOME);
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE.copy(),
LOCAL_TIMELINE.copy(),
FEDERATED_TIMELINE.copy(),
POSTS_TIMELINE.copy()
);
}

View File

@@ -11,8 +11,14 @@ import java.util.List;
import androidx.annotation.NonNull;
public class PhotoLayoutHelper{
public static final int MAX_WIDTH=1000;
public static final int MAX_HEIGHT=1910;
@NonNull
public static TiledLayoutResult processThumbs(int _maxW, int _maxH, List<Attachment> thumbs){
public static TiledLayoutResult processThumbs(List<Attachment> thumbs){
int _maxW=MAX_WIDTH;
int _maxH=MAX_HEIGHT;
TiledLayoutResult result=new TiledLayoutResult();
if(thumbs.size()==1){
Attachment att=thumbs.get(0);
@@ -45,13 +51,8 @@ public class PhotoLayoutHelper{
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
float maxW, maxH, marginW=0, marginH=0;
if(_maxW>0){
maxW=_maxW;
maxH=_maxH;
}else{
maxW=510;
maxH=510;
}
maxW=_maxW;
maxH=_maxH;
float maxRatio=maxW/maxH;

View File

@@ -96,9 +96,10 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
visibility.setImageResource(switch (s.visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
case LOCAL -> R.drawable.ic_fluent_eye_20_regular;
});
}

View File

@@ -137,7 +137,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
}
@@ -190,6 +190,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
String accountID = session.getID();
args.putString("account", accountID);
UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> {
if (status == null) return;
args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
@@ -239,8 +240,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_community_24_regular);
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_checkmark_24_regular);
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_open_24_regular);
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_closed_24_regular);
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
// e.g. post visibility is unlisted, but default is public

View File

@@ -1,42 +0,0 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.graphics.Outline;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class GifVStatusDisplayItem extends ImageStatusDisplayItem{
public GifVStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile);
request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000);
}
@Override
public Type getType(){
return Type.GIFV;
}
public static class Holder extends ImageStatusDisplayItem.Holder<GifVStatusDisplayItem>{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_gifv, parent);
View play=findViewById(R.id.play_button);
play.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
outline.setAlpha(.99f); // fixes shadow rendering
}
});
}
}
}

View File

@@ -9,7 +9,6 @@ import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
@@ -21,6 +20,8 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.StringRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
@@ -31,6 +32,7 @@ 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.ComposeFragment;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
@@ -42,6 +44,7 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
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;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -51,6 +54,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -135,7 +139,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView name, username, timestamp, extraText, separator;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator;
private final View collapseBtn;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, collapseBtnIcon;
private final PopupMenu optionsMenu;
private Relationship relationship;
private APIRequest<?> currentRelationshipRequest;
@@ -158,6 +163,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
visibility=findViewById(R.id.visibility);
deleteNotification=findViewById(R.id.delete_notification);
unreadIndicator=findViewById(R.id.unread_indicator);
collapseBtn=findViewById(R.id.collapse_btn);
collapseBtnIcon=findViewById(R.id.collapse_btn_icon);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(roundCornersOutline);
@@ -169,6 +176,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
fragment.removeNotification(item.notification);
}
}));
collapseBtn.setOnClickListener(l -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
@@ -180,7 +188,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
final Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("editStatus", Parcels.wrap(item.status));
if (id==R.id.delete_and_redraft) {
boolean redraft = id==R.id.delete_and_redraft;
if (redraft) {
args.putBoolean("redraftStatus", true);
if (item.parentFragment instanceof ThreadFragment thread && !thread.isItemEnabled(item.status.id)) {
// ("enabled" = clickable; opened status is not clickable)
@@ -188,7 +197,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putBoolean("navigateToStatus", true);
}
}
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
if(!redraft && TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text);
@@ -203,7 +212,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void onSuccess(GetStatusSourceText.Response result){
args.putString("sourceText", result.text);
args.putString("sourceSpoiler", result.spoilerText);
if (id==R.id.delete_and_redraft) {
if (redraft) {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}, true);
@@ -261,6 +270,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
}else if(id==R.id.bookmark){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
}else if(id==R.id.manage_user_lists){
final Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(item.parentFragment.getActivity(), ListTimelinesFragment.class, args);
}
return true;
});
@@ -291,7 +306,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
}
else if ((item.status==null || item.status.editedAt==null) && item.createdAt != null)
else if ((!item.inset || item.status==null || item.status.editedAt==null) && item.createdAt != null)
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
else if (item.status != null && item.status.editedAt != null)
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
@@ -310,12 +325,15 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
if(TextUtils.isEmpty(item.extraText)){
extraText.setVisibility(View.GONE);
if (item.status != null) {
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, item.status.visibility, item.status.localOnly);
}
}else{
extraText.setVisibility(View.VISIBLE);
extraText.setText(item.extraText);
}
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
more.setVisibility(item.inset || (item.notification != null && item.notification.report != null)
? View.GONE : View.VISIBLE);
avatar.setClickable(!item.inset);
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
if(currentRelationshipRequest!=null){
@@ -343,6 +361,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void onSuccess(Object o) {
item.consumeReadAnnouncement.accept(item.announcement.id);
item.announcement.read = true;
if (item.parentFragment.getActivity() == null) return;
rebind();
}
@@ -360,6 +379,17 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
more.setContentDescription(desc);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc);
if (item.status == null || !item.status.textExpandable) {
collapseBtn.setVisibility(View.GONE);
} else {
String collapseText = item.parentFragment.getString(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
collapseBtn.setVisibility(item.status.textExpandable ? View.VISIBLE : View.GONE);
collapseBtn.setContentDescription(collapseText);
if (GlobalUserPreferences.reduceMotion) collapseBtnIcon.setScaleY(item.status.textExpanded ? -1 : 1);
else collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
}
}
@Override
@@ -415,6 +445,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void updateOptionsMenu(){
if (item.parentFragment.getActivity() == null) return;
if (item.announcement != null) return;
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
Menu menu=optionsMenu.getMenu();
@@ -424,7 +455,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
if (hasMultipleAccounts && accountsMenu != null) {
openWithAccounts.setVisible(true);
accountsMenu.clear();
populateAccountsMenu(accountsMenu);
UiUtils.populateAccountsMenu(item.accountID, accountsMenu, s-> UiUtils.openURL(
item.parentFragment.getActivity(), s.getID(), item.status.url, false
));
} else if (openWithAccounts != null) {
openWithAccounts.setVisible(false);
}
@@ -445,6 +478,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
MenuItem block=menu.findItem(R.id.block);
MenuItem report=menu.findItem(R.id.report);
MenuItem follow=menu.findItem(R.id.follow);
MenuItem manageUserLists = menu.findItem(R.id.manage_user_lists);
MenuItem bookmark=menu.findItem(R.id.bookmark);
bookmark.setVisible(false);
/* disabled in megalodon: add/remove bookmark is already available through status footer
@@ -461,6 +495,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
report.setVisible(false);
follow.setVisible(false);
blockDomain.setVisible(false);
manageUserLists.setVisible(false);
}else{
mute.setVisible(true);
block.setVisible(true);
@@ -481,6 +516,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
boolean following = relationship!=null && relationship.following;
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, account.getShortUsername()));
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
manageUserLists.setVisible(relationship != null && relationship.following);
manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow);
}
}

View File

@@ -1,105 +0,0 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout;
import androidx.annotation.LayoutRes;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
public final int index;
public final int totalPhotos;
protected Attachment attachment;
protected ImageLoaderRequest request;
public final Status status;
public final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
public final PhotoLayoutHelper.TiledLayoutResult.Tile thisTile;
public int horizontalInset;
public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment);
this.attachment=photo;
this.status=status;
this.index=index;
this.totalPhotos=totalPhotos;
this.tiledLayout=tiledLayout;
this.thisTile=thisTile;
}
@Override
public int getImageCount(){
return 1;
}
@Override
public ImageLoaderRequest getImageRequest(int index){
return request;
}
public static abstract class Holder<T extends ImageStatusDisplayItem> extends StatusDisplayItem.Holder<T> implements ImageLoaderViewHolder{
public final ImageView photo;
private ImageAttachmentFrameLayout layout;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
super(activity, layout, parent);
photo=findViewById(R.id.photo);
photo.setOnClickListener(this::onViewClick);
this.layout=(ImageAttachmentFrameLayout)itemView;
}
@Override
public void onBind(ImageStatusDisplayItem item){
layout.setLayout(item.tiledLayout, item.thisTile, item.horizontalInset);
crossfadeDrawable.setSize(item.attachment.getWidth(), item.attachment.getHeight());
crossfadeDrawable.setBlurhashDrawable(item.attachment.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f);
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
photo.setContentDescription(TextUtils.isEmpty(item.attachment.description) ? item.parentFragment.getString(R.string.media_no_description) : item.attachment.description);
didClear=false;
}
@Override
public void setImage(int index, Drawable drawable){
crossfadeDrawable.setImageDrawable(drawable);
if(didClear && item.status.spoilerRevealed)
crossfadeDrawable.animateAlpha(0f);
}
@Override
public void clearImage(int index){
crossfadeDrawable.setCrossfadeAlpha(1f);
crossfadeDrawable.setImageDrawable(null);
didClear=true;
}
private void onViewClick(View v){
if(!item.status.spoilerRevealed){
item.parentFragment.onRevealSpoilerClick(this);
}else if(item.parentFragment instanceof PhotoViewerHost){
Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status;
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment));
}
}
public void setRevealed(boolean revealed){
crossfadeDrawable.animateAlpha(revealed ? 0f : 1f);
}
}
}

View File

@@ -0,0 +1,311 @@
package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.ui.utils.MediaAttachmentViewController.altWrapPadding;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageButton;
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.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
import java.util.List;
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;
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private static final String TAG="MediaGridDisplayItem";
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments;
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
super(parentID, parentFragment);
this.tiledLayout=tiledLayout;
this.viewPool=parentFragment.getAttachmentViewsPool();
this.attachments=attachments;
this.status=status;
for(Attachment att:attachments){
requests.add(new UrlImageLoaderRequest(switch(att.type){
case IMAGE -> att.url;
case VIDEO, GIFV -> att.previewUrl;
default -> throw new IllegalStateException("Unexpected value: "+att.type);
}, 1000, 1000));
}
}
@Override
public Type getType(){
return Type.MEDIA_GRID;
}
@Override
public int getImageCount(){
return requests.size();
}
@Override
public ImageLoaderRequest getImageRequest(int index){
return requests.get(index);
}
public enum GridItemType{
PHOTO,
VIDEO,
GIFV
}
public static class Holder extends StatusDisplayItem.Holder<MediaGridStatusDisplayItem> implements ImageLoaderViewHolder{
private final FrameLayout wrapper;
private final MediaGridLayout layout;
private final View.OnClickListener clickListener=this::onViewClick, altTextClickListener=this::onAltTextClick;
private final ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
private final FrameLayout altTextWrapper;
private final TextView altTextButton;
private final ImageView noAltTextButton;
private final View altTextScroller;
private final ImageButton altTextClose;
private final TextView altText, noAltText;
private int altTextIndex=-1;
private Animator altTextAnimator;
public Holder(Activity activity, ViewGroup parent){
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
wrapper=(FrameLayout)itemView;
layout=new MediaGridLayout(activity);
wrapper.addView(layout);
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, wrapper);
altTextWrapper=findViewById(R.id.alt_text_wrapper);
altTextButton=findViewById(R.id.alt_button);
noAltTextButton=findViewById(R.id.no_alt_button);
altTextScroller=findViewById(R.id.alt_text_scroller);
altTextClose=findViewById(R.id.alt_text_close);
altText=findViewById(R.id.alt_text);
noAltText=findViewById(R.id.no_alt_text);
altTextClose.setOnClickListener(this::onAltTextCloseClick);
}
@Override
public void onBind(MediaGridStatusDisplayItem item){
if(altTextAnimator!=null)
altTextAnimator.cancel();
layout.setTiledLayout(item.tiledLayout);
for(MediaAttachmentViewController c:controllers){
item.viewPool.reuse(c.type, c);
}
layout.removeAllViews();
controllers.clear();
int i=0;
for(Attachment att:item.attachments){
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
case IMAGE -> GridItemType.PHOTO;
case VIDEO -> GridItemType.VIDEO;
case GIFV -> GridItemType.GIFV;
default -> throw new IllegalStateException("Unexpected value: "+att.type);
});
if(c.view.getLayoutParams()==null)
c.view.setLayoutParams(new MediaGridLayout.LayoutParams(item.tiledLayout.tiles[i]));
else
((MediaGridLayout.LayoutParams) c.view.getLayoutParams()).tile=item.tiledLayout.tiles[i];
layout.addView(c.view);
c.view.setOnClickListener(clickListener);
c.view.setTag(i);
if(c.btnsWrap!=null){
c.btnsWrap.setOnClickListener(altTextClickListener);
c.btnsWrap.setTag(i);
c.btnsWrap.setAlpha(1f);
}
controllers.add(c);
c.bind(att, item.status);
i++;
}
altTextButton.setVisibility(View.VISIBLE);
noAltTextButton.setVisibility(View.VISIBLE);
altTextWrapper.setVisibility(View.GONE);
altTextIndex=-1;
}
@Override
public void setImage(int index, Drawable drawable){
controllers.get(index).setImage(drawable);
}
@Override
public void clearImage(int index){
controllers.get(index).clearImage();
}
private void onViewClick(View v){
int index=(Integer)v.getTag();
if(!item.status.spoilerRevealed){
item.parentFragment.onRevealSpoilerClick(this);
}else if(item.parentFragment instanceof PhotoViewerHost){
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, index, this);
}
}
private void onAltTextClick(View v){
if(altTextAnimator!=null)
altTextAnimator.cancel();
v.setVisibility(View.INVISIBLE);
int index=(Integer)v.getTag();
altTextIndex=index;
Attachment att=item.attachments.get(index);
boolean hasAltText = !TextUtils.isEmpty(att.description);
altTextButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE);
noAltTextButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE);
altText.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE);
noAltText.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE);
altText.setText(att.description);
altTextWrapper.setVisibility(View.VISIBLE);
altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_overlay : R.drawable.bg_image_no_alt_overlay);
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
int[] loc={0, 0};
v.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
wrapper.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0], altTextWrapper.getLeft()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1], altTextWrapper.getTop()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-altWrapPadding[2], altTextWrapper.getRight()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-altWrapPadding[3], altTextWrapper.getBottom()));
for(Animator a:anims)
a.setDuration(300);
for(MediaAttachmentViewController c:controllers){
if(c.btnsWrap!=null && c.btnsWrap!=v){
anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1, 0).setDuration(150));
}
}
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
altTextAnimator=null;
for(MediaAttachmentViewController c:controllers){
if(c.btnsWrap!=null){
c.btnsWrap.setVisibility(View.INVISIBLE);
}
}
}
});
altTextAnimator=set;
set.start();
return true;
}
});
}
private void onAltTextCloseClick(View v){
if(altTextAnimator!=null)
altTextAnimator.cancel();
View btn=controllers.get(altTextIndex).btnsWrap;
for(MediaAttachmentViewController c:controllers){
if(c.btnsWrap!=null && c.btnsWrap!=btn) {
c.btnsWrap.setVisibility(View.VISIBLE);
}
}
int[] loc={0, 0};
btn.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
wrapper.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-altWrapPadding[2]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-altWrapPadding[3]));
for(Animator a:anims)
a.setDuration(300);
for(MediaAttachmentViewController c:controllers){
// if(c.btnsWrap!=null && c.btnsWrap!=btn){
anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1).setDuration(150));
// }
}
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
altTextAnimator=null;
altTextWrapper.setVisibility(View.GONE);
btn.setVisibility(View.VISIBLE);
}
});
altTextAnimator=set;
set.start();
}
public void setRevealed(boolean revealed){
for(MediaAttachmentViewController c:controllers){
c.setRevealed(revealed);
}
}
public MediaAttachmentViewController getViewController(int index){
return controllers.get(index);
}
public void setClipChildren(boolean clip){
layout.setClipChildren(clip);
wrapper.setClipChildren(clip);
}
}
}

View File

@@ -1,31 +0,0 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment, photo, status, index, totalPhotos, tiledLayout, thisTile);
request=new UrlImageLoaderRequest(photo.url, 1000, 1000);
}
@Override
public Type getType(){
return Type.PHOTO;
}
public static class Holder extends ImageStatusDisplayItem.Holder<PhotoStatusDisplayItem>{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_photo, parent);
}
}
}

View File

@@ -39,10 +39,11 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(PollFooterStatusDisplayItem item){
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_voters, item.poll.votersCount, item.poll.votersCount);
String sep=item.parentFragment.getString(R.string.sk_separator);
if(item.poll.expiresAt!=null && !item.poll.isExpired()){
text+=" · "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
text+=" "+sep+" "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
}else if(item.poll.isExpired()){
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
text+=" "+sep+" "+item.parentFragment.getString(R.string.poll_closed);
}
this.text.setText(text);
button.setVisibility(item.poll.isExpired() || item.poll.voted || (!item.poll.multiple && !GlobalUserPreferences.voteButtonForSingleChoice) ? View.GONE : View.VISIBLE);

View File

@@ -35,8 +35,9 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
emojiHelper.setText(text);
showResults=poll.isExpired() || poll.voted;
if(showResults && option.votesCount!=null && poll.votersCount>0){
votesFraction=(float)option.votesCount/(float)poll.votersCount;
int total=poll.votersCount>0 ? poll.votersCount : poll.votesCount;
if(showResults && option.votesCount!=null && total>0){
votesFraction=(float)option.votesCount/(float)total;
int mostVotedCount=0;
for(Poll.Option opt:poll.options)
mostVotedCount=Math.max(mostVotedCount, opt.votesCount);

View File

@@ -27,6 +27,7 @@ import androidx.annotation.Nullable;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
@@ -37,6 +38,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private View.OnClickListener handleClick;
private boolean isLastLine = true;
private int lineNo = 0;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick){
super(parentID, parentFragment);
@@ -51,12 +54,20 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
updateVisibility(visibility);
}
public void setIsLastLine(boolean isLastLine) {
this.isLastLine = isLastLine;
}
public void setLineNo(int lineNo) {
this.lineNo = lineNo;
}
public void updateVisibility(StatusPrivacy visibility) {
this.visibility = visibility;
this.iconEnd = visibility != null ? switch (visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
default -> 0;
} : 0;
}
@@ -78,18 +89,21 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView text;
private final View frame;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_reblog_or_reply_line, parent);
text=findViewById(R.id.text);
frame=findViewById(R.id.frame);
}
@Override
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
text.setEnabled(!item.inset);
text.setClickable(!item.inset);
text.setOnClickListener(item.handleClick);
text.setEnabled(!item.inset && item.handleClick != null);
text.setClickable(!item.inset && item.handleClick != null);
Context ctx = itemView.getContext();
int visibilityText = item.visibility != null ? switch (item.visibility) {
case PUBLIC -> R.string.visibility_public;
@@ -100,6 +114,10 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")");
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.bottomMargin = V.dp(item.isLastLine ? -12 : -18);
params.leftMargin = V.dp(13) * item.lineNo;
frame.setLayoutParams(params);
}
@Override

View File

@@ -10,23 +10,27 @@ import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTabFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
@@ -60,10 +64,7 @@ public abstract class StatusDisplayItem{
case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent);
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent);
case GIFV -> new GifVStatusDisplayItem.Holder(activity, parent);
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
case VIDEO -> new VideoStatusDisplayItem.Holder(activity, parent);
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
@@ -73,52 +74,100 @@ public abstract class StatusDisplayItem{
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
};
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream()
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList());
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
if(!statusForContent.filterRevealed){
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
}
if(status.reblog!=null){
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, null, i->{
} else if (!(status.tags.isEmpty() ||
fragment instanceof HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment
) && fragment.getParentFragment() instanceof HomeTabFragment home) {
home.getHashtags().stream()
.filter(followed -> status.tags.stream()
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
.findAny()
// post contains a hashtag the user is following
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, hashtag.name, List.of(),
R.drawable.ic_fluent_number_symbol_20_filled, null,
i -> {
args.putString("hashtag", hashtag.name);
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
}
)));
}
if(statusForContent.inReplyToAccountId!=null){
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
View.OnClickListener handleClick = account == null ? null : i -> {
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
};
String text = account != null ? fragment.getString(R.string.in_reply_to, account.displayName) : fragment.getString(R.string.sk_in_reply);
items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20_filled, null, handleClick
));
}
int l = 0;
ReblogOrReplyLineStatusDisplayItem lastLine = null;
for (StatusDisplayItem item : items) {
if (item instanceof ReblogOrReplyLineStatusDisplayItem line) {
line.setLineNo(l);
line.setIsLastLine(false);
lastLine = line;
l++;
}
}
if (lastLine != null) lastLine.setIsLastLine(true);
HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus));
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
else
header.needBottomPadding=true;
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
int photoIndex=0;
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(1000, 1910, imageAttachments);
for(Attachment attachment:imageAttachments){
if(attachment.type==Attachment.Type.IMAGE){
items.add(new PhotoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex]));
}else if(attachment.type==Attachment.Type.GIFV){
items.add(new GifVStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex]));
}else if(attachment.type==Attachment.Type.VIDEO){
items.add(new VideoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex]));
}else{
throw new IllegalStateException("This isn't supposed to happen, type is "+attachment.type);
}
photoIndex++;
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
}
for(Attachment att:statusForContent.mediaAttachments){
if(att.type==Attachment.Type.AUDIO){
@@ -141,6 +190,13 @@ public abstract class StatusDisplayItem{
item.inset=inset;
item.index=i++;
}
if (!statusForContent.filterRevealed) {
return new ArrayList<>(List.of(
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
));
}
return items;
}
@@ -155,9 +211,6 @@ public abstract class StatusDisplayItem{
HEADER,
REBLOG_OR_REPLY_LINE,
TEXT,
PHOTO,
VIDEO,
GIFV,
AUDIO,
POLL_OPTION,
POLL_FOOTER,
@@ -167,7 +220,9 @@ public abstract class StatusDisplayItem{
ACCOUNT,
HASHTAG,
GAP,
EXTENDED_FOOTER
EXTENDED_FOOTER,
MEDIA_GRID,
WARNING
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
@@ -176,7 +231,7 @@ public abstract class StatusDisplayItem{
}
public Holder(Context context, int layout, ViewGroup parent){
super(context, layout, parent);
super(context, layout, parent);
}
public String getItemID(){

View File

@@ -3,14 +3,17 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Button;
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;
@@ -20,13 +23,15 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.TranslatedStatus;
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.regex.Pattern;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -42,14 +47,17 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
private CharSequence parsedSpoilerText;
public boolean textSelectable;
public final Status status;
public boolean disableTranslate;
public boolean translated = false;
public TranslatedStatus translation = null;
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){
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
super(parentID, parentFragment);
this.text=text;
this.status=status;
this.disableTranslate=disableTranslate;
emojiHelper.setText(text);
if(!TextUtils.isEmpty(status.spoilerText)){
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
@@ -81,10 +89,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
private final LinkedTextView text;
private final LinearLayout spoilerHeader;
private final TextView spoilerTitle, spoilerTitleInline, translateInfo;
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress;
private final TextView spoilerTitle, spoilerTitleInline, translateInfo, readMore;
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress, spaceBelowText;
private final int backgroundColor, borderColor;
private final Button translateButton;
private final ScrollView textScrollView;
private final float textMaxHeight, textCollapsedHeight;
private final LinearLayout.LayoutParams collapseParams, wrapParams;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_text, parent);
@@ -103,6 +115,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight);
borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted);
textScrollView=findViewById(R.id.text_scroll_view);
readMore=findViewById(R.id.read_more);
spaceBelowText=findViewById(R.id.space_below_text);
textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height);
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()));
}
@Override
@@ -111,6 +131,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
? HtmlParser.parse(item.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));
}
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
spoilerTitleInline.setBackgroundColor(item.inset ? 0 : backgroundColor);
@@ -139,19 +162,34 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
}
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
boolean translateEnabled = instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
boolean translateEnabled = !item.disableTranslate && instanceInfo != null &&
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
instanceInfo.v2.configuration.translation.enabled;
translateWrap.setVisibility(
(!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) &&
boolean isBottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find();
boolean translateVisible = (isBottomText || (
translateEnabled &&
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
item.status.language != null &&
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))
? View.VISIBLE : View.GONE);
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
item.status.language != null &&
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))))
&& (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable);
translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post);
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, item.translation.provider) : "");
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, isBottomText ? "bottom-java" : item.translation.provider) : "");
translateButton.setOnClickListener(v->{
if (item.translation == null) {
if (isBottomText) {
try {
item.translation = new TranslatedStatus();
item.translation.content = new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN);
item.translated = true;
} catch (TranslationError err) {
item.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();
@@ -160,6 +198,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void onSuccess(TranslatedStatus translatedStatus) {
item.translation = translatedStatus;
item.translated = true;
if (item.parentFragment.getActivity() == null) return;
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
@@ -179,6 +218,26 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
rebind();
}
});
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
if (!GlobalUserPreferences.collapseLongPosts) {
textScrollView.setLayoutParams(wrapParams);
readMore.setVisibility(View.GONE);
}
if (GlobalUserPreferences.collapseLongPosts) text.post(() -> {
boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
boolean inTimeline = !item.textSelectable;
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
boolean expandable = inTimeline && tooBig && !hasSpoiler;
item.parentFragment.onEnableExpandable(this, expandable);
});
readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE);
textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams);
if (item.status.textExpandable && !translateVisible) spaceBelowText.setVisibility(View.VISIBLE);
}
@Override

View File

@@ -1,42 +0,0 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.graphics.Outline;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class VideoStatusDisplayItem extends ImageStatusDisplayItem{
public VideoStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile);
request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000);
}
@Override
public Type getType(){
return Type.VIDEO;
}
public static class Holder extends ImageStatusDisplayItem.Holder<VideoStatusDisplayItem>{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_video, parent);
View play=findViewById(R.id.play_button);
play.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
outline.setAlpha(.99f); // fixes shadow rendering
}
});
}
}
}

View File

@@ -0,0 +1,52 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
public final Status status;
public List<StatusDisplayItem> filteredItems;
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems){
super(parentID, parentFragment);
this.status=status;
this.filteredItems = filteredItems;
}
@Override
public Type getType(){
return Type.WARNING;
}
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);
}
@Override
public void onBind(WarningFilteredStatusDisplayItem item) {
filteredItems = item.filteredItems;
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.status.filtered.get(item.status.filtered.size() -1).filter.title));
}
@Override
public void onClick() {
item.parentFragment.onWarningClick(this);
}
}
}

View File

@@ -1,7 +1,8 @@
package org.joinmastodon.android.ui.photoviewer;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
public interface PhotoViewerHost{
void openPhotoViewer(String parentID, Status status, int attachmentIndex);
void openPhotoViewer(String parentID, Status status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder);
}

View File

@@ -602,7 +602,7 @@ public class TabLayout extends HorizontalScrollView {
* <p>If the tab indicator color is not {@code Color.TRANSPARENT}, the indicator will be wrapped
* and tinted right before it is drawn by {@link SlidingTabIndicator#draw(Canvas)}. If you'd like
* the inherent color or the tinted color of a custom drawable to be used, make sure this color is
* set to {@code Color.TRANSPARENT} to avoid your color/tint being overriden.
* set to {@code Color.TRANSPARENT} to avoid your color/tint being overridden.
*
* @param color color to use for the indicator
* @attr ref com.google.android.material.R.styleable#TabLayout_tabIndicatorColor

View File

@@ -8,24 +8,25 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout;
import android.text.Spanned;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V;
public class ClickableLinksDelegate {
private Paint hlPaint;
private final Paint hlPaint;
private Path hlPath;
private LinkSpan selectedSpan;
private TextView view;
private final TextView view;
private final Runnable longClickRunnable = () -> {
if (selectedSpan != null) selectedSpan.onLongClick(view);
};
private final GestureDetector gestureDetector;
public ClickableLinksDelegate(TextView view) {
this.view=view;
@@ -33,11 +34,45 @@ public class ClickableLinksDelegate {
hlPaint.setAntiAlias(true);
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
}
public boolean onTouch(MotionEvent event) {
long eventDuration = event.getEventTime() - event.getDownTime();
if(event.getAction()==MotionEvent.ACTION_DOWN){
if(event.getAction()==MotionEvent.ACTION_CANCEL){
// the gestureDetector does not provide a callback for CANCEL, therefore:
// remove background color of view before passing event to gestureDetector
resetAndInvalidate();
}
return gestureDetector.onTouchEvent(event);
}
/**
* remove highlighting from span and let the system redraw the view
*/
private void resetAndInvalidate() {
hlPath=null;
selectedSpan=null;
view.invalidate();
}
public void onDraw(Canvas canvas){
if(hlPath!=null){
canvas.save();
canvas.translate(0, view.getPaddingTop());
canvas.drawPath(hlPath, hlPaint);
canvas.restore();
}
}
/**
* GestureListener for spans that represent URLs.
* onDown: on start of touch event, set highlighting
* onSingleTapUp: when there was a (short) tap, call onClick and reset highlighting
* onLongPress: copy URL to clipboard, let user know, reset highlighting
*/
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent event) {
int line=-1;
Rect rect=new Rect();
Layout l=view.getLayout();
@@ -52,8 +87,7 @@ public class ClickableLinksDelegate {
return false;
}
CharSequence text=view.getText();
if(text instanceof Spanned){
Spanned s=(Spanned)text;
if(text instanceof Spanned s){
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
if(spans.length>0){
for(LinkSpan span:spans){
@@ -70,7 +104,6 @@ public class ClickableLinksDelegate {
}
hlPath=new Path();
selectedSpan=span;
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
@@ -96,35 +129,26 @@ public class ClickableLinksDelegate {
}
}
}
return super.onDown(event);
}
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
if (eventDuration <= ViewConfiguration.getLongPressTimeout()) {
@Override
public boolean onSingleTapUp(@NonNull MotionEvent event) {
if(selectedSpan!=null){
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
resetAndInvalidate();
return true;
}
view.removeCallbacks(longClickRunnable);
hlPath=null;
selectedSpan=null;
view.invalidate();
return false;
}
if(event.getAction()==MotionEvent.ACTION_CANCEL){
hlPath=null;
selectedSpan=null;
view.removeCallbacks(longClickRunnable);
view.invalidate();
return false;
}
return false;
}
public void onDraw(Canvas canvas){
if(hlPath!=null){
canvas.save();
canvas.translate(0, view.getPaddingTop());
canvas.drawPath(hlPath, hlPaint);
canvas.restore();
}
}
@Override
public void onLongPress(@NonNull MotionEvent event) {
if (selectedSpan == null) return;
UiUtils.copyText(view, selectedSpan.getType() == LinkSpan.Type.URL ? selectedSpan.getLink() : selectedSpan.getText());
//reset view
resetAndInvalidate();
}
}
}

View File

@@ -16,6 +16,10 @@ public class LinkSpan extends CharacterStyle {
private String accountID;
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){
this.listener=listener;
this.link=link;
@@ -38,17 +42,18 @@ public class LinkSpan extends CharacterStyle {
case URL -> UiUtils.openURL(context, accountID, link);
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link, null);
case CUSTOM -> listener.onLinkClick(this);
}
}
public void onLongClick(View view) {
UiUtils.copyText(view, getType() == Type.URL ? link : text);
}
public String getLink(){
return link;
}
public String getText() {
return text;
}
public Type getType(){
return type;
}
@@ -64,6 +69,7 @@ public class LinkSpan extends CharacterStyle {
public enum Type{
URL,
MENTION,
HASHTAG
HASHTAG,
CUSTOM
}
}

View File

@@ -37,6 +37,7 @@ public class DiscoverInfoBannerHelper{
case TRENDING_LINKS -> R.string.trending_links_info_banner;
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
});
}
}
@@ -61,6 +62,7 @@ public class DiscoverInfoBannerHelper{
TRENDING_LINKS,
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POST_NOTIFICATIONS,
// ACCOUNTS
}
}

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