Compare commits

...

338 Commits

Author SHA1 Message Date
sk
99f0817bdb generalize filtering logic 2023-05-26 02:07:50 +02:00
sk
220cd35d82 fix context not checked for warnings
closes sk22#518
2023-05-25 21:25:09 +02:00
sk
07f4ef1697 remove unused imports 2023-05-25 21:22:27 +02:00
sk
f20732ddc2 Merge remote-tracking branch 'upstream/master' 2023-05-25 20:27:23 +02:00
sk
b1e0dc5843 show compose button when switching tab
closes sk22#506
2023-05-25 20:26:59 +02:00
Gregory K
285eb25706 Merge pull request #584 from sk22/fix-restored-tab-selection
Fix wrong tab being selected on restore
2023-05-25 21:00:12 +03:00
sk
ec556511e6 Merge branch 'fix-restored-tab-selection' 2023-05-25 19:57:34 +02:00
sk
85c3d9f65f fix wrong tab being selected on restore 2023-05-25 19:51:45 +02:00
sk
a7ebadf269 increase read timeout
re: sk22#392
2023-05-25 15:11:40 +02:00
sk
94c09d46c2 fix contentType being a required field
re: sk22#516
2023-05-25 14:53:26 +02:00
sk
f6f08d176c change user-agent string 2023-05-23 10:26:48 +02:00
sk
66cdd63496 use fedinuke block list
closes sk22#511
2023-05-22 18:04:37 +02:00
Jacoco
8b502b605c Alternative content types (#516)
* Akkoma content types

* Default content type preference

* per-account content types, compatible with glitch

* disable content types by default, change icon

* persist content type to state

* update string

* fall back to plain text if formatting enabled

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-05-22 17:56:50 +02:00
FineFindus
2c0ec28803 Panic responder (#512)
* feat: add panic responder

* refactor: logOut before removing session

* fix(panic): close app after logOut to avoid crash

* build: reset gradle.properties
2023-05-20 13:20:25 +02:00
Jacoco
a9ab9cb249 Fix crashes on Calckey and GoToSocial (#515)
* Fix crashes on calckey and gts

* Use url if previewUrl is null
2023-05-14 23:26:03 +02:00
sk
961c69b525 Merge remote-tracking branch 'upstream/master' 2023-05-13 15:15:00 +02:00
sk
c70f393559 account card layout adjustments 2023-05-13 15:13:43 +02:00
sk
9abdc174f4 guarantee space for display name in header
closes sk22#513
2023-05-13 14:07:44 +02:00
LucasGGamerM
2e5bfa1d9c fix: NPE when instance is null and attempts to get new notifications
For some weird reason, someone saw a pixelfed instance return null as the instance, causing a crash on the updateNotificationsBadge method. This reminds me of why java is such a shit language
2023-05-13 13:37:19 +02:00
Grishka
33cbd85e19 Bump version 2023-05-11 05:42:40 +03:00
Grishka
8cb1f3f387 Merge branch 'l10n_master' 2023-05-11 05:40:54 +03:00
Eugen Rochko
3f0c6fcec5 New translations strings.xml (Icelandic) 2023-05-10 13:59:12 +02:00
Eugen Rochko
797cf893da New translations strings.xml (Italian) 2023-05-08 15:58:22 +02:00
Eugen Rochko
a3564b70e1 New translations strings.xml (Ukrainian) 2023-05-08 07:52:51 +02:00
Eugen Rochko
43004307b8 New translations strings.xml (Ukrainian) 2023-05-08 06:34:46 +02:00
Eugen Rochko
acd1e4ced3 New translations full_description.txt (Scottish Gaelic) 2023-05-06 07:17:18 +02:00
Eugen Rochko
6717070f93 New translations strings.xml (Scottish Gaelic) 2023-05-06 07:17:17 +02:00
Eugen Rochko
387499ae49 New translations strings.xml (Vietnamese) 2023-05-05 08:49:53 +02:00
Eugen Rochko
8ab140c55d New translations strings.xml (Japanese) 2023-05-04 22:50:38 +02:00
Eugen Rochko
914abb95dd New translations strings.xml (Japanese) 2023-05-04 21:34:45 +02:00
Eugen Rochko
5360c0f0f7 New translations strings.xml (Greek) 2023-05-04 01:44:50 +02:00
Eugen Rochko
243d803b51 New translations strings.xml (Turkish) 2023-05-03 23:31:14 +02:00
Eugen Rochko
b343fe3835 New translations strings.xml (Turkish) 2023-05-03 22:27:36 +02:00
Eugen Rochko
3c42c1120f New translations strings.xml (Japanese) 2023-05-03 17:42:48 +02:00
Eugen Rochko
ad840dcef6 New translations strings.xml (Japanese) 2023-05-03 16:47:22 +02:00
Eugen Rochko
f73072d95e New translations strings.xml (Spanish) 2023-05-03 01:08:32 +02:00
Eugen Rochko
95cb9b5079 New translations strings.xml (Thai) 2023-05-02 21:12:14 +02:00
Eugen Rochko
c6684d3c9b New translations strings.xml (Thai) 2023-05-02 19:41:48 +02:00
Eugen Rochko
5c5989d8c0 New translations strings.xml (Chinese Traditional) 2023-05-02 18:13:38 +02:00
Eugen Rochko
60e92d30b0 New translations strings.xml (Italian) 2023-05-02 14:58:27 +02:00
Eugen Rochko
8bf8e3f86b New translations strings.xml (Slovenian) 2023-05-02 00:17:51 +02:00
Eugen Rochko
891ee2d06b New translations strings.xml (Indonesian) 2023-05-01 21:04:03 +02:00
Eugen Rochko
b450bc7ae8 New translations strings.xml (Icelandic) 2023-05-01 21:04:02 +02:00
Eugen Rochko
4ca1e0d5db New translations strings.xml (Vietnamese) 2023-05-01 21:04:01 +02:00
Eugen Rochko
859213dd9e New translations strings.xml (Chinese Simplified) 2023-05-01 21:04:01 +02:00
Eugen Rochko
ad2857791d New translations strings.xml (Turkish) 2023-05-01 21:03:59 +02:00
Eugen Rochko
497827f2e2 New translations strings.xml (Slovenian) 2023-05-01 21:03:57 +02:00
Eugen Rochko
967e333022 New translations strings.xml (Japanese) 2023-05-01 21:03:54 +02:00
Eugen Rochko
8df1406006 New translations strings.xml (Dutch) 2023-05-01 21:03:46 +02:00
Eugen Rochko
4af42fafdc New translations strings.xml (Russian) 2023-05-01 21:03:45 +02:00
Eugen Rochko
a9e6a452c1 New translations strings.xml (Galician) 2023-05-01 21:03:43 +02:00
Eugen Rochko
a4a4632397 New translations strings.xml (Chinese Traditional) 2023-05-01 21:03:42 +02:00
Eugen Rochko
421f39e414 New translations strings.xml (Italian) 2023-05-01 21:03:41 +02:00
Eugen Rochko
f8121e2dc4 New translations strings.xml (German) 2023-05-01 21:03:41 +02:00
Eugen Rochko
b1784fc51c New translations strings.xml (Danish) 2023-05-01 21:03:40 +02:00
Eugen Rochko
96db0d7de7 New translations strings.xml (Greek) 2023-05-01 21:03:39 +02:00
Eugen Rochko
3837ed9cb1 New translations strings.xml (Thai) 2023-05-01 21:03:38 +02:00
Eugen Rochko
2be789a43c New translations strings.xml (Spanish) 2023-05-01 21:03:37 +02:00
Grishka
fd8d96169a Update string 2023-05-01 21:38:16 +03:00
Eugen Rochko
1562dc32c1 New translations strings.xml (Dutch) 2023-05-01 13:44:56 +02:00
Eugen Rochko
38f377ca09 New translations full_description.txt (Armenian) 2023-04-28 20:21:00 +02:00
Eugen Rochko
cc28bba884 New translations strings.xml (Armenian) 2023-04-28 20:20:59 +02:00
Eugen Rochko
beb3081918 New translations strings.xml (Armenian) 2023-04-28 19:06:20 +02:00
Eugen Rochko
1b3c9106b5 New translations strings.xml (Russian) 2023-04-28 09:05:19 +02:00
Eugen Rochko
385b91761b New translations strings.xml (Portuguese, Brazilian) 2023-04-28 02:45:25 +02:00
Luna!
d7b76ed70a Fixed a typo in a comment in blocks.tsv (#509) 2023-04-27 17:49:15 +02:00
Eugen Rochko
43600756c0 New translations strings.xml (Chinese Traditional) 2023-04-25 18:41:49 +02:00
Eugen Rochko
3c3e0633ad New translations strings.xml (Danish) 2023-04-25 12:23:29 +02:00
Eugen Rochko
f819ad6917 New translations strings.xml (Danish) 2023-04-25 11:26:19 +02:00
sk
2e84faa505 update languages 2023-04-23 17:08:51 +02:00
sk
e7e8d13d9e fix auto hide fab in profile fragment 2023-04-22 22:33:20 +02:00
sk
a683c2cb11 hide fab in notifications 2023-04-22 22:20:40 +02:00
sk
addf7de316 single fab for home tabs
closes sk22#415
2023-04-22 21:25:02 +02:00
sk
44d4eada51 fix "load missing more" being hidden
closes sk22#482
2023-04-22 20:14:07 +02:00
sk
40bfdea5b1 fix pleroma emoji reaction notifications 2023-04-22 19:52:46 +02:00
sk
55138c1e86 fix wrong true black badge border color
closes sk22#485
2023-04-22 19:17:12 +02:00
Eugen Rochko
e7ad396fc6 New translations strings.xml (Italian) 2023-04-22 19:15:42 +02:00
sk
0aef680572 Merge remote-tracking branch 'weblate/main' 2023-04-22 19:06:03 +02:00
sk22
6dc37d6bde Translated using Weblate (German)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-04-22 17:05:32 +00:00
sk
60ea7cedf6 support glitch react notification 2023-04-22 19:05:14 +02:00
Espasant3
c986b10e14 Translated using Weblate (Galician)
Currently translated at 99.6% (272 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-04-22 17:00:55 +00:00
Choukajohn
d52174bd9e Translated using Weblate (French)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-04-22 17:00:55 +00:00
sk22
c65d138911 Translated using Weblate (German)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-04-22 17:00:55 +00:00
sk
ad9bb8ad58 support glitch react notification 2023-04-22 19:00:37 +02:00
sk
63e536c66c fix hidden no alt/alt badge remaining clickable
closes sk22#498
2023-04-22 18:20:05 +02:00
Jacoco
b5a08b1b98 Pleroma emoji reaction notifications (#499) 2023-04-22 17:39:41 +02:00
r3g_5z
226e2a7cdc Minor maintenance things (#501)
* validate gradle wrapper jar file

this is extremely important. see the following:

https://blog.gradle.org/wrapper-attack-report
https://github.com/gradle/wrapper-validation-action#the-gradle-wrapper-problem-in-open-source

Signed-off-by: r3g_5z <june@girlboss.ceo>

* update gradle wrapper to 8.1.1

it is necessary to run the gradlew update command twice to actually
update the jar file properly, e.g.:

./gradlew wrapper --gradle-version=8.1.1 --gradle-distribution-sha256-sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
./gradlew wrapper --gradle-version=8.1.1 --gradle-distribution-sha256-sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f

Signed-off-by: r3g_5z <june@girlboss.ceo>

* use Gradle toolchain

this allows for better build reproducibility and avoid mix and matching
JDKs from other projects

https://docs.gradle.org/current/userguide/toolchains.html

Signed-off-by: r3g_5z <june@girlboss.ceo>

* update dependencies and fix build errors

Signed-off-by: r3g_5z <june@girlboss.ceo>

---------

Signed-off-by: r3g_5z <june@girlboss.ceo>
Co-authored-by: sk22 <sk22@mailbox.org>
2023-04-22 17:28:16 +02:00
sk
4d7c4aed4c enable nonTransitiveRClass 2023-04-22 16:51:44 +02:00
sk
c9bcd000c3 update gradle 2023-04-22 16:49:42 +02:00
Eugen Rochko
b1cb4d4257 New translations strings.xml (German) 2023-04-22 16:44:30 +02:00
sk22
de42145f30 Translated using Weblate (German)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-04-22 14:38:25 +00:00
sk
7bcdd6070a boost instead of reblog 2023-04-22 16:38:16 +02:00
sk
8a215e90d0 Merge remote-tracking branch 'weblate/main' 2023-04-22 16:34:11 +02:00
sk
b736fa18bb remove empty french metadata 2023-04-22 16:32:00 +02:00
ihor_ck
43c19e4942 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-04-22 14:30:34 +00:00
Eryk Michalak
ffc18029bb Translated using Weblate (Polish)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-04-22 14:30:34 +00:00
Linerly
b88b3d15f8 Translated using Weblate (Indonesian)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-04-22 14:30:34 +00:00
Choukajohn
c817886a2d Translated using Weblate (French)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-04-22 14:30:34 +00:00
gallegonovato
aae239494e Translated using Weblate (Spanish)
Currently translated at 100.0% (273 of 273 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-04-22 14:30:34 +00:00
a_mento
b0b2daa5d5 Translated using Weblate (Basque)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/eu/
2023-04-22 14:30:34 +00:00
Espasant3
eea2e38f1b Translated using Weblate (Galician)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-04-22 14:30:34 +00:00
AiOO
f894ecd25b Translated using Weblate (Korean)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2023-04-22 14:30:34 +00:00
AiOO
e0b6ed7103 Translated using Weblate (Korean)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-04-22 14:30:34 +00:00
Espasant3
a78e75747a Translated using Weblate (Galician)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2023-04-22 14:30:34 +00:00
ihor_ck
3b25e367bb Translated using Weblate (Ukrainian)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-04-22 14:30:34 +00:00
gallegonovato
08b29dff3d Translated using Weblate (Spanish)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-04-22 14:30:34 +00:00
Choukajohn
2f2e053d26 Translated using Weblate (French)
Currently translated at 17.6% (3 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2023-04-22 14:30:34 +00:00
Pegasus89
191d582c30 Translated using Weblate (Croatian)
Currently translated at 12.5% (2 of 16 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/hr/
2023-04-22 14:30:34 +00:00
Pegasus89
8d3380ff6e Translated using Weblate (Croatian)
Currently translated at 97.4% (265 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/hr/
2023-04-22 14:30:34 +00:00
AiOO
ba85d18574 Translated using Weblate (Korean)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-04-22 14:30:34 +00:00
sk22
0f53b17515 Translated using Weblate (Spanish)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-04-22 14:30:34 +00:00
Anonymous
cb9c869712 Translated using Weblate (Russian)
Currently translated at 92.2% (251 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-04-22 14:30:34 +00:00
poesty
aa3d9e7b8f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-04-22 14:30:34 +00:00
sk
b3a9b5824d fix non-positional subtitution in string 2023-04-22 16:29:41 +02:00
sk
b6186a349f update gradle 2023-04-20 16:03:57 +02:00
Eugen Rochko
100bd4b062 New translations strings.xml (Galician) 2023-04-19 07:36:52 +02:00
Eugen Rochko
7da09d9b37 New translations strings.xml (Spanish) 2023-04-19 07:36:51 +02:00
Eugen Rochko
f46eb07228 New translations strings.xml (Galician) 2023-04-19 06:04:49 +02:00
Eugen Rochko
7627b5eb25 New translations strings.xml (Greek) 2023-04-17 15:01:08 +02:00
Eugen Rochko
c710448c6b New translations strings.xml (Greek) 2023-04-17 13:59:43 +02:00
Eugen Rochko
1ad270b1d6 New translations strings.xml (Chinese Simplified) 2023-04-16 18:13:11 +02:00
Eugen Rochko
099e253b2b New translations strings.xml (Chinese Simplified) 2023-04-16 17:11:48 +02:00
Eugen Rochko
66de4a5b91 New translations strings.xml (Thai) 2023-04-16 09:19:23 +02:00
Eugen Rochko
41437d91d5 New translations strings.xml (Icelandic) 2023-04-15 11:54:07 +02:00
Eugen Rochko
d33d5a6efa New translations strings.xml (Icelandic) 2023-04-15 10:45:31 +02:00
Eugen Rochko
4f9248d040 New translations strings.xml (Vietnamese) 2023-04-15 05:15:40 +02:00
Eugen Rochko
f40c0e41f3 New translations strings.xml (Japanese) 2023-04-14 20:26:49 +02:00
sk
15fcb0e25d fix alt badge padding and margin 2023-04-13 23:17:46 +02:00
sk
2dae662333 fix username displacement in compose 2023-04-13 23:03:07 +02:00
sk
3ad46926f1 Merge remote-tracking branch 'upstream/master' 2023-04-13 21:46:45 +02:00
sk
2385d102ae fix header username displacement 2023-04-13 21:42:43 +02:00
Eugen Rochko
deeb03ff2b New translations strings.xml (Indonesian) 2023-04-13 21:36:15 +02:00
Eugen Rochko
5c2a09e243 New translations strings.xml (Indonesian) 2023-04-13 20:38:20 +02:00
Eugen Rochko
2473c999db New translations strings.xml (German) 2023-04-13 14:38:33 +02:00
Eugen Rochko
ea2cc265e3 New translations strings.xml (Turkish) 2023-04-13 05:12:38 +02:00
Eugen Rochko
a0cd2d42cf New translations strings.xml (Turkish) 2023-04-13 04:16:05 +02:00
Grishka
0a17ceb984 Merge branch 'l10n_master' 2023-04-12 19:20:42 +03:00
Grishka
4ef18f1f4a Add touch interaction for the SplashFragment art 2023-04-12 19:20:23 +03:00
Grishka
0de227ab9c Use fixed colors for SplashFragment
fixes #561
2023-04-12 18:47:02 +03:00
Eugen Rochko
19cb8703a6 New translations strings.xml (Kabyle) 2023-04-12 17:37:10 +02:00
Eugen Rochko
e18567dd82 New translations strings.xml (Filipino) 2023-04-12 17:37:06 +02:00
Eugen Rochko
bfb3bcdbfb New translations strings.xml (Portuguese, Brazilian) 2023-04-12 17:37:02 +02:00
Eugen Rochko
565cd14d88 New translations strings.xml (Galician) 2023-04-12 17:37:00 +02:00
Eugen Rochko
ebc37eac75 New translations strings.xml (Vietnamese) 2023-04-12 17:36:59 +02:00
Eugen Rochko
c3702db577 New translations strings.xml (Chinese Traditional) 2023-04-12 17:36:59 +02:00
Eugen Rochko
e15c4fa342 New translations strings.xml (Chinese Simplified) 2023-04-12 17:36:58 +02:00
Eugen Rochko
8330b9f1c5 New translations strings.xml (Turkish) 2023-04-12 17:36:57 +02:00
Eugen Rochko
f759150982 New translations strings.xml (Swedish) 2023-04-12 17:36:56 +02:00
Eugen Rochko
6af177b596 New translations strings.xml (Slovenian) 2023-04-12 17:36:55 +02:00
Eugen Rochko
657bb94975 New translations strings.xml (Russian) 2023-04-12 17:36:54 +02:00
Eugen Rochko
3c946212b1 New translations strings.xml (Polish) 2023-04-12 17:36:52 +02:00
Eugen Rochko
b4e80f7fca New translations strings.xml (Norwegian) 2023-04-12 17:36:51 +02:00
Eugen Rochko
c9efc2cb2b New translations strings.xml (Korean) 2023-04-12 17:36:50 +02:00
Eugen Rochko
0d62e33dc7 New translations strings.xml (Japanese) 2023-04-12 17:36:49 +02:00
Eugen Rochko
ac88b9e19c New translations strings.xml (Italian) 2023-04-12 17:36:48 +02:00
Eugen Rochko
871dfda79e New translations strings.xml (Hungarian) 2023-04-12 17:36:47 +02:00
Eugen Rochko
e0c2c208ae New translations strings.xml (Basque) 2023-04-12 17:36:44 +02:00
Eugen Rochko
22ac112bdb New translations strings.xml (German) 2023-04-12 17:36:43 +02:00
Eugen Rochko
afd0cca176 New translations strings.xml (Danish) 2023-04-12 17:36:42 +02:00
Eugen Rochko
c083c8bce5 New translations strings.xml (Arabic) 2023-04-12 17:36:41 +02:00
Eugen Rochko
63bde032b3 New translations strings.xml (French) 2023-04-12 17:36:40 +02:00
Eugen Rochko
49492c0788 New translations strings.xml (Belarusian) 2023-04-12 17:36:38 +02:00
Eugen Rochko
b439c64add New translations strings.xml (Greek) 2023-04-12 17:36:37 +02:00
Eugen Rochko
1868bfe8e3 New translations strings.xml (Thai) 2023-04-12 17:36:36 +02:00
Eugen Rochko
f240a3d996 New translations strings.xml (Indonesian) 2023-04-12 17:36:35 +02:00
Eugen Rochko
788e5bd12e New translations strings.xml (Icelandic) 2023-04-12 17:36:34 +02:00
Eugen Rochko
a55fed4502 New translations strings.xml (Ukrainian) 2023-04-12 17:36:33 +02:00
Eugen Rochko
a8fdaf1a47 New translations strings.xml (Dutch) 2023-04-12 17:36:32 +02:00
Eugen Rochko
4a758bd488 New translations strings.xml (Czech) 2023-04-12 17:36:31 +02:00
Eugen Rochko
2c9731ec2a New translations strings.xml (Spanish) 2023-04-12 17:36:30 +02:00
Grishka
eef33266fc Remove unused code and strings 2023-04-12 18:34:28 +03:00
Eugen Rochko
58d2c3e5a6 New translations strings.xml (Indonesian) 2023-04-11 17:28:22 +02:00
Eugen Rochko
9e6a355db0 New translations strings.xml (Slovenian) 2023-04-10 00:54:45 +02:00
Eugen Rochko
0d10e09fd6 New translations strings.xml (Slovenian) 2023-04-09 23:47:00 +02:00
Eugen Rochko
f85bb995ba New translations strings.xml (Greek) 2023-04-09 17:56:44 +02:00
Eugen Rochko
268e5639f6 New translations strings.xml (Greek) 2023-04-09 16:26:19 +02:00
Eugen Rochko
51809df8ca New translations strings.xml (Thai) 2023-04-09 07:25:51 +02:00
Eugen Rochko
94fb676b0c New translations strings.xml (Chinese Traditional) 2023-04-09 04:29:35 +02:00
Eugen Rochko
a47106594b New translations strings.xml (Chinese Traditional) 2023-04-09 03:10:02 +02:00
Grishka
d93d66f702 Prepare new release 2023-04-09 01:59:13 +03:00
Eugen Rochko
b2f9f7ae54 New translations strings.xml (Italian) 2023-04-09 00:51:05 +02:00
Grishka
de7b908c78 Merge branch 'l10n_master' 2023-04-09 01:50:55 +03:00
Eugen Rochko
75d3c2fdce New translations strings.xml (Italian) 2023-04-08 23:55:36 +02:00
Eugen Rochko
ea1b6c5835 New translations strings.xml (Greek) 2023-04-08 22:51:05 +02:00
Eugen Rochko
cb7887da41 New translations strings.xml (Greek) 2023-04-08 21:38:06 +02:00
Eugen Rochko
a80313ee6b New translations strings.xml (Thai) 2023-04-07 23:53:35 +02:00
Eugen Rochko
e1a821bc43 New translations strings.xml (Thai) 2023-04-07 22:56:50 +02:00
Grishka
924ea2d03a Fix #557 2023-04-07 22:58:04 +03:00
Grishka
55270fe654 Fix 2023-04-07 22:55:29 +03:00
Eugen Rochko
a125fab57b New translations strings.xml (Kabyle) 2023-04-07 21:48:05 +02:00
Eugen Rochko
395ee0aa99 New translations strings.xml (Scottish Gaelic) 2023-04-07 21:48:03 +02:00
Eugen Rochko
0f50fa6ba1 New translations strings.xml (Bosnian) 2023-04-07 21:48:01 +02:00
Eugen Rochko
adb7df3c71 New translations strings.xml (Filipino) 2023-04-07 21:48:00 +02:00
Eugen Rochko
5d7bcb629b New translations strings.xml (Burmese) 2023-04-07 21:47:59 +02:00
Eugen Rochko
a00f1417d2 New translations strings.xml (Hindi) 2023-04-07 21:47:59 +02:00
Eugen Rochko
8efd7e8ebf New translations strings.xml (Croatian) 2023-04-07 21:47:58 +02:00
Eugen Rochko
b016d277e0 New translations strings.xml (Bengali) 2023-04-07 21:47:57 +02:00
Eugen Rochko
fdb39617d1 New translations strings.xml (Persian) 2023-04-07 21:47:56 +02:00
Eugen Rochko
89f83fbf62 New translations strings.xml (Portuguese, Brazilian) 2023-04-07 21:47:55 +02:00
Eugen Rochko
ecee9e01a6 New translations strings.xml (Galician) 2023-04-07 21:47:54 +02:00
Eugen Rochko
20dc9bb8b9 New translations strings.xml (Vietnamese) 2023-04-07 21:47:53 +02:00
Eugen Rochko
2c47d0e9ed New translations strings.xml (Chinese Traditional) 2023-04-07 21:47:52 +02:00
Eugen Rochko
8e13d52e51 New translations strings.xml (Chinese Simplified) 2023-04-07 21:47:51 +02:00
Eugen Rochko
cc40198c9e New translations strings.xml (Turkish) 2023-04-07 21:47:50 +02:00
Eugen Rochko
290897ea41 New translations strings.xml (Swedish) 2023-04-07 21:47:49 +02:00
Eugen Rochko
b9e1c84304 New translations strings.xml (Slovenian) 2023-04-07 21:47:48 +02:00
Eugen Rochko
3c44c80e2e New translations strings.xml (Russian) 2023-04-07 21:47:47 +02:00
Eugen Rochko
dffa4e4594 New translations strings.xml (Portuguese) 2023-04-07 21:47:46 +02:00
Eugen Rochko
fa2d9fec58 New translations strings.xml (Polish) 2023-04-07 21:47:45 +02:00
Eugen Rochko
09c1a2cfa0 New translations strings.xml (Norwegian) 2023-04-07 21:47:44 +02:00
Eugen Rochko
d1f90eb231 New translations strings.xml (Korean) 2023-04-07 21:47:42 +02:00
Eugen Rochko
1f7d97134b New translations strings.xml (Japanese) 2023-04-07 21:47:42 +02:00
Eugen Rochko
79be91784d New translations strings.xml (Italian) 2023-04-07 21:47:41 +02:00
Eugen Rochko
de2654def3 New translations strings.xml (Hungarian) 2023-04-07 21:47:39 +02:00
Eugen Rochko
56343dacff New translations strings.xml (Hebrew) 2023-04-07 21:47:38 +02:00
Eugen Rochko
0e677f8ce7 New translations strings.xml (Basque) 2023-04-07 21:47:36 +02:00
Eugen Rochko
aa911896d6 New translations strings.xml (German) 2023-04-07 21:47:35 +02:00
Eugen Rochko
c62a8635b9 New translations strings.xml (Danish) 2023-04-07 21:47:34 +02:00
Eugen Rochko
4e900247c5 New translations strings.xml (Catalan) 2023-04-07 21:47:33 +02:00
Eugen Rochko
b3bd62bc6c New translations strings.xml (Arabic) 2023-04-07 21:47:32 +02:00
Eugen Rochko
8e5fd48ecd New translations strings.xml (French) 2023-04-07 21:47:31 +02:00
Eugen Rochko
b2bca9dd2c New translations strings.xml (Romanian) 2023-04-07 21:47:30 +02:00
Eugen Rochko
b8c0dc3181 New translations strings.xml (Belarusian) 2023-04-07 21:47:29 +02:00
Eugen Rochko
cccdc5292e New translations strings.xml (Greek) 2023-04-07 21:47:28 +02:00
Eugen Rochko
76d77a0e7a New translations strings.xml (Thai) 2023-04-07 21:47:27 +02:00
Eugen Rochko
e737f4bf9a New translations strings.xml (Indonesian) 2023-04-07 21:47:26 +02:00
Eugen Rochko
391db2f1c9 New translations strings.xml (Icelandic) 2023-04-07 21:47:25 +02:00
Eugen Rochko
359d61183c New translations strings.xml (Ukrainian) 2023-04-07 21:47:24 +02:00
Eugen Rochko
46fd05d88e New translations strings.xml (Dutch) 2023-04-07 21:47:23 +02:00
Eugen Rochko
cde22a0945 New translations strings.xml (Czech) 2023-04-07 21:47:22 +02:00
Eugen Rochko
111b7e25c5 New translations strings.xml (Spanish) 2023-04-07 21:47:21 +02:00
Grishka
4f8d8f0c8d Welcome fragment redesign again
# Conflicts:
#	mastodon/src/main/res/values/strings.xml
#	mastodon/src/main/res/values/styles.xml
2023-04-07 22:44:28 +03:00
Grishka
915b0603d0 Reblog -> boost 2023-04-07 22:42:16 +03:00
sk
6ec43a6f86 slightly smaller collapsed height
closes sk22#480
2023-04-07 18:45:11 +02:00
sk
df93a1a845 increase max height 2023-04-07 18:42:18 +02:00
sk
41a70a353c distinct default languages
closes sk22#487
2023-04-07 18:21:23 +02:00
sk
8d69bcfd4b new profile counters for account card
closes sk22#483
2023-04-07 18:08:57 +02:00
sk
0ef30f82a7 fix disappearing no-alt indicator
closes sk22#484
2023-04-07 17:16:45 +02:00
sk
be60e78ea6 improve external share behavior 2023-04-07 16:58:02 +02:00
sk
5434325fa8 fix header alignments… again 2023-04-07 16:30:49 +02:00
sk
0a04c9357c Revert "display reblog popup by default"
This reverts commit 21c4cef397.

okay, so, i think i'll keep reblog as a default. i fear that exposing everyone
to an overwhelming menu (you literally have to *decide* for a visibility!)
when just pressing reblog might not be a good idea. i'll just have "confirm
before reblogging" as an option in the settings instead
https://floss.social/@megalodon/110157968813469351
2023-04-07 16:20:29 +02:00
Eugen Rochko
075aab8074 New translations strings.xml (Greek) 2023-04-07 16:16:35 +02:00
sk
21c4cef397 display reblog popup by default 2023-04-07 16:04:35 +02:00
sk
4b2fcd760a add option to confirm before reblog
closes sk22#456
2023-04-07 15:29:43 +02:00
Eugen Rochko
6ebe4c86af New translations strings.xml (Greek) 2023-04-07 15:16:19 +02:00
Eugen Rochko
0925c8c582 New translations strings.xml (Spanish) 2023-04-06 21:52:50 +02:00
sk
9824b5fb56 allow boosting with every visibility
closes sk22#486
2023-04-06 20:24:40 +02:00
sk
78fcf31e34 remove unused crowdin.yml 2023-04-06 20:16:33 +02:00
sk
eadb62d3a8 fix wrong rel=me link on website 2023-04-06 20:16:26 +02:00
sk
f6279fcc0c Merge branch 'l10n_fr' of codeberg.org:butterflyoffire/megalodon 2023-04-06 20:08:46 +02:00
Eugen Rochko
a683fdce62 New translations strings.xml (Greek) 2023-04-06 18:46:34 +02:00
Eugen Rochko
b958299446 New translations strings.xml (Greek) 2023-04-06 17:45:19 +02:00
Eugen Rochko
3f80be8377 New translations strings.xml (Greek) 2023-04-05 16:29:26 +02:00
Eugen Rochko
ced0accde5 New translations strings.xml (Belarusian) 2023-04-05 12:06:47 +02:00
Eugen Rochko
b454ff5ec7 New translations strings.xml (Belarusian) 2023-04-05 11:07:06 +02:00
Eugen Rochko
45af198f32 New translations strings.xml (Greek) 2023-04-05 02:04:08 +02:00
Eugen Rochko
ff374f8899 New translations strings.xml (Greek) 2023-04-05 00:46:17 +02:00
Eugen Rochko
faecb3bc4b New translations strings.xml (Greek) 2023-04-04 20:21:01 +02:00
Eugen Rochko
6b893fadef New translations short_description.txt (Greek) 2023-04-04 17:47:01 +02:00
Eugen Rochko
c328467a41 New translations strings.xml (Greek) 2023-04-04 17:47:00 +02:00
Eugen Rochko
182325470b New translations full_description.txt (Greek) 2023-04-04 17:46:59 +02:00
Eugen Rochko
f330ad71ac New translations full_description.txt (Greek) 2023-04-04 16:06:32 +02:00
Eugen Rochko
ba0c064f36 New translations full_description.txt (Chinese Traditional) 2023-04-03 16:47:01 +02:00
Eugen Rochko
8d7aaee5b9 New translations strings.xml (Dutch) 2023-04-01 15:16:39 +02:00
Eugen Rochko
68cba2de63 New translations strings.xml (Icelandic) 2023-03-29 00:29:42 +02:00
Eugen Rochko
5a914f9c0e New translations strings.xml (Thai) 2023-03-28 21:06:33 +02:00
Eugen Rochko
b0e6805a20 New translations strings.xml (Czech) 2023-03-28 16:34:35 +02:00
Eugen Rochko
21e7e44c01 New translations strings.xml (Czech) 2023-03-28 15:36:30 +02:00
Eugen Rochko
f7df4abdae New translations strings.xml (Spanish) 2023-03-26 17:05:29 +02:00
Eugen Rochko
7674ceefe9 New translations strings.xml (Ukrainian) 2023-03-25 20:42:18 +01:00
Eugen Rochko
4be575c534 New translations strings.xml (Ukrainian) 2023-03-25 19:25:16 +01:00
Eugen Rochko
dd0f0a7d5a New translations strings.xml (Indonesian) 2023-03-25 04:22:02 +01:00
Eugen Rochko
759b44c224 New translations strings.xml (Indonesian) 2023-03-25 03:16:02 +01:00
sk
ebdd1a0b03 rename version 2023-03-24 20:05:42 +01:00
sk
6c44575f7a bump version 2023-03-24 20:04:35 +01:00
sk
8be832239a Merge remote-tracking branch 'upstream/l10n_master' 2023-03-24 20:03:31 +01:00
sk
c7be202430 Merge remote-tracking branch 'weblate/main' 2023-03-24 20:03:21 +01:00
sk
244c5dc6b4 bump version 2023-03-24 19:58:10 +01:00
poesty
6bbb9a638e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-03-24 18:40:14 +00:00
Espasant3
44041b136a Translated using Weblate (Galician)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-03-24 18:40:14 +00:00
ihor_ck
7e17c30ce2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-03-24 18:40:14 +00:00
McKris
c4d3d1b409 Translated using Weblate (Polish)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-03-24 18:40:14 +00:00
Linerly
03de63754b Translated using Weblate (Indonesian)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-03-24 18:40:14 +00:00
Choukajohn
0e506f0b1a Translated using Weblate (French)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-03-24 18:40:14 +00:00
gallegonovato
a2ab752870 Translated using Weblate (Spanish)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-03-24 18:40:14 +00:00
sk22
eaa032828a Translated using Weblate (German)
Currently translated at 100.0% (272 of 272 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-03-24 18:40:14 +00:00
sk
cceb0b4c6c Merge remote-tracking branch 'upstream/master' 2023-03-24 19:33:40 +01:00
sk
a58640a718 fix crash when instance not yet loaded
closes sk22#474
2023-03-24 19:04:13 +01:00
Eugen Rochko
920384b26c New translations strings.xml (Dutch) 2023-03-24 16:26:08 +01:00
Eugen Rochko
193a2c4f70 New translations strings.xml (German) 2023-03-23 13:42:28 +01:00
Eugen Rochko
793dec98b2 New translations strings.xml (Vietnamese) 2023-03-23 07:36:02 +01:00
Eugen Rochko
11ddf8015d New translations strings.xml (Galician) 2023-03-23 05:17:18 +01:00
Eugen Rochko
1753bdbd8b New translations strings.xml (Thai) 2023-03-22 17:21:08 +01:00
Grishka
d6e563486b Fix alt text button 2023-03-22 02:46:48 +03:00
Grishka
0112bfa9c4 Fix #547 2023-03-22 02:38:58 +03:00
Grishka
5951611fb0 Fix #551 2023-03-22 02:34:25 +03:00
sk
2d31b726ac Merge branch 'main' of github.com:sk22/megalodon 2023-03-21 16:04:43 +01:00
sk22
d2f295ef88 Update README.md 2023-03-21 16:03:35 +01:00
sk
d1b53554ce bump version 2023-03-21 15:59:40 +01:00
sk
ec5db122d0 full text can have emojis, too 2023-03-21 15:56:44 +01:00
sk
0216e22fcc don't remove badge if loaded from cache 2023-03-21 15:25:01 +01:00
sk
5734b19d8c remove unused variable 2023-03-21 15:14:25 +01:00
Eugen Rochko
cc8c818e13 New translations strings.xml (Slovenian) 2023-03-21 11:52:06 +01:00
sk
e58aeec097 query notifications on load posts
closes sk22#471
2023-03-21 10:22:09 +01:00
sk
58b000927a session-specific notification badge
fix sk22#470
2023-03-21 10:03:31 +01:00
sk
797642b972 fix profile fragment crash
closes sk22#469
2023-03-21 09:56:44 +01:00
Eugen Rochko
2e1f273d78 New translations strings.xml (Chinese Traditional) 2023-03-21 06:38:54 +01:00
Eugen Rochko
35d6800877 New translations strings.xml (Chinese Traditional) 2023-03-21 05:17:21 +01:00
sk
781856b822 bump version 2023-03-21 00:55:11 +01:00
sk
ff272179e7 implement notification badge using markers 2023-03-21 00:52:20 +01:00
sk
bec47f40f7 colorful swipe-to-refresh spinner
closes sk22#455
2023-03-20 23:25:29 +01:00
sk
f9607a434a use accent color for notif title
closes sk22#461
2023-03-20 22:19:15 +01:00
sk
b650ca85bc Merge remote-tracking branch 'upstream/master' 2023-03-20 21:51:15 +01:00
Eugen Rochko
91e154bbee New translations strings.xml (Italian) 2023-03-20 20:01:49 +01:00
sk
f4365ed163 fix context menu resizing
closes sk22#467
2023-03-20 19:17:22 +01:00
Eugen Rochko
0c02b0cb68 New translations strings.xml (Dutch) 2023-03-20 18:49:16 +01:00
Gregory K
b0aaa58fa7 Merge pull request #550 from sk22/fix/header-follow-protected-account
Fix following protected account from header
2023-03-20 20:36:11 +03:00
sk
054ab774d5 Merge branch 'fix/header-follow-protected-account' 2023-03-20 18:28:27 +01:00
sk
8ca33b552d fix following protected account from header
closes mastodon#549
2023-03-20 18:23:49 +01:00
sk
6734c2b9f7 add null check for source object
closes sk22#460
2023-03-20 14:22:46 +01:00
sk
c484477d6a persist translation between display items
closes sk22#466
2023-03-20 11:24:47 +01:00
sk
d9b5223749 collapse regardless of in timeline
re: sk22#462
2023-03-20 10:48:00 +01:00
sk
55856450b3 hopefully fix broken compact reply line 2023-03-20 10:22:43 +01:00
sk
bb28a3bf83 Merge remote-tracking branch 'upstream/master' 2023-03-19 18:51:10 +01:00
Gregory K
5e4e56bd2c Merge pull request #545 from FineFindus/fix/empty-search-query
fix(search): check for empty queries
2023-03-17 20:10:02 +03:00
FineFindus
82fac1d4e7 fix(search): check for empty queries
Fixes an error message, which would appear, if the search query was blank.
2023-03-17 16:19:38 +01:00
Weblate Admin
c97a7e5158 Translated using Weblate (French)
Currently translated at 98.8% (351 of 355 strings)

Co-authored-by: Weblate Admin <butterflyoffire@todz.ynh.fr>
Translate-URL: https://rosette.todz.ynh.fr/projects/megalodon-app/app-strings/fr/
Translation: Megalodon app/Megalodon app
2022-12-06 09:19:24 +01:00
Weblate Admin
32e2d24b15 Translated using Weblate (Arabic (Algeria))
Currently translated at 94.0% (334 of 355 strings)

Translated using Weblate (French)

Currently translated at 96.9% (344 of 355 strings)

Co-authored-by: Weblate Admin <butterflyoffire@todz.ynh.fr>
Translate-URL: https://rosette.todz.ynh.fr/projects/megalodon-app/app-strings/ar_DZ/
Translate-URL: https://rosette.todz.ynh.fr/projects/megalodon-app/app-strings/fr/
Translation: Megalodon app/Megalodon app
2022-12-04 07:10:53 +01:00
Weblate Admin
c102aae819 Translated using Weblate (French)
Currently translated at 95.4% (339 of 355 strings)

Co-authored-by: Weblate Admin <butterflyoffire@todz.ynh.fr>
Translate-URL: https://rosette.todz.ynh.fr/projects/megalodon-app/app-strings/fr/
Translation: Megalodon app/Megalodon app
2022-12-03 18:22:49 +01:00
Weblate
bed72cb5ed Added translation using Weblate (Occitan)
Co-authored-by: Weblate <noreply@weblate.org>
2022-12-03 04:15:12 +01:00
Weblate
f0c1046fe9 Added translation using Weblate (Kabyle)
Co-authored-by: Weblate <noreply@weblate.org>
2022-12-03 04:15:08 +01:00
Weblate
d88104d105 Added translation using Weblate (French)
Added translation using Weblate (Arabic (Algeria))

Co-authored-by: Weblate <noreply@weblate.org>
2022-12-03 04:15:03 +01:00
Weblate
f3e21e5a82 Added translation using Weblate (Occitan)
Co-authored-by: Weblate <noreply@weblate.org>
2022-12-03 04:14:56 +01:00
Weblate
a77bee8664 Added translation using Weblate (Kabyle)
Co-authored-by: Weblate <noreply@weblate.org>
2022-12-03 04:14:51 +01:00
Weblate
b5d0aed59e Added translation using Weblate (French)
Added translation using Weblate (Arabic (Algeria))

Co-authored-by: Weblate <noreply@weblate.org>
2022-12-03 04:14:47 +01:00
Weblate Admin
2440cc6af5 Translated using Weblate (Arabic (Algeria))
Currently translated at 91.2% (324 of 355 strings)

Co-authored-by: Weblate Admin <butterflyoffire@todz.ynh.fr>
Translate-URL: https://rosette.todz.ynh.fr/projects/megalodon-app/app-strings/ar_DZ/
Translation: Megalodon app/Megalodon app
2022-12-02 22:40:00 +01:00
butterflyoffire
90114dfbe0 Supprimer 'mastodon/src/main/res/values-ar-DZ/strings.xml' 2022-12-02 21:34:54 +00:00
Weblate Admin
64b8cdf7dc Translated using Weblate (Arabic (Algeria))
Currently translated at 89.8% (319 of 355 strings)

Translated using Weblate (Arabic (Algeria))

Currently translated at 0.2% (1 of 355 strings)

Added translation using Weblate (Arabic (Algeria))

Added translation using Weblate (Arabic (Algeria))

Co-authored-by: Weblate Admin <butterflyoffire@todz.ynh.fr>
Translate-URL: https://rosette.todz.ynh.fr/projects/megalodon-app/app-strings/ar_DZ/
Translation: Megalodon app/Megalodon app
2022-12-02 19:14:49 +01:00
201 changed files with 4611 additions and 1511 deletions

View File

@@ -0,0 +1,11 @@
name: Validate Gradle Wrapper
on: [pull_request, push]
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1

View File

@@ -55,7 +55,7 @@ On the Fediverse, its quite common for people to pin posts they want others t
[apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk) [apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a> <a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first: Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Megalodon</title> <title>Megalodon</title>
<link rel="icon" href="mastodon/src/main/res/mipmap-mdpi/ic_launcher_round.png"> <link rel="icon" href="mastodon/src/main/res/mipmap-mdpi/ic_launcher_round.png">
<link rel="me" href="https://floss.social/@mastodon"> <link rel="me" href="https://floss.social/@megalodon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css">
</head> </head>
<body class="markdown-body"> <body class="markdown-body">

View File

@@ -5,7 +5,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.3.1' classpath 'com.android.tools.build:gradle:8.0.0'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }

View File

@@ -1,5 +0,0 @@
files:
- source: /mastodon/src/main/res/values/strings.xml
translation: /mastodon/src/main/res/values-%android_code%/strings.xml
- source: /fastlane/metadata/android/en-US/*.txt
translation: /fastlane/metadata/android/%locale%/%original_file_name%

View File

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

Binary file not shown.

View File

@@ -1,6 +1,7 @@
#Thu Jan 13 11:33:43 MSK 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

288
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,67 +17,98 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" # This is normally unused
APP_BASE_NAME=`basename "$0"` # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +129,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +137,109 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
MAX_FD="$MAX_FD_LIMIT" # shellcheck disable=SC3045
fi MAX_FD=$( ulimit -H -n ) ||
ulimit -n $MAX_FD warn "Could not query maximum file descriptor limit"
if [ $? -ne 0 ] ; then esac
warn "Could not set maximum file descriptor limit: $MAX_FD" case $MAX_FD in #(
fi '' | soft) :;; #(
else *)
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
fi # shellcheck disable=SC3045
fi ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

15
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@@ -2,6 +2,12 @@ plugins {
id 'com.android.application' id 'com.android.application'
} }
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
android { android {
compileSdk 33 compileSdk 33
defaultConfig { defaultConfig {
@@ -9,11 +15,11 @@ android {
applicationId "org.joinmastodon.android.sk" applicationId "org.joinmastodon.android.sk"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 79 versionCode 83
versionName "1.2.0+fork.79" versionName "1.2.3+fork.83"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW" resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
} }
buildTypes { buildTypes {
release { release {
@@ -49,14 +55,19 @@ android {
setRoot "src/github" setRoot "src/github"
} }
} }
lintOptions{ namespace 'org.joinmastodon.android'
checkReleaseBuilds false lint {
abortOnError false abortOnError false
checkReleaseBuilds false
}
buildFeatures {
buildConfig true
} }
} }
dependencies { dependencies {
api 'androidx.annotation:annotation:1.3.0' api 'androidx.annotation:annotation:1.6.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.9' implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'me.grishka.litex:recyclerview:1.2.1.1' implementation 'me.grishka.litex:recyclerview:1.2.1.1'
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0.1' implementation 'me.grishka.litex:swiperefreshlayout:1.1.0.1'
@@ -65,7 +76,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0' implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0' implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.7' implementation 'me.grishka.appkit:appkit:1.2.7'
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.jsoup:jsoup:1.14.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8' implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3' implementation 'de.psdev:async-otto:1.0.3'

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="org.joinmastodon.android">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="org.joinmastodon.android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -17,6 +16,9 @@
<action android:name="android.intent.action.PROCESS_TEXT" /> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent> </intent>
<intent>
<action android:name="android.intent.action.TRANSLATE" />
</intent>
</queries> </queries>
<application <application
@@ -36,6 +38,22 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".PanicResponderActivity"
android:exported="true"
android:launchMode="singleInstance"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ExitActivity"
android:exported="false"
android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask"> <activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>

View File

@@ -1,89 +0,0 @@
# lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
# https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
# This list contains domains of toxic mastodon instances
# Last-Modified: 1672044500
# gab - a neonazi social network
gab.ai
gab.com
gab.protohype.net
# consequence-free speech
social.unzensiert.to
freeatlantis.com
# reactionary bigotry and hatespeech against magrinalized groups
poa.st
freespeechextremist.com
rdrama.cc
outpoa.st
anime.website
gameliberty.club
social.byoblu.com
yggdrasil.social
smuglo.li
dogeposting.social
unsafe.space
freezepeach.xyz
# + CSAM
rojogato.com
# antivaxxer shitposting & fearmongering
shadowsocial.org
# Kiwifarms
kiwifarms.net
kiwifarms.cc
kiwifarms.is
kiwifarms.pleroma.net
# https://mastodon.art/@Curator/109649354849593592
poa.st antisemitic racist homophobic
nicecrew.digital antisemitic
beefyboys.win antisemitic racist homophobic harassment
cawfee.club antisemitic racist homophobic
comfyboy.club antisemitic racist homophobic
freespeechextremist.com racist homophobic
cum.salon racist misogynist
bae.st racist
natehiggers.online racist
rapemeat.solutions misogynist
rapist.town misogynist
rapefeminists.network misogynist
kiwifarms.cc harassment
noagendasocial.com noagenda
posting.lolicon.rocks underage
urchan.org harassment homophobic racist
ryona.agency harassment
yggdrasil.social antisemitic homophobic racist
genderheretics.xyz transphobic
baraag.net underage
lolison.top underage
shota.house underage
shota.social underage
aethy.com underage
taullo.social underage
childpawn.shop underage
posting.lolicon.rocks underage
loli.best underage
gothloli.club underage
smuglo.li underage
youjo.love underage
pedo.school underage
lolison.network underage
freak.university underage
mirr0r.city underage
xhais.love underage
refusal.biz underage
refusal.llc underage
mirr0r.city underage
nnia.space underage
ignorelist.com malicious
repl.co malicious
# custom
pawoo.net csam
1 # lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
2 # https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
3 # This list contains domains of toxic mastodon instances
4 # Last-Modified: 1672044500
5 # gab - a neonazi social network
6 gab.ai
7 gab.com
8 gab.protohype.net
9 # consequence-free speech
10 social.unzensiert.to
11 freeatlantis.com
12 # reactionary bigotry and hatespeech against magrinalized groups
13 poa.st
14 freespeechextremist.com
15 rdrama.cc
16 outpoa.st
17 anime.website
18 gameliberty.club
19 social.byoblu.com
20 yggdrasil.social
21 smuglo.li
22 dogeposting.social
23 unsafe.space
24 freezepeach.xyz
25 # + CSAM
26 rojogato.com
27 # antivaxxer shitposting & fearmongering
28 shadowsocial.org
29 # Kiwifarms
30 kiwifarms.net
31 kiwifarms.cc
32 kiwifarms.is
33 kiwifarms.pleroma.net
34 # https://mastodon.art/@Curator/109649354849593592
35 poa.st antisemitic racist homophobic
36 nicecrew.digital antisemitic
37 beefyboys.win antisemitic racist homophobic harassment
38 cawfee.club antisemitic racist homophobic
39 comfyboy.club antisemitic racist homophobic
40 freespeechextremist.com racist homophobic
41 cum.salon racist misogynist
42 bae.st racist
43 natehiggers.online racist
44 rapemeat.solutions misogynist
45 rapist.town misogynist
46 rapefeminists.network misogynist
47 kiwifarms.cc harassment
48 noagendasocial.com noagenda
49 posting.lolicon.rocks underage
50 urchan.org harassment homophobic racist
51 ryona.agency harassment
52 yggdrasil.social antisemitic homophobic racist
53 genderheretics.xyz transphobic
54 baraag.net underage
55 lolison.top underage
56 shota.house underage
57 shota.social underage
58 aethy.com underage
59 taullo.social underage
60 childpawn.shop underage
61 posting.lolicon.rocks underage
62 loli.best underage
63 gothloli.club underage
64 smuglo.li underage
65 youjo.love underage
66 pedo.school underage
67 lolison.network underage
68 freak.university underage
69 mirr0r.city underage
70 xhais.love underage
71 refusal.biz underage
72 refusal.llc underage
73 mirr0r.city underage
74 nnia.space underage
75 ignorelist.com malicious
76 repl.co malicious
77 # custom
78 pawoo.net csam

View File

@@ -0,0 +1,171 @@
13bells.com
4aem.com
aethy.com
anime.website
annihilation.social
anon-kenkai.com
asbestos.cafe
bae.st
bajax.us
banepo.st
baraag.net
beefyboys.win
beepboop.ga
berserker.town
bikeshed.party
boks.moe
brainsoap.net
breastmilk.club
brighteon.social
cawfee.club
clew.lol
clubcyberia.co
collapsitarian.io
comfyboy.club
contrapointsfan.club
cum.camp
cum.salon
cybercriminal.eu
darknight-coffee.org
dembased.xyz
desupost.soy
detroitriotcity.com
eatthebugs.social
eientei.org
elementality.org
eveningzoo.club
firedragonstudios.com
firefaithfellowship.com
fluf.club
foxfam.club
freak.university
freeatlantis.com
freecumextremist.com
freedomstrike.org
freesoftwareextremist.com
freespeech.group
freespeechextremist.com
freetalklive.com
froth.zone
fulltermprivacy.com
gameliberty.club
gearlandia.haus
genderheretics.xyz
geofront.rocks
gleasonator.com
glee.li
glindr.org
goyim.app
goyslop.cafe
haeder.net
handholding.io
hidamari.apartments
hitchhiker.social
hunk.city
iddqd.social
intkos.link
justicewarrior.social
kawa-kun.com
kitsunemimi.club
kiwifarms.cc
kompost.cz
kurosawa.moe
leafposter.club
leftychan.net
lewdieheaven.com
liberdon.com
ligma.pro
lizards.live
lolicon.rocks
lolison.top
lovingexpressions.net
lucasvl.nl
mahodou.moe
makemysarcophagus.com
maladaptive.art
masochi.st
mastinator.com
merovingian.club
midwaytrades.com
mirr0r.city
moa.st
mouse.services
mugicha.club
narrativerry.xyz
natehiggers.online
neckbeard.xyz
needs.vodka
neenster.org
nicecrew.digital
nnia.space
noagendasocial.com
noagendasocial.nl
noagendatube.com
nobodyhasthe.biz
nukem.biz
obo.sh
onionfarms.org
outpoa.st
pawlicker.com
pawoo.net
pedo.school
piazza.today
pibvt.net
pieville.net
pisskey.io
plagu.ee
pmth.us
poa.st
poast.org
poast.tv
poster.place
prospeech.space
quodverum.com
rakket.app
rapemeat.solutions
rdrama.cc
rebelbase.site
retardedniggers.forsale
rojogato.com
ryona.agency
schwartzwelt.xyz
seal.cafe
shigusegubu.club
shitpost.cloud
shitposter.club
shota.house
silliness.observer
skinheads.eu
skinheads.io
skinheads.social
skinheads.uk
skippers-bin.com
skyshanty.xyz
slash.cl
sleepy.cafe
smuglo.li
sneed.social
sonichu.com
spinster.xyz
springbo.cc
starnix.network
stereophonic.space
strelizia.net
syspxl.xyz
tastingtraffic.net
teci.world
theapex.social
thepostearthdestination.com
tkammer.de
trumpislovetrumpis.life
truthsocial.co.in
urchan.org
varishangout.net
whinge.house
whinge.town
wideboys.org
wolfgirl.bar
xn--p1abe3d.xn--80asehdb
yggdrasil.social
youjo.love
zztails.gay

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class ExitActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
}
public static void exit(Context context) {
Intent intent = new Intent(context, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
}
}

View File

@@ -13,6 +13,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -51,9 +52,15 @@ public class ExternalShareActivity extends FragmentStackActivity{
String subject = ""; String subject = "";
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (!subject.isBlank()) builder.append(subject).append("\n\n"); if (!StringUtil.isBlank(subject)) builder.append(subject).append("\n\n");
}
if (intent.hasExtra(Intent.EXTRA_TEXT)) {
String extra = intent.getStringExtra(Intent.EXTRA_TEXT);
if (!StringUtil.isBlank(extra)) {
if (extra.startsWith(subject)) extra = extra.substring(subject.length()).trim();
builder.append(extra).append("\n\n");
}
} }
if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
String text=builder.toString(); String text=builder.toString();
List<Uri> mediaUris; List<Uri> mediaUris;
if(Intent.ACTION_SEND.equals(intent.getAction())){ if(Intent.ACTION_SEND.equals(intent.getAction())){
@@ -80,8 +87,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
args.putString("account", accountID); args.putString("account", accountID);
if(!TextUtils.isEmpty(text)) if(!TextUtils.isEmpty(text))
args.putString("prefilledText", text); args.putString("prefilledText", text);
if(!subject.isBlank()) args.putInt("selectionStart", StringUtil.isBlank(subject) ? 0 : subject.length());
args.putInt("selectionEnd", subject.length());
if(mediaUris!=null && !mediaUris.isEmpty()) if(mediaUris!=null && !mediaUris.isEmpty())
args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris)); args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris));
Fragment fragment=new ComposeFragment(); Fragment fragment=new ComposeFragment();

View File

@@ -8,6 +8,7 @@ import android.content.SharedPreferences;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@@ -46,16 +47,20 @@ public class GlobalUserPreferences{
public static boolean autoHideFab; public static boolean autoHideFab;
public static boolean replyLineAboveHeader; public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine; public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static String publishButtonText; public static String publishButtonText;
public static ThemePreference theme; public static ThemePreference theme;
public static ColorPreference color; public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType(); private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType(); private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
public static Map<String, List<String>> recentLanguages; public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines; public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport; public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> accountsInGlitchMode; public static Set<String> accountsInGlitchMode;
public static Set<String> accountsWithContentTypesEnabled;
public static Map<String, ContentType> accountsDefaultContentTypes;
/** /**
* Pleroma * Pleroma
@@ -102,6 +107,7 @@ public class GlobalUserPreferences{
autoHideFab=prefs.getBoolean("autoHideFab", true); autoHideFab=prefs.getBoolean("autoHideFab", true);
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true); replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true); compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
publishButtonText=prefs.getString("publishButtonText", ""); publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)]; theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>()); recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
@@ -109,6 +115,8 @@ public class GlobalUserPreferences{
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>()); accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>()); accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
replyVisibility=prefs.getString("replyVisibility", null); replyVisibility=prefs.getString("replyVisibility", null);
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
try { try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name())); color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -148,6 +156,7 @@ public class GlobalUserPreferences{
.putString("publishButtonText", publishButtonText) .putString("publishButtonText", publishButtonText)
.putBoolean("bottomEncoding", bottomEncoding) .putBoolean("bottomEncoding", bottomEncoding)
.putBoolean("replyLineAboveHeader", replyLineAboveHeader) .putBoolean("replyLineAboveHeader", replyLineAboveHeader)
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
.putInt("theme", theme.ordinal()) .putInt("theme", theme.ordinal())
.putString("color", color.name()) .putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages)) .putString("recentLanguages", gson.toJson(recentLanguages))
@@ -155,6 +164,8 @@ public class GlobalUserPreferences{
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport) .putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode) .putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.putString("replyVisibility", replyVisibility) .putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.apply(); .apply();
} }

View File

@@ -0,0 +1,49 @@
package org.joinmastodon.android;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class PanicResponderActivity extends Activity {
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
AccountSessionManager.getInstance().getLoggedInAccounts().forEach(accountSession -> logOut(accountSession.getID()));
ExitActivity.exit(this);
}
finishAndRemoveTask();
}
private void logOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
onLoggedOut(accountID);
}
@Override
public void onError(ErrorResponse error){
onLoggedOut(accountID);
}
})
.exec(accountID);
}
private void onLoggedOut(String accountID){
AccountSessionManager.getInstance().removeAccount(accountID);
}
}

View File

@@ -23,6 +23,7 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.NotificationAction; import org.joinmastodon.android.model.NotificationAction;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
@@ -55,6 +56,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
@Override @Override
public void onReceive(Context context, Intent intent){ public void onReceive(Context context, Intent intent){
UiUtils.setUserPreferredTheme(context);
if(BuildConfig.DEBUG){ if(BuildConfig.DEBUG){
Log.e(TAG, "received: "+intent); Log.e(TAG, "received: "+intent);
Bundle extras=intent.getExtras(); Bundle extras=intent.getExtras();
@@ -84,6 +86,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
} }
String accountID=account.getID(); String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s); PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
E.post(new NotificationReceivedEvent(accountID, pn.notificationId+""));
new GetNotificationByID(pn.notificationId+"") new GetNotificationByID(pn.notificationId+"")
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
@@ -185,7 +188,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setShowWhen(true) .setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL) .setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true) .setAutoCancel(true)
.setColor(context.getColor(R.color.shortcut_icon_background)); .setColor(UiUtils.getThemeColor(context, android.R.attr.colorAccent));
if (!GlobalUserPreferences.uniformNotificationIcon) { if (!GlobalUserPreferences.uniformNotificationIcon) {
builder.setSmallIcon(switch (pn.notificationType) { builder.setSmallIcon(switch (pn.notificationType) {

View File

@@ -11,7 +11,7 @@ public class ApiUtils{
//no instance //no instance
} }
public static <E extends Enum<E>> List<String> enumSetToStrings(EnumSet<E> e, Class<E> cls){ public static <E extends Enum<E>> List<String> enumSetToStrings(EnumSet<E> e, Class<E> cls){
return e.stream().map(ev->{ return e.stream().map(ev->{
try{ try{
SerializedName annotation=cls.getField(ev.name()).getAnnotation(SerializedName.class); SerializedName annotation=cls.getField(ev.name()).getAnnotation(SerializedName.class);

View File

@@ -74,10 +74,8 @@ public class CacheController{
int flags=cursor.getInt(1); int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0); status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id; newMaxID=status.id;
for(Filter filter:filters){ if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
if(filter.matches(status)) continue outer;
continue outer;
}
result.add(status); result.add(status);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
String _newMaxID=newMaxID; String _newMaxID=newMaxID;
@@ -92,7 +90,7 @@ public class CacheController{
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false)); callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null); putHomeTimeline(result, maxID==null);
} }
@@ -128,7 +126,7 @@ public class CacheController{
}); });
} }
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){ public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<CacheablePaginatedResponse<List<Notification>>> callback){
cancelDelayedClose(); cancelDelayedClose();
databaseThread.postRunnable(()->{ databaseThread.postRunnable(()->{
try{ try{
@@ -148,15 +146,13 @@ public class CacheController{
ntf.postprocess(); ntf.postprocess();
newMaxID=ntf.id; newMaxID=ntf.id;
if(ntf.status!=null){ if(ntf.status!=null){
for(Filter filter:filters){ if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
if(filter.matches(ntf.status)) continue outer;
continue outer;
}
} }
result.add(ntf); result.add(ntf);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
String _newMaxID=newMaxID; String _newMaxID=newMaxID;
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID))); uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
return; return;
} }
}catch(IOException x){ }catch(IOException x){
@@ -168,16 +164,12 @@ public class CacheController{
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Notification> result){ public void onSuccess(List<Notification> result){
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{ callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){ if(ntf.status!=null){
for(Filter filter:filters){ return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
if(filter.matches(ntf.status)){
return false;
}
}
} }
return true; return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id)); }).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
putNotifications(result, onlyMentions, onlyPosts, maxID==null); putNotifications(result, onlyMentions, onlyPosts, maxID==null);
} }

View File

@@ -28,6 +28,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -51,7 +52,9 @@ public class MastodonAPIController{
.registerTypeAdapter(Status.class, new Status.StatusDeserializer()) .registerTypeAdapter(Status.class, new Status.StatusDeserializer())
.create(); .create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController"); private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder().build(); private static OkHttpClient httpClient=new OkHttpClient.Builder()
.readTimeout(5, TimeUnit.MINUTES)
.build();
private AccountSession session; private AccountSession session;
private static List<String> badDomains = new ArrayList<>(); private static List<String> badDomains = new ArrayList<>();
@@ -60,7 +63,7 @@ public class MastodonAPIController{
thread.start(); thread.start();
try { try {
final BufferedReader reader = new BufferedReader(new InputStreamReader( final BufferedReader reader = new BufferedReader(new InputStreamReader(
MastodonApp.context.getAssets().open("blocks.tsv") MastodonApp.context.getAssets().open("blocks.txt")
)); ));
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
@@ -91,7 +94,7 @@ public class MastodonAPIController{
Request.Builder builder=new Request.Builder() Request.Builder builder=new Request.Builder()
.url(req.getURL().toString()) .url(req.getURL().toString())
.method(req.getMethod(), req.getRequestBody()) .method(req.getMethod(), req.getRequestBody())
.header("User-Agent", "MastodonAndroid/"+BuildConfig.VERSION_NAME); .header("User-Agent", "MegalodonAndroid/"+BuildConfig.VERSION_NAME);
String token=null; String token=null;
if(session!=null) if(session!=null)

View File

@@ -5,8 +5,6 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.R;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{ public class MastodonErrorResponse extends ErrorResponse{
@@ -22,7 +20,7 @@ public class MastodonErrorResponse extends ErrorResponse{
@Override @Override
public void bindErrorView(View view){ public void bindErrorView(View view){
TextView text=view.findViewById(R.id.error_text); TextView text=view.findViewById(me.grishka.appkit.R.id.error_text);
text.setText(error); text.setText(error);
} }

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import java.util.EnumSet;
public class GetMarkers extends MastodonAPIRequest<Markers> {
public GetMarkers(EnumSet<Marker.Type> timelines) {
super(HttpMethod.GET, "/markers", Markers.class);
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
addQueryParameter("timeline[]", type);
}
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.statuses; package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.StatusPrivacy;
@@ -46,6 +47,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public String language; public String language;
public String quoteId; public String quoteId;
public ContentType contentType;
public static class Poll{ public static class Poll{
public ArrayList<String> options=new ArrayList<>(); public ArrayList<String> options=new ArrayList<>();

View File

@@ -2,17 +2,22 @@ package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.AllFieldsAreRequired; import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel; import org.joinmastodon.android.model.BaseModel;
import org.joinmastodon.android.model.ContentType;
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{ public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
public GetStatusSourceText(String id){ public GetStatusSourceText(String id){
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class); super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
} }
@AllFieldsAreRequired
public static class Response extends BaseModel{ public static class Response extends BaseModel{
@RequiredField
public String id; public String id;
@RequiredField
public String text; public String text;
@RequiredField
public String spoilerText; public String spoilerText;
public ContentType contentType;
} }
} }

View File

@@ -7,6 +7,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
@@ -31,6 +32,7 @@ public class AccountSession{
public String pushAccountID; public String pushAccountID;
public Preferences preferences; public Preferences preferences;
public AccountActivationInfo activationInfo; public AccountActivationInfo activationInfo;
public Markers markers;
private transient MastodonAPIController apiController; private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController; private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
private transient CacheController cacheController; private transient CacheController cacheController;

View File

@@ -25,6 +25,7 @@ import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent; import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
@@ -33,6 +34,8 @@ import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
@@ -46,6 +49,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -255,6 +259,7 @@ public class AccountSessionManager{
// if(now-session.filtersLastUpdated>3600_000L){ // if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session); updateSessionWordFilters(session);
// } // }
updateSessionMarkers(session);
} }
if(loadedInstances){ if(loadedInstances){
maybeUpdateCustomEmojis(domains); maybeUpdateCustomEmojis(domains);
@@ -271,6 +276,15 @@ public class AccountSessionManager{
} }
} }
private void preferencesFromSource(AccountSession session, Account account) {
if (account != null && account.source != null && session.preferences != null) {
if (account.source.privacy != null)
session.preferences.postingDefaultVisibility = account.source.privacy;
if (account.source.language != null)
session.preferences.postingDefaultLanguage = account.source.language;
}
}
private void updateSessionLocalInfo(AccountSession session){ private void updateSessionLocalInfo(AccountSession session){
new GetOwnAccount() new GetOwnAccount()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@@ -278,19 +292,12 @@ public class AccountSessionManager{
public void onSuccess(Account result){ public void onSuccess(Account result){
session.self=result; session.self=result;
session.infoLastUpdated=System.currentTimeMillis(); session.infoLastUpdated=System.currentTimeMillis();
if(session.preferences != null && session.preferences.postingDefaultVisibility != null){ preferencesFromSource(session, result);
session.preferences.postingDefaultVisibility = result.source.privacy;
}
if(session.preferences != null && session.preferences.postingDefaultLanguage != null){
session.preferences.postingDefaultLanguage = result.source.language;
}
writeAccountsFile(); writeAccountsFile();
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){}
}
}) })
.exec(session.getID()); .exec(session.getID());
} }
@@ -300,16 +307,13 @@ public class AccountSessionManager{
@Override @Override
public void onSuccess(Preferences preferences) { public void onSuccess(Preferences preferences) {
session.preferences=preferences; session.preferences=preferences;
preferencesFromSource(session, session.self);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
Preferences preferences = new Preferences(); session.preferences = new Preferences();
if(session.self != null){ preferencesFromSource(session, session.self);
preferences.postingDefaultVisibility = session.self.source.privacy;
preferences.postingDefaultLanguage = session.self.source.language;
}
session.preferences = preferences;
} }
}).exec(session.getID()); }).exec(session.getID());
} }
@@ -332,6 +336,21 @@ public class AccountSessionManager{
.exec(session.getID()); .exec(session.getID());
} }
private void updateSessionMarkers(AccountSession session) {
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() {
@Override
public void onSuccess(Markers markers) {
session.markers = markers;
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error) {
}
}).exec(session.getID());
}
public void updateInstanceInfo(String domain){ public void updateInstanceInfo(String domain){
new GetInstance() new GetInstance()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){

View File

@@ -0,0 +1,4 @@
package org.joinmastodon.android.events;
public class AllNotificationsSeenEvent {
}

View File

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

View File

@@ -3,10 +3,6 @@ package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; 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.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -64,7 +60,12 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override @Override
public void onSuccess(List<Status> result){ 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()); AccountSessionManager asm = AccountSessionManager.getInstance();
result=result.stream().filter(status -> {
// don't hide own posts in own profile
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
}).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -122,4 +123,10 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){ protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op // no-op
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
} }

View File

@@ -45,7 +45,6 @@ import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool; import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList; import java.util.ArrayList;
@@ -62,7 +61,6 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -70,7 +68,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab{
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>(); protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter; protected DisplayItemsAdapter adapter;
protected String accountID; protected String accountID;
@@ -84,16 +82,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public BaseStatusListFragment(){ public BaseStatusListFragment(){
super(20); super(20);
if (withComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab); if (wantsComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab);
} }
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return false; return false;
} }
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
UiUtils.loadMaxWidth(getContext());
if(GlobalUserPreferences.disableMarquee){ if(GlobalUserPreferences.disableMarquee){
setTitleMarqueeEnabled(false); setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false); setSubtitleMarqueeEnabled(false);
@@ -102,8 +101,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
setRetainInstance(true); setRetainInstance(true);
} }
@Override @Override
protected RecyclerView.Adapter getAdapter(){ protected RecyclerView.Adapter getAdapter(){
return adapter=new DisplayItemsAdapter(); return adapter=new DisplayItemsAdapter();
@@ -271,37 +268,59 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}); });
} }
@Override
public @Nullable View getFab() {
if (getParentFragment() instanceof HasFab l) return l.getFab();
else return fab;
}
@Override
public void showFab() {
View fab = getFab();
if (fab == null || fab.getVisibility() == View.VISIBLE) return;
fab.setVisibility(View.VISIBLE);
TranslateAnimation animate = new TranslateAnimation(
0,
0,
fab.getHeight() * 2,
0);
animate.setDuration(300);
fab.startAnimation(animate);
}
@Override
public void hideFab() {
View fab = getFab();
if (fab == null || fab.getVisibility() != View.VISIBLE) return;
TranslateAnimation animate = new TranslateAnimation(
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
fab.startAnimation(animate);
fab.setVisibility(View.INVISIBLE);
scrollDiff = 0;
}
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab); fab=view.findViewById(R.id.fab);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){ list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override @Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(currentPhotoViewer!=null) if(currentPhotoViewer!=null)
currentPhotoViewer.offsetView(-dx, -dy); currentPhotoViewer.offsetView(-dx, -dy);
View fab = getFab();
if (fab!=null && GlobalUserPreferences.autoHideFab) { if (fab!=null && GlobalUserPreferences.autoHideFab) {
if (dy > 0 && fab.getVisibility() == View.VISIBLE) { if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
TranslateAnimation animate = new TranslateAnimation( hideFab();
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
fab.startAnimation(animate);
fab.setVisibility(View.INVISIBLE);
scrollDiff = 0;
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) { } else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) { if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) {
fab.setVisibility(View.VISIBLE); showFab();
TranslateAnimation animate = new TranslateAnimation(
0,
0,
fab.getHeight() * 2,
0);
animate.setDuration(300);
fab.startAnimation(animate);
scrollDiff = 0; scrollDiff = 0;
} else { } else {
scrollDiff += Math.abs(dy); scrollDiff += Math.abs(dy);
@@ -344,10 +363,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true); ((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
updateToolbar(); updateToolbar();
if (withComposeButton()) { if (wantsComposeButton() && !getArguments().getBoolean("__disable_fab", false)) {
fab.setVisibility(View.VISIBLE); fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(this::onFabClick); fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(this::onFabLongClick); fab.setOnLongClickListener(this::onFabLongClick);
} else if (fab != null) {
fab.setVisibility(View.GONE);
} }
} }
@@ -656,13 +677,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer.onPause(); currentPhotoViewer.onPause();
} }
protected void onFabClick(View v){ public void onFabClick(View v){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args); Nav.go(getActivity(), ComposeFragment.class, args);
} }
protected boolean onFabLongClick(View v) { public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID); return UiUtils.pickAccountForCompose(getActivity(), accountID);
} }
@@ -759,7 +780,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!imgHolder.getItem().status.spoilerRevealed){ if(!imgHolder.getItem().status.spoilerRevealed){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout(); int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH)); int width=Math.min(listWidth, UiUtils.MAX_WIDTH);
if(currentMediaHiddenLayoutsWidth!=width) if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32)); rebuildMediaHiddenLayouts(width-V.dp(32));
c.save(); c.save();

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses; import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -35,4 +36,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
}) })
.exec(accountID); .exec(accountID);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
} }

View File

@@ -91,6 +91,7 @@ import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
@@ -179,8 +180,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private int charCount, charLimit, trimmedCharCount; private int charCount, charLimit, trimmedCharCount;
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn; private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup; private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss; private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss, contentTypeBtn;
private ImageView sensitiveIcon; private ImageView sensitiveIcon;
private ComposeMediaLayout attachmentsView; private ComposeMediaLayout attachmentsView;
private TextView replyText; private TextView replyText;
@@ -234,6 +235,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private Runnable updateUploadEtaRunnable; private Runnable updateUploadEtaRunnable;
private String language, encoding; private String language, encoding;
private ContentType contentType;
private MastodonLanguage.LanguageResolver languageResolver; private MastodonLanguage.LanguageResolver languageResolver;
@Override @Override
@@ -242,6 +244,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
setRetainInstance(true); setRetainInstance(true);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
if (contentType == null && GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
// if formatting is enabled, use plain to avoid confusing unspecified default setting
contentType = ContentType.PLAIN;
}
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self; self=session.self;
instanceDomain=session.domain; instanceDomain=session.domain;
@@ -330,6 +338,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji); emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler); spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility); visibilityBtn=view.findViewById(R.id.btn_visibility);
contentTypeBtn=view.findViewById(R.id.btn_content_type);
scheduleDraftView=view.findViewById(R.id.schedule_draft_view); scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
scheduleDraftText=view.findViewById(R.id.schedule_draft_text); scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss); scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
@@ -364,6 +373,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityBtn.setOnClickListener(v->visibilityPopup.show()); visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener()); visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
buildContentTypePopup(contentTypeBtn);
contentTypeBtn.setOnClickListener(v->contentTypePopup.show());
contentTypeBtn.setOnTouchListener(contentTypePopup.getDragToOpenListener());
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null)); scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime()); scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
@@ -466,8 +479,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
} }
if(editingStatus!=null && editingStatus.visibility!=null) { if (savedInstanceState != null) {
statusVisibility=editingStatus.visibility; statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
} else if (editingStatus != null && editingStatus.visibility != null) {
statusVisibility = editingStatus.visibility;
} else { } else {
loadDefaultStatusVisibility(savedInstanceState); loadDefaultStatusVisibility(savedInstanceState);
} }
@@ -482,6 +497,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}).setChecked(true); }).setChecked(true);
visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly); visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly);
if (savedInstanceState != null && savedInstanceState.containsKey("contentType")) {
contentType = (ContentType) savedInstanceState.getSerializable("contentType");
} else if (getArguments().containsKey("sourceContentType")) {
try {
String val = getArguments().getString("sourceContentType");
contentType = val == null ? null : ContentType.valueOf(val);
} catch (IllegalArgumentException ignored) {}
}
int contentTypeId = ContentType.getContentTypeRes(contentType);
contentTypePopup.getMenu().findItem(contentTypeId).setChecked(true);
contentTypeBtn.setSelected(contentTypeId != R.id.content_type_null && contentTypeId != R.id.content_type_plain);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID); autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected); autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
View autocompleteView=autocompleteViewController.getView(); View autocompleteView=autocompleteViewController.getView();
@@ -518,6 +547,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putParcelableArrayList("attachments", serializedAttachments); outState.putParcelableArrayList("attachments", serializedAttachments);
} }
outState.putSerializable("visibility", statusVisibility); outState.putSerializable("visibility", statusVisibility);
outState.putSerializable("contentType", contentType);
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt); if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus)); if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
} }
@@ -907,6 +937,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}); });
} }
private int getContentTypeName(String id) {
return switch (id) {
case "text/plain" -> R.string.sk_content_type_plain;
case "text/html" -> R.string.sk_content_type_html;
case "text/markdown" -> R.string.sk_content_type_markdown;
case "text/bbcode" -> R.string.sk_content_type_bbcode;
case "text/x.misskeymarkdown" -> R.string.sk_content_type_mfm;
default -> throw new IllegalArgumentException("Invalid content type");
};
}
private void addBottomLanguage(Menu menu) { private void addBottomLanguage(Menu menu) {
if (menu.findItem(allLanguages.size()) == null) { if (menu.findItem(allLanguages.size()) == null) {
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)"); menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
@@ -1053,6 +1094,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility; req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive; req.sensitive=sensitive;
req.language=language; req.language=language;
req.contentType=contentType;
req.scheduledAt = scheduledAt; req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){ if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList()); req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
@@ -1547,7 +1589,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.isUploadingOrProcessing()) if(att.isUploadingOrProcessing())
att.cancelUpload(); att.cancelUpload();
attachments.remove(att); attachments.remove(att);
uploadNextQueuedAttachment(); if(!areThereAnyUploadingAttachments())
uploadNextQueuedAttachment();
attachmentsView.removeView(att.view); attachmentsView.removeView(att.view);
if(getMediaAttachmentsCount()==0) if(getMediaAttachmentsCount()==0)
attachmentsView.setVisibility(View.GONE); attachmentsView.setVisibility(View.GONE);
@@ -1894,14 +1937,36 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}); });
} }
@SuppressLint("ClickableViewAccessibility")
private void buildContentTypePopup(View btn) {
contentTypePopup=new PopupMenu(getActivity(), btn);
contentTypePopup.inflate(R.menu.compose_content_type);
Menu m = contentTypePopup.getMenu();
ContentType.adaptMenuToInstance(m, instance);
if (contentType != null) m.findItem(R.id.content_type_null).setVisible(false);
contentTypePopup.setOnMenuItemClickListener(i->{
int id=i.getItemId();
if (id == R.id.content_type_null) contentType = null;
else if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
else return false;
btn.setSelected(id != R.id.content_type_null && id != R.id.content_type_plain);
i.setChecked(true);
return true;
});
if (!GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
btn.setVisibility(View.GONE);
}
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState) { private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
if(replyTo != null) 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");
}
AccountSessionManager asm = AccountSessionManager.getInstance(); AccountSessionManager asm = AccountSessionManager.getInstance();
Preferences prefs = asm.getAccount(accountID).preferences; Preferences prefs = asm.getAccount(accountID).preferences;
if (prefs != null) { if (prefs != null) {

View File

@@ -47,11 +47,10 @@ import java.util.Map;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends BaseRecyclerFragment<TimelineDefinition> implements ScrollableToTop { public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition> implements ScrollableToTop {
private String accountID; private String accountID;
private TimelinesAdapter adapter; private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper; private final ItemTouchHelper itemTouchHelper;

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses; import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -35,4 +36,9 @@ public class FavoritedStatusListFragment extends StatusListFragment{
}) })
.exec(accountID); .exec(accountID);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
} }

View File

@@ -38,7 +38,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -47,7 +46,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;

View File

@@ -16,11 +16,10 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop { public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop {
private String nextMaxID; private String nextMaxID;
private String accountId; private String accountId;

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.fragments;
import android.view.View;
public interface HasFab {
View getFab();
void showFab();
void hideFab();
}

View File

@@ -39,7 +39,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
private MenuItem followButton; private MenuItem followButton;
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@@ -123,7 +123,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -138,12 +138,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
} }
@Override @Override
protected boolean onFabLongClick(View v) { public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '); return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
} }
@Override @Override
protected void onFabClick(View v){ public void onFabClick(View v){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putString("prefilledText", '#'+hashtag+' '); args.putString("prefilledText", '#'+hashtag+' ');
@@ -154,4 +154,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
protected void onSetFabBottomInset(int inset){ protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset; ((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@@ -16,21 +16,34 @@ import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.fragments.discover.DiscoverFragment; import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.ui.views.TabBar;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment; import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
@@ -48,6 +61,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private TabBar tabBar; private TabBar tabBar;
private View tabBarWrap; private View tabBarWrap;
private ImageView tabBarAvatar; private ImageView tabBarAvatar;
private ImageView notificationTabIcon;
@IdRes @IdRes
private int currentTab=R.id.tab_home; private int currentTab=R.id.tab_home;
@@ -56,6 +70,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
E.register(this);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name); setTitle(R.string.sk_app_name);
@@ -89,7 +104,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
content.setOrientation(LinearLayout.VERTICAL); content.setOrientation(LinearLayout.VERTICAL);
FrameLayout fragmentContainer=new FrameLayout(getActivity()); FrameLayout fragmentContainer=new FrameLayout(getActivity());
fragmentContainer.setId(R.id.fragment_wrap); fragmentContainer.setId(me.grishka.appkit.R.id.fragment_wrap);
content.addView(fragmentContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); content.addView(fragmentContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
inflater.inflate(R.layout.tab_bar, content); inflater.inflate(R.layout.tab_bar, content);
@@ -108,12 +123,15 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
Account self=AccountSessionManager.getInstance().getAccount(accountID).self; Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28))); ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
notificationTabIcon=content.findViewById(R.id.tab_notifications);
updateNotificationBadge();
if(savedInstanceState==null){ if(savedInstanceState==null){
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(R.id.fragment_wrap, homeTabFragment) .add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
.add(R.id.fragment_wrap, searchFragment).hide(searchFragment) .add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment)
.add(R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment) .add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(R.id.fragment_wrap, profileFragment).hide(profileFragment) .add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit(); .commit();
String defaultTab=getArguments().getString("tab"); String defaultTab=getArguments().getString("tab");
@@ -142,6 +160,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment"); notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment"); profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab"); currentTab=savedInstanceState.getInt("selectedTab");
tabBar.selectTab(currentTab);
Fragment current=fragmentForTab(currentTab); Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.hide(homeTabFragment) .hide(homeTabFragment)
@@ -209,6 +228,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
} }
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit(); getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment); maybeTriggerLoading(newFragment);
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
currentTab=tab; currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
} }
@@ -267,4 +287,47 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment); if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment); if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
} }
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
if (instance == null) return;
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null)
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {
if (notifications.size() > 0) {
try {
long newestId = Long.parseLong(notifications.get(0).id);
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
setNotificationBadge(newestId > lastSeenId);
} catch (Exception ignored) {
setNotificationBadge(false);
}
}
}
@Override
public void onError(ErrorResponse error) {
setNotificationBadge(false);
}
}).exec(accountID);
}
public void setNotificationBadge(boolean badge) {
notificationTabIcon.setImageResource(badge
? R.drawable.ic_fluent_alert_28_selector_badged
: R.drawable.ic_fluent_alert_28_selector);
}
@Subscribe
public void onNotificationReceived(NotificationReceivedEvent notificationReceivedEvent) {
if (notificationReceivedEvent.account.equals(accountID)) setNotificationBadge(true);
}
@Subscribe
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
setNotificationBadge(false);
}
} }

View File

@@ -24,6 +24,7 @@ import android.view.ViewParent;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
@@ -70,7 +71,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener { public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab {
private static final int ANNOUNCEMENTS_RESULT = 654; private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID; private String accountID;
@@ -98,6 +99,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private PopupMenu overflowPopup; private PopupMenu overflowPopup;
private View overflowActionView = null; private View overflowActionView = null;
private boolean announcementsBadged, settingsBadged; private boolean announcementsBadged, settingsBadged;
private ImageButton fab;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -122,6 +124,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override @Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
FrameLayout view = new FrameLayout(getContext()); FrameLayout view = new FrameLayout(getContext());
inflater.inflate(R.layout.compose_fab, view);
fab = view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(this::onFabLongClick);
pager = new ViewPager2(getContext()); pager = new ViewPager2(getContext());
toolbarFrame = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.home_toolbar, getToolbar(), false); toolbarFrame = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.home_toolbar, getToolbar(), false);
@@ -129,6 +135,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putBoolean("__is_tab", true); args.putBoolean("__is_tab", true);
args.putBoolean("__disable_fab", true);
args.putBoolean("onlyPosts", true); args.putBoolean("onlyPosts", true);
for (int i = 0; i < timelineDefinitions.size(); i++) { for (int i = 0; i < timelineDefinitions.size(); i++) {
@@ -280,6 +287,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}).exec(accountID); }).exec(accountID);
} }
private void onFabClick(View v){
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
l.onFabClick(v);
}
}
private boolean onFabLongClick(View v) {
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
return l.onFabLongClick(v);
} else {
return false;
}
}
private void addListsToOverflowMenu() { private void addListsToOverflowMenu() {
Context ctx = getContext(); Context ctx = getContext();
listsMenu.clear(); listsMenu.clear();
@@ -427,9 +448,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
updateSwitcherIcon(i); updateSwitcherIcon(i);
} }
@Override
public void showFab() {
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.showFab();
}
@Override
public void hideFab() {
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
}
private void updateSwitcherIcon(int i) { private void updateSwitcherIcon(int i) {
timelineIcon.setImageResource(timelines[i].getIcon().iconRes); timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext())); timelineTitle.setText(timelines[i].getTitle(getContext()));
showFab();
} }
@Override @Override
@@ -657,6 +689,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return hashtagsItems.values(); return hashtagsItems.values();
} }
public ImageButton getFab() {
return fab;
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> { private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull @NonNull
@Override @Override

View File

@@ -8,8 +8,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences; 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.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -38,7 +36,7 @@ public class HomeTimelineFragment extends StatusListFragment {
private String lastSavedMarkerID; private String lastSavedMarkerID;
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@@ -150,7 +148,7 @@ public class HomeTimelineFragment extends StatusListFragment {
result.get(result.size()-1).hasGapAfter=true; result.get(result.size()-1).hasGapAfter=true;
toAdd=result; toAdd=result;
} }
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME); StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList()); toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){ if(!toAdd.isEmpty()){
prependItems(toAdd, true); prependItems(toAdd, true);
@@ -166,6 +164,10 @@ public class HomeTimelineFragment extends StatusListFragment {
} }
}) })
.exec(accountID); .exec(accountID);
if (parent.getParentFragment() instanceof HomeFragment homeFragment) {
homeFragment.updateNotificationBadge();
}
} }
@Override @Override
@@ -225,7 +227,7 @@ public class HomeTimelineFragment extends StatusListFragment {
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1); List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear(); targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1); List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME); StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
for(Status s:result){ for(Status s:result){
if(idsBelowGap.contains(s.id)) if(idsBelowGap.contains(s.id))
break; break;
@@ -278,4 +280,9 @@ public class HomeTimelineFragment extends StatusListFragment {
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){ protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true; return true;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
}
} }

View File

@@ -44,7 +44,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
private ListTimeline.RepliesPolicy repliesPolicy; private ListTimeline.RepliesPolicy repliesPolicy;
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@@ -137,7 +137,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override @Override
public void onSuccess(List<Status> result) { public void onSuccess(List<Status> result) {
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -152,7 +152,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
} }
@Override @Override
protected void onFabClick(View v){ public void onFabClick(View v){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args); Nav.go(getActivity(), ComposeFragment.class, args);
@@ -162,4 +162,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
protected void onSetFabBottomInset(int inset) { protected void onSetFabBottomInset(int inset) {
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset; ((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
}
} }

View File

@@ -37,11 +37,10 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop { public class ListTimelinesFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop {
private String accountId; private String accountId;
private String profileAccountId; private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>(); private final HashMap<String, Boolean> userInListBefore = new HashMap<>();

View File

@@ -11,17 +11,20 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers; import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -46,8 +49,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS); private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return false;
} }
@Override @Override
@@ -81,6 +84,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected List<StatusDisplayItem> buildDisplayItems(Notification n){ protected List<StatusDisplayItem> buildDisplayItems(Notification n){
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null : Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null :
n.report.targetAccount; n.report.targetAccount;
Emoji emoji = new Emoji();
if(n.emojiUrl!=null){
emoji.shortcode=n.emoji.substring(1,n.emoji.length()-1);
emoji.url=n.emojiUrl;
emoji.staticUrl=n.emojiUrl;
emoji.visibleInPicker=false;
}
String extraText=switch(n.type){ String extraText=switch(n.type){
case FOLLOW -> getString(R.string.user_followed_you); case FOLLOW -> getString(R.string.user_followed_you);
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request); case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
@@ -91,8 +101,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
case UPDATE -> getString(R.string.sk_post_edited); case UPDATE -> getString(R.string.sk_post_edited);
case SIGN_UP -> getString(R.string.sk_signed_up); case SIGN_UP -> getString(R.string.sk_signed_up);
case REPORT -> getString(R.string.sk_reported); case REPORT -> getString(R.string.sk_reported);
case REACTION, PLEROMA_EMOJI_REACTION ->
n.emoji != null ? getString(R.string.sk_reacted_with, n.emoji) : getString(R.string.sk_reacted);
}; };
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null; HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, n.emojiUrl!=null ? HtmlParser.parseCustomEmoji(extraText, Collections.singletonList(emoji)) : extraText, n, null) : null;
if(n.status!=null){ if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS); ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
if(titleItem!=null) if(titleItem!=null)
@@ -127,7 +139,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
.getAccount(accountID).getCacheController() .getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){ .getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
@Override @Override
public void onSuccess(PaginatedResponse<List<Notification>> result){ public void onSuccess(CacheablePaginatedResponse<List<Notification>> result){
if (getActivity() == null) return; if (getActivity() == null) return;
if(refreshing) if(refreshing)
relationships.clear(); relationships.clear();
@@ -139,8 +151,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
loadRelationships(needRelationships); loadRelationships(needRelationships);
maxID=result.maxID; maxID=result.maxID;
if(offset==0 && !result.items.isEmpty()){ if(offset==0 && !result.items.isEmpty() && !result.isFromCache()){
E.post(new AllNotificationsSeenEvent());
new SaveMarkers(null, result.items.get(0).id).exec(accountID); new SaveMarkers(null, result.items.get(0).id).exec(accountID);
if (AccountSessionManager.getInstance().getAccount(accountID).markers != null)
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
} }
} }
}); });
@@ -190,7 +207,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this)); list.addItemDecoration(new InsetStatusItemDecoration(this));
if (getParentFragment() instanceof NotificationsFragment) fab.setVisibility(View.GONE);
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap); if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
} }

View File

@@ -337,7 +337,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){ public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.onSelectedChanged(viewHolder, actionState); super.onSelectedChanged(viewHolder, actionState);
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){ 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.setTag(me.grishka.appkit.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(); viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=viewHolder; draggedViewHolder=viewHolder;
} }

View File

@@ -19,13 +19,11 @@ import android.os.Bundle;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ImageSpan; import android.text.style.ImageSpan;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu; import android.view.SubMenu;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -112,7 +110,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{ public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{
private static final int AVATAR_RESULT=722; private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343; private static final int COVER_RESULT=343;
@@ -152,7 +150,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private WindowInsets childInsets; private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer; private PhotoViewer currentPhotoViewer;
private boolean editModeLoading; private boolean editModeLoading;
protected int scrollDiff = 0;
private static final int MAX_FIELDS=4; private static final int MAX_FIELDS=4;
@@ -439,6 +436,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
toolbarTitleView.setTranslationY(titleTransY); toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY); toolbarSubtitleView.setTranslationY(titleTransY);
} }
RecyclerFragment.setRefreshLayoutColors(refreshLayout);
} }
@Override @Override
@@ -760,6 +758,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return fab; return fab;
} }
@Override
public void showFab() {
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.showFab();
}
@Override
public void hideFab() {
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
int topBarsH=getToolbar().getHeight()+statusBarHeight; int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatarBorder.getTop()-topBarsH){ if(scrollY>avatarBorder.getTop()-topBarsH){
@@ -790,35 +798,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(currentPhotoViewer!=null){ if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY); 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);
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);
fab.startAnimation(animate);
scrollDiff = 0;
} else {
scrollDiff += Math.abs(dy);
}
}
}
} }
private Fragment getFragmentForPage(int page){ private Fragment getFragmentForPage(int page){
@@ -1374,7 +1353,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){ public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.onSelectedChanged(viewHolder, actionState); super.onSelectedChanged(viewHolder, actionState);
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){ 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.setTag(me.grishka.appkit.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(); viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=viewHolder; draggedViewHolder=viewHolder;
} }

View File

@@ -0,0 +1,50 @@
package org.joinmastodon.android.fragments;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
public abstract class RecyclerFragment<T> extends BaseRecyclerFragment<T> {
public RecyclerFragment(int perPage) {
super(perPage);
}
public RecyclerFragment(int layout, int perPage) {
super(layout, perPage);
}
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (refreshLayout != null) setRefreshLayoutColors(refreshLayout);
}
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
List<Integer> colors = new ArrayList<>(Arrays.asList(
R.color.primary_600,
R.color.red_primary_600,
R.color.green_primary_600,
R.color.blue_primary_600,
R.color.purple_600
));
int primary = UiUtils.getThemeColorRes(l.getContext(), R.attr.colorPrimary600);
if (!colors.contains(primary)) colors.add(0, primary);
int offset = colors.indexOf(primary);
int[] sorted = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
sorted[i] = colors.get((i + offset) % colors.size());
}
l.setColorSchemeResources(sorted);
}
}

View File

@@ -31,7 +31,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
private static final int SCHEDULED_STATUS_LIST_OPENED = 161; private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@@ -56,7 +56,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
} }
@Override @Override
protected void onFabClick(View v) { public void onFabClick(View v) {
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant()); args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
@@ -64,7 +64,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
} }
@Override @Override
protected boolean onFabLongClick(View v) { public boolean onFabLongClick(View v) {
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant()); args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
@@ -79,7 +79,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) { protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true); return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
} }
@Override @Override
@@ -96,6 +96,8 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
args.putString("sourceText", status.text); args.putString("sourceText", status.text);
args.putString("sourceSpoiler", status.spoilerText); args.putString("sourceSpoiler", status.spoilerText);
args.putBoolean("redraftStatus", true); args.putBoolean("redraftStatus", true);
args.putString("sourceContentType", scheduledStatus.params.contentType != null ?
scheduledStatus.params.contentType.name() : null);
setResult(true, null); setResult(true, null);
// closing this scheduled status list if another status list is opened from compose fragment // closing this scheduled status list if another status list is opened from compose fragment

View File

@@ -14,6 +14,7 @@ import android.util.LruCache;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -48,6 +49,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent; import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment; import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment; import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.PushNotification; import org.joinmastodon.android.model.PushNotification;
@@ -64,6 +66,7 @@ import java.util.function.Consumer;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -80,7 +83,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ArrayList<Item> items=new ArrayList<>(); private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem; private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem; private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem; private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
private ButtonItem defaultContentTypeButtonItem;
private String accountID; private String accountID;
private boolean needUpdateNotificationSettings; private boolean needUpdateNotificationSettings;
private boolean needAppRestart; private boolean needAppRestart;
@@ -89,7 +93,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ImageView themeTransitionWindowView; private ImageView themeTransitionWindowView;
private TextItem checkForUpdateItem, clearImageCacheItem; private TextItem checkForUpdateItem, clearImageCacheItem;
private ImageCache imageCache; private ImageCache imageCache;
private Menu contentTypeMenu;
@SuppressLint("ClickableViewAccessibility")
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -207,6 +213,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.prefixRepliesWithRe=i.checked; GlobalUserPreferences.prefixRepliesWithRe=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
})); }));
items.add(new SwitchItem(R.string.sk_settings_confirm_before_reblog, R.drawable.ic_fluent_checkmark_circle_24_regular, GlobalUserPreferences.confirmBeforeReblog, i->{
GlobalUserPreferences.confirmBeforeReblog=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.sk_timelines)); 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->{ items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
@@ -235,15 +245,15 @@ public class SettingsFragment extends MastodonToolbarFragment{
})); }));
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{ 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; GlobalUserPreferences.loadNewPosts=i.checked;
showNewPostsButtonItem.enabled = i.checked; showNewPostsItem.enabled = i.checked;
if (!i.checked) { if (!i.checked) {
GlobalUserPreferences.showNewPostsButton = false; GlobalUserPreferences.showNewPostsButton = false;
showNewPostsButtonItem.checked = false; showNewPostsItem.checked = false;
} }
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsButtonItem)) instanceof SwitchViewHolder svh) svh.rebind(); if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsItem)) instanceof SwitchViewHolder svh) svh.rebind();
GlobalUserPreferences.save(); 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->{ items.add(showNewPostsItem = 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.showNewPostsButton=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
})); }));
@@ -326,6 +336,36 @@ public class SettingsFragment extends MastodonToolbarFragment{
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version))); 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 HeaderItem(R.string.sk_instance_features));
items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{
if (i.checked) {
GlobalUserPreferences.accountsWithContentTypesEnabled.add(accountID);
if (GlobalUserPreferences.accountsDefaultContentTypes.get(accountID) == null) {
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, ContentType.PLAIN);
}
} else {
GlobalUserPreferences.accountsWithContentTypesEnabled.remove(accountID);
GlobalUserPreferences.accountsDefaultContentTypes.remove(accountID);
}
if (list.findViewHolderForAdapterPosition(items.indexOf(defaultContentTypeButtonItem))
instanceof ButtonViewHolder bvh) bvh.rebind();
GlobalUserPreferences.save();
}));
items.add(new SmallTextItem(getString(R.string.sk_settings_content_types_explanation)));
items.add(defaultContentTypeButtonItem = new ButtonItem(R.string.sk_settings_default_content_type, 0, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.compose_content_type);
popupMenu.setOnMenuItemClickListener(item -> this.onContentTypeChanged(item, b));
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
ContentType contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
b.setText(getContentTypeString(contentType));
contentTypeMenu = popupMenu.getMenu();
contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true);
ContentType.adaptMenuToInstance(contentTypeMenu, instance);
contentTypeMenu.findItem(R.id.content_type_null).setVisible(
!GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID));
}));
items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation)));
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{ items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
glitchModeItem.enabled = i.checked; glitchModeItem.enabled = i.checked;
if (i.checked) { if (i.checked) {
@@ -496,6 +536,34 @@ public class SettingsFragment extends MastodonToolbarFragment{
} }
} }
private @StringRes int getContentTypeString(@Nullable ContentType contentType) {
if (contentType == null) return R.string.sk_content_type_unspecified;
return switch (contentType) {
case PLAIN -> R.string.sk_content_type_plain;
case HTML -> R.string.sk_content_type_html;
case MARKDOWN -> R.string.sk_content_type_markdown;
case BBCODE -> R.string.sk_content_type_bbcode;
case MISSKEY_MARKDOWN -> R.string.sk_content_type_mfm;
};
}
private boolean onContentTypeChanged(MenuItem item, Button btn){
int id = item.getItemId();
ContentType contentType = switch (id) {
case R.id.content_type_plain -> ContentType.PLAIN;
case R.id.content_type_html -> ContentType.HTML;
case R.id.content_type_markdown -> ContentType.MARKDOWN;
case R.id.content_type_bbcode -> ContentType.BBCODE;
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
default -> null;
};
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
GlobalUserPreferences.save();
btn.setText(getContentTypeString(contentType));
item.setChecked(true);
return true;
}
private boolean onReplyVisibilityChanged(MenuItem item, Button btn){ private boolean onReplyVisibilityChanged(MenuItem item, Button btn){
String pref = null; String pref = null;
int id = item.getItemId(); int id = item.getItemId();
@@ -1003,7 +1071,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
@Override @Override
public void onBind(ButtonItem item){ public void onBind(ButtonItem item){
text.setText(item.text); text.setText(item.text);
icon.setImageResource(item.icon); if (item.icon == 0) {
icon.setVisibility(View.GONE);
} else {
icon.setImageResource(item.icon);
}
item.buttonConsumer.accept(button); item.buttonConsumer.accept(button);
} }
} }

View File

@@ -1,43 +1,47 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.graphics.Canvas; import android.graphics.drawable.ColorDrawable;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.LinearLayout; import android.widget.Button;
import android.widget.TextView;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment; import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout; import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import org.parceler.Parcels;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class SplashFragment extends AppKitFragment{ public class SplashFragment extends AppKitFragment{
private static final String DEFAULT_SERVER="mastodon.social";
private SizeListenerFrameLayout contentView; private SizeListenerFrameLayout contentView;
private View artContainer, blueFill, greenFill; private View artContainer, blueFill, greenFill;
private ViewPager2 pager; private InterpolatingMotionEffect motionEffect;
private ViewGroup pagerDots;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill; private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
} }
@Nullable @Nullable
@@ -46,44 +50,26 @@ public class SplashFragment extends AppKitFragment{
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false); contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick); contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick); contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
Button joinDefault=contentView.findViewById(R.id.btn_join_default_server);
joinDefault.setText(getString(R.string.join_default_server, DEFAULT_SERVER));
joinDefault.setOnClickListener(this::onJoinDefaultServerClick);
contentView.findViewById(R.id.btn_learn_more).setOnClickListener(this::onLearnMoreClick);
artClouds=contentView.findViewById(R.id.art_clouds); artClouds=contentView.findViewById(R.id.art_clouds);
artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant); artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant);
artRightHill=contentView.findViewById(R.id.art_right_hill); artRightHill=contentView.findViewById(R.id.art_right_hill);
artLeftHill=contentView.findViewById(R.id.art_left_hill); artLeftHill=contentView.findViewById(R.id.art_left_hill);
artCenterHill=contentView.findViewById(R.id.art_center_hill); artCenterHill=contentView.findViewById(R.id.art_center_hill);
pager=contentView.findViewById(R.id.pager);
pagerDots=contentView.findViewById(R.id.pager_dots);
pager.setAdapter(new PagerAdapter());
pager.setOffscreenPageLimit(3);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
for(int i=0;i<pagerDots.getChildCount();i++){
float alpha;
if(i==position){
alpha=0.3f+0.7f*(1f-positionOffset);
}else if(i==position+1){
alpha=0.3f+0.7f*positionOffset;
}else{
alpha=0.3f;
}
pagerDots.getChildAt(i).setAlpha(alpha);
}
float parallaxProgress=(position+positionOffset)/2f;
artClouds.setTranslationX(V.dp(-27)*(position>=1 ? 1f : positionOffset));
artPlaneElephant.setTranslationX(V.dp(101.55f)*parallaxProgress);
artLeftHill.setTranslationX(V.dp(-88)*parallaxProgress);
artLeftHill.setTranslationY(V.dp(24)*parallaxProgress);
artRightHill.setTranslationX(V.dp(-88)*parallaxProgress);
artRightHill.setTranslationY(V.dp(-24)*parallaxProgress);
artCenterHill.setTranslationX(V.dp(-40)*parallaxProgress);
}
});
artContainer=contentView.findViewById(R.id.art_container); artContainer=contentView.findViewById(R.id.art_container);
blueFill=contentView.findViewById(R.id.blue_fill); blueFill=contentView.findViewById(R.id.blue_fill);
greenFill=contentView.findViewById(R.id.green_fill); greenFill=contentView.findViewById(R.id.green_fill);
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artClouds, V.dp(-5), V.dp(5), V.dp(-5), V.dp(5)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artRightHill, V.dp(-15), V.dp(25), V.dp(-10), V.dp(10)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artLeftHill, V.dp(-25), V.dp(15), V.dp(-15), V.dp(15)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artCenterHill, V.dp(-14), V.dp(14), V.dp(-5), V.dp(25)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artPlaneElephant, V.dp(-20), V.dp(12), V.dp(-20), V.dp(12)));
artContainer.setOnTouchListener(motionEffect);
contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){ contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){
@Override @Override
@@ -109,6 +95,38 @@ public class SplashFragment extends AppKitFragment{
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras); Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
} }
private void onJoinDefaultServerClick(View v){
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(DEFAULT_SERVER);
}
private void onLearnMoreClick(View v){
View sheetView=getActivity().getLayoutInflater().inflate(R.layout.intro_bottom_sheet, null);
BottomSheet sheet=new BottomSheet(getActivity());
sheet.setContentView(sheetView);
sheet.setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface),
UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
sheet.show();
}
private void updateArtSize(int w, int h){ private void updateArtSize(int w, int h){
float scale=w/(float)V.dp(360); float scale=w/(float)V.dp(360);
artContainer.setScaleX(scale); artContainer.setScaleX(scale);
@@ -139,60 +157,15 @@ public class SplashFragment extends AppKitFragment{
return true; return true;
} }
private class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder>{ @Override
protected void onShown(){
@NonNull super.onShown();
@Override motionEffect.activate();
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new PagerViewHolder(viewType);
}
@Override
public void onBindViewHolder(@NonNull PagerViewHolder holder, int position){}
@Override
public int getItemCount(){
return 3;
}
@Override
public int getItemViewType(int position){
return position;
}
} }
private class PagerViewHolder extends RecyclerView.ViewHolder{ @Override
public PagerViewHolder(int page){ protected void onHidden(){
super(new LinearLayout(getActivity())); super.onHidden();
LinearLayout ll=(LinearLayout) itemView; motionEffect.deactivate();
ll.setOrientation(LinearLayout.VERTICAL);
int pad=V.dp(16);
ll.setPadding(pad, pad, pad, pad);
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
TextView title=new TextView(getActivity());
title.setTextAppearance(R.style.m3_headline_medium);
title.setText(switch(page){
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);
});
title.setTextColor(0xFF17063B);
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(page==0 ? 46 : 36));
lp.bottomMargin=V.dp(page==0 ? 4 : 14);
ll.addView(title, lp);
TextView text=new TextView(getActivity());
text.setTextAppearance(R.style.m3_body_medium);
text.setText(switch(page){
case 0 -> R.string.welcome_page1_text;
case 1 -> R.string.welcome_page2_text;
case 2 -> R.string.welcome_page3_text;
default -> throw new IllegalStateException("Unexpected value: "+page);
});
text.setTextColor(0xFF17063B);
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
} }
} }

View File

@@ -6,6 +6,7 @@ import android.view.View;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory; import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -55,7 +56,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null); List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null);
int idx=data.indexOf(s); int idx=data.indexOf(s);
if(idx>=0){ if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -156,4 +157,9 @@ public class StatusEditHistoryFragment extends StatusListFragment{
public boolean isItemEnabled(String id){ public boolean isItemEnabled(String id){
return false; return false;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return null;
}
} }

View File

@@ -34,9 +34,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
boolean addFooter = !GlobalUserPreferences.spectatorMode || boolean addFooter = !GlobalUserPreferences.spectatorMode ||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id)); (this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME); return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
} }
protected abstract Filter.FilterContext getFilterContext();
@Override @Override
protected void addAccountToKnown(Status s){ protected void addAccountToKnown(Status s){
if(!knownAccounts.containsKey(s.account.id)) if(!knownAccounts.containsKey(s.account.id))
@@ -151,7 +153,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){ protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
List<Status> toRemove=Stream.concat(data.stream(), preloadedData.stream()) List<Status> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(s->s.account.id.equals(ev.postsByAccountID) || (s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID))) .filter(s->s.account.id.equals(ev.postsByAccountID) || (!ev.isUnfollow && s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID)))
.collect(Collectors.toList()); .collect(Collectors.toList());
for(Status s:toRemove){ for(Status s:toRemove){
removeStatus(s); removeStatus(s);

View File

@@ -137,7 +137,7 @@ public class ThreadFragment extends StatusListFragment{
} }
private List<Status> filterStatuses(List<Status> statuses){ private List<Status> filterStatuses(List<Status> statuses){
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD); StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
return statuses.stream() return statuses.stream()
.filter(statusFilterPredicate) .filter(statusFilterPredicate)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -181,4 +181,10 @@ public class ThreadFragment extends StatusListFragment{
public boolean wantsLightNavigationBar(){ public boolean wantsLightNavigationBar(){
return !UiUtils.isDarkTheme(); return !UiUtils.isDarkTheme();
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.THREAD;
}
} }

View File

@@ -25,6 +25,7 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ListTimelinesFragment; import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
@@ -48,7 +49,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -57,7 +57,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseAccountListFragment.AccountItem>{ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> {
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID; protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>(); protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -295,7 +295,6 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername())); menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername())); menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername())).setVisible(relationship.following);
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following); menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts); MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists); MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
@@ -309,7 +308,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
manageUserLists.setVisible(true); manageUserLists.setVisible(true);
}else{ }else{
hideBoosts.setVisible(false); hideBoosts.setVisible(false);
manageUserLists.setVisible(true); manageUserLists.setVisible(false);
} }
menu.findItem(R.id.block_domain).setVisible(false); menu.findItem(R.id.block_domain).setVisible(false);

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions; import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.IsOnTop; import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowSuggestion; import org.joinmastodon.android.model.FollowSuggestion;
@@ -40,7 +41,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -49,7 +49,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop { public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop {
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;

View File

@@ -11,6 +11,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks; import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.IsOnTop; import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card; import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
@@ -26,7 +27,6 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -35,7 +35,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop, IsOnTop { public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop {
private String accountID; private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList(); private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS); private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);

View File

@@ -26,7 +26,7 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}).exec(accountID); }).exec(accountID);
@@ -42,4 +42,10 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
public boolean isOnTop() { public boolean isOnTop() {
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@@ -21,7 +21,7 @@ public class FederatedTimelineFragment extends StatusListFragment {
private String maxID; private String maxID;
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@@ -35,7 +35,7 @@ public class FederatedTimelineFragment extends StatusListFragment {
if(!result.isEmpty()) if(!result.isEmpty())
maxID=result.get(result.size()-1).id; maxID=result.get(result.size()-1).id;
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -47,4 +47,9 @@ public class FederatedTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(contentWrap);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@@ -20,7 +20,7 @@ public class LocalTimelineFragment extends StatusListFragment {
private String maxID; private String maxID;
@Override @Override
protected boolean withComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@@ -34,7 +34,7 @@ public class LocalTimelineFragment extends StatusListFragment {
if(!result.isEmpty()) if(!result.isEmpty())
maxID=result.get(result.size()-1).id; maxID=result.get(result.size()-1).id;
if (getActivity() == null) return; if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -46,4 +46,9 @@ public class LocalTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(contentWrap);
} }
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
} }

View File

@@ -15,6 +15,7 @@ import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults; import org.joinmastodon.android.model.SearchResults;
@@ -80,7 +81,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
return switch(s.type){ return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)); case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)); case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null); case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.PUBLIC);
}; };
} }
@@ -247,7 +248,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
} }
public void setQuery(String q){ public void setQuery(String q){
if(Objects.equals(q, currentQuery)) if(Objects.equals(q, currentQuery) || q.isBlank())
return; return;
if(currentRequest!=null){ if(currentRequest!=null){
currentRequest.cancel(); currentRequest.cancel();

View File

@@ -8,6 +8,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags; import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
import org.joinmastodon.android.fragments.IsOnTop; import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
@@ -20,11 +21,10 @@ import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop { public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop {
private String accountID; private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS); private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -44,7 +45,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
@@ -52,7 +52,7 @@ import okhttp3.Call;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{ abstract class InstanceCatalogFragment extends RecyclerFragment<CatalogInstance> {
protected RecyclerView.Adapter adapter; protected RecyclerView.Adapter adapter;
protected MergeRecyclerAdapter mergeAdapter; protected MergeRecyclerAdapter mergeAdapter;
protected CatalogInstance chosenInstance; protected CatalogInstance chosenInstance;
@@ -75,7 +75,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
private static final double DUNBAR=Math.log(800); private static final double DUNBAR=Math.log(800);
public InstanceCatalogFragment(int layout, int perPage){ public InstanceCatalogFragment(int layout, int perPage){
super(layout, perPage); super(layout, perPage);
} }
@Override @Override

View File

@@ -21,6 +21,7 @@ import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.fragments.HomeFragment; import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.model.FollowSuggestion; import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.ParsedAccount; import org.joinmastodon.android.model.ParsedAccount;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
@@ -43,7 +44,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -52,7 +52,7 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout; import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<ParsedAccount> {
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;

View File

@@ -21,6 +21,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.events.FinishReportFragmentsEvent; import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
@@ -261,4 +262,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected boolean wantsOverlaySystemNavigation(){ protected boolean wantsOverlaySystemNavigation(){
return false; return false;
} }
@Override
protected Filter.FilterContext getFilterContext() {
return null;
}
} }

View File

@@ -0,0 +1,40 @@
package org.joinmastodon.android.model;
import android.view.Menu;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.R;
public enum ContentType {
@SerializedName("text/plain")
PLAIN,
@SerializedName("text/html")
HTML,
@SerializedName("text/markdown")
MARKDOWN,
@SerializedName("text/bbcode")
BBCODE, // akkoma
@SerializedName("text/x.misskeymarkdown")
MISSKEY_MARKDOWN; // akkoma/*key
public static int getContentTypeRes(@Nullable ContentType contentType) {
return contentType == null ? R.id.content_type_null : switch(contentType) {
case PLAIN -> R.id.content_type_plain;
case HTML -> R.id.content_type_html;
case MARKDOWN -> R.id.content_type_markdown;
case BBCODE -> R.id.content_type_bbcode;
case MISSKEY_MARKDOWN -> R.id.content_type_misskey_markdown;
};
}
public static void adaptMenuToInstance(Menu m, Instance i) {
if (i.pleroma == null) {
// memo: change this if glitch or another mastodon fork supports bbcode or mfm
m.findItem(R.id.content_type_bbcode).setVisible(false);
m.findItem(R.id.content_type_misskey_markdown).setVisible(false);
}
}
}

View File

@@ -2,22 +2,22 @@ package org.joinmastodon.android.model;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel; import org.parceler.Parcel;
import java.time.Instant; import java.time.Instant;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream;
@Parcel @Parcel
public class Filter extends BaseModel{ public class Filter extends BaseModel{
@RequiredField
public String id; public String id;
@RequiredField
public String phrase; public String phrase;
public String title; public String title;
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class); public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);

View File

@@ -1,8 +1,15 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.parceler.Parcel; import org.parceler.Parcel;
@Parcel @Parcel
public class FilterResult extends BaseModel { public class FilterResult extends BaseModel {
public Filter filter; public Filter filter;
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if (filter != null) filter.postprocess();
}
} }

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.AllFieldsAreRequired; import org.joinmastodon.android.api.AllFieldsAreRequired;
import java.time.Instant; import java.time.Instant;
@@ -18,4 +20,11 @@ public class Marker extends BaseModel{
", updatedAt="+updatedAt+ ", updatedAt="+updatedAt+
'}'; '}';
} }
public enum Type {
@SerializedName("home")
HOME,
@SerializedName("notifications")
NOTIFICATIONS
}
} }

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.model;
public class Markers {
public Marker notifications;
public Marker home;
@Override
public String toString() {
return "Markers{" +
"notifications=" + notifications +
", home=" + home +
'}';
}
}

View File

@@ -20,6 +20,8 @@ public class Notification extends BaseModel implements DisplayItemsParent{
public Account account; public Account account;
public Status status; public Status status;
public Report report; public Report report;
public String emoji;
public String emojiUrl;
@Override @Override
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
@@ -51,6 +53,10 @@ public class Notification extends BaseModel implements DisplayItemsParent{
STATUS, STATUS,
@SerializedName("update") @SerializedName("update")
UPDATE, UPDATE,
@SerializedName("reaction")
REACTION,
@SerializedName("pleroma:emoji_reaction")
PLEROMA_EMOJI_REACTION,
@SerializedName("admin.sign_up") @SerializedName("admin.sign_up")
SIGN_UP, SIGN_UP,
@SerializedName("admin.report") @SerializedName("admin.report")

View File

@@ -39,6 +39,7 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public String idempotency; public String idempotency;
public String applicationId; public String applicationId;
public List<String> mediaIds; public List<String> mediaIds;
public ContentType contentType;
} }
@Parcel @Parcel

View File

@@ -74,6 +74,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public transient boolean spoilerRevealed; public transient boolean spoilerRevealed;
public transient boolean textExpanded, textExpandable; public transient boolean textExpanded, textExpandable;
public transient boolean hasGapAfter; public transient boolean hasGapAfter;
public transient TranslatedStatus translation;
public transient boolean translationShown;
private transient String strippedText; private transient String strippedText;
@Override @Override
@@ -99,6 +101,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
card.postprocess(); card.postprocess();
if(reblog!=null) if(reblog!=null)
reblog.postprocess(); reblog.postprocess();
if(filtered!=null)
for(FilterResult fr : filtered)
fr.postprocess();
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive; spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true; if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;

View File

@@ -1,17 +1,25 @@
package org.joinmastodon.android.ui; package org.joinmastodon.android.ui;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.hardware.Sensor; import android.hardware.Sensor;
import android.hardware.SensorEvent; import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.view.MotionEvent;
import android.view.Surface; import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.PathInterpolator;
import java.util.ArrayList; import java.util.ArrayList;
public class InterpolatingMotionEffect implements SensorEventListener{ import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
public class InterpolatingMotionEffect implements SensorEventListener, View.OnTouchListener{
private SensorManager sm; private SensorManager sm;
private WindowManager wm; private WindowManager wm;
@@ -20,6 +28,34 @@ public class InterpolatingMotionEffect implements SensorEventListener{
private Sensor accelerometer; private Sensor accelerometer;
private boolean accelerometerEnabled; private boolean accelerometerEnabled;
private ArrayList<ViewEffect> views=new ArrayList<>(); private ArrayList<ViewEffect> views=new ArrayList<>();
private float pitch, roll;
private float touchDownX, touchDownY, touchAddX, touchAddY, touchAddLastAnimX, touchAddLastAnimY;
private PathInterpolator touchInterpolator=new PathInterpolator(0.5f, 1f, 0.89f, 1f);
private SpringAnimation touchSpringX, touchSpringY;
private FloatValueHolder touchSpringXHolder=new FloatValueHolder(){
@Override
public float getValue(){
return touchAddX;
}
@Override
public void setValue(float value){
touchAddX=value;
updateEffects();
}
};
private FloatValueHolder touchSpringYHolder=new FloatValueHolder(){
@Override
public float getValue(){
return touchAddY;
}
@Override
public void setValue(float value){
touchAddY=value;
updateEffects();
}
};
public InterpolatingMotionEffect(Context context){ public InterpolatingMotionEffect(Context context){
sm=context.getSystemService(SensorManager.class); sm=context.getSystemService(SensorManager.class);
@@ -50,8 +86,8 @@ public class InterpolatingMotionEffect implements SensorEventListener{
float z=event.values[2]/SensorManager.GRAVITY_EARTH; float z=event.values[2]/SensorManager.GRAVITY_EARTH;
float pitch=(float) (Math.atan2(x, Math.sqrt(y*y+z*z))/Math.PI*2.0); pitch=(float) (Math.atan2(x, Math.sqrt(y*y+z*z))/Math.PI*2.0);
float roll=(float) (Math.atan2(y, Math.sqrt(x*x+z*z))/Math.PI*2.0); roll=(float) (Math.atan2(y, Math.sqrt(x*x+z*z))/Math.PI*2.0);
switch(rotation){ switch(rotation){
case Surface.ROTATION_0: case Surface.ROTATION_0:
@@ -88,9 +124,7 @@ public class InterpolatingMotionEffect implements SensorEventListener{
}else if(roll<-1f){ }else if(roll<-1f){
roll=-2f-roll; roll=-2f-roll;
} }
for(ViewEffect view:views){ updateEffects();
view.update(pitch, roll);
}
} }
@Override @Override
@@ -110,6 +144,62 @@ public class InterpolatingMotionEffect implements SensorEventListener{
views.clear(); views.clear();
} }
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent ev){
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN -> {
if(touchSpringX!=null){
touchAddLastAnimX=touchAddX;
touchSpringX.cancel();
touchSpringX=null;
}else{
touchAddLastAnimX=0;
}
if(touchSpringY!=null){
touchAddLastAnimY=touchAddY;
touchSpringY.cancel();
touchSpringY=null;
}else{
touchAddLastAnimY=0;
}
touchDownX=ev.getX();
touchDownY=ev.getY();
}
case MotionEvent.ACTION_MOVE -> {
touchAddX=touchInterpolator.getInterpolation(Math.min(1f, Math.abs((ev.getX()-touchDownX)/(v.getWidth()/2f))));
touchAddY=touchInterpolator.getInterpolation(Math.min(1f, Math.abs((ev.getY()-touchDownY)/(v.getHeight()/2f))));
if(ev.getX()>touchDownX)
touchAddX=-touchAddX;
if(ev.getY()<touchDownY)
touchAddY=-touchAddY;
touchAddX+=touchAddLastAnimX;
touchAddY+=touchAddLastAnimY;
updateEffects();
}
case MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
touchSpringX=new SpringAnimation(touchSpringXHolder, 0f);
touchSpringX.setMinimumVisibleChange(0.01f);
touchSpringX.getSpring().setStiffness(SpringForce.STIFFNESS_LOW).setDampingRatio(0.85f);
touchSpringX.addEndListener((animation, canceled, value, velocity)->touchSpringX=null);
touchSpringX.start();
touchSpringY=new SpringAnimation(touchSpringYHolder, 0f);
touchSpringY.setMinimumVisibleChange(0.01f);
touchSpringY.getSpring().setStiffness(SpringForce.STIFFNESS_LOW).setDampingRatio(0.85f);
touchSpringY.addEndListener((animation, canceled, value, velocity)->touchSpringY=null);
touchSpringY.start();
updateEffects();
}
}
return true;
}
private void updateEffects(){
for(ViewEffect view:views){
view.update(Math.min(1f, Math.max(-1f, pitch+touchAddX)), Math.min(1f, Math.max(-1f, roll+touchAddY)));
}
}
public static class ViewEffect{ public static class ViewEffect{
private View view; private View view;
private float minX, maxX, minY, maxY; private float minX, maxX, minY, maxY;

View File

@@ -200,6 +200,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private void onBoostClick(View v){ private void onBoostClick(View v){
if (GlobalUserPreferences.confirmBeforeReblog) {
v.startAnimation(opacityIn);
onBoostLongClick(v);
return;
}
boost.setSelected(!item.status.reblogged); boost.setSelected(!item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r)); AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
} }
@@ -235,9 +240,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE); reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE); reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE);
itemPublic.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PUBLIC) ? View.GONE : View.VISIBLE); itemPublic.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE); itemUnlisted.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
itemFollowers.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PRIVATE) ? View.GONE : View.VISIBLE); itemFollowers.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular); Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular); Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
@@ -245,16 +250,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_closed_24_regular); Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_closed_24_regular);
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null; StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
// e.g. post visibility is unlisted, but default is public
// in this case, we want to display the check mark on the most visible visibility
if (defaultVisibility != null && item.status.visibility.isLessVisibleThan(defaultVisibility)) {
for (StatusPrivacy vis : StatusPrivacy.values()) {
if (vis.equals(item.status.visibility)) {
defaultVisibility = vis;
break;
}
}
}
itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null); itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null);
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null); itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null); itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);

View File

@@ -1,8 +1,12 @@
package org.joinmastodon.android.ui.displayitems; package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
@@ -40,6 +44,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement; import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.ScheduledStatus;
@@ -79,13 +84,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public final Status status; public final Status status;
private boolean hasVisibilityToggle; private boolean hasVisibilityToggle;
boolean needBottomPadding; boolean needBottomPadding;
private String extraText; private CharSequence extraText;
private Notification notification; private Notification notification;
private ScheduledStatus scheduledStatus; private ScheduledStatus scheduledStatus;
private Announcement announcement; private Announcement announcement;
private Consumer<String> consumeReadAnnouncement; private Consumer<String> consumeReadAnnouncement;
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText, Notification notification, ScheduledStatus scheduledStatus){ public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, CharSequence extraText, Notification notification, ScheduledStatus scheduledStatus){
super(parentID, parentFragment); super(parentID, parentFragment);
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user; user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
this.user=user; this.user=user;
@@ -110,6 +115,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
} }
} }
this.extraText=extraText; this.extraText=extraText;
emojiHelper.addText(extraText);
} }
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) { public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
@@ -212,6 +218,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void onSuccess(GetStatusSourceText.Response result){ public void onSuccess(GetStatusSourceText.Response result){
args.putString("sourceText", result.text); args.putString("sourceText", result.text);
args.putString("sourceSpoiler", result.spoilerText); args.putString("sourceSpoiler", result.spoilerText);
if (result.contentType != null) {
args.putString("sourceContentType", result.contentType.name());
}
if (redraft) { if (redraft) {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{ UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
@@ -264,7 +273,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
progress.dismiss(); progress.dismiss();
}, rel->{ }, rel->{
relationship=rel; relationship=rel;
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getShortUsername()), Toast.LENGTH_SHORT).show(); Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : rel.requested ? R.string.following_user_requested : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
}); });
}else if(id==R.id.block_domain){ }else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{}); UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
@@ -463,6 +472,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
} }
Account account=item.user; Account account=item.user;
String username = account.getShortUsername();
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account); boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
boolean isPostScheduled=item.scheduledStatus!=null; boolean isPostScheduled=item.scheduledStatus!=null;
menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts); menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts);
@@ -498,14 +508,15 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
manageUserLists.setVisible(false); manageUserLists.setVisible(false);
}else{ }else{
mute.setVisible(true); mute.setVisible(true);
block.setVisible(true); // hiding when following to keep menu item count equal (trading it for user lists)
block.setVisible(relationship == null || !relationship.following);
report.setVisible(true); report.setVisible(true);
follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting)); follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting));
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername())); mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, username));
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular); mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute); UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername())); block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, username));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getShortUsername())); report.setTitle(item.parentFragment.getString(R.string.report_user, username));
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place // disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
// if(!account.isLocal()){ // if(!account.isLocal()){
// blockDomain.setVisible(true); // blockDomain.setVisible(true);
@@ -514,12 +525,53 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
blockDomain.setVisible(false); blockDomain.setVisible(false);
// } // }
boolean following = relationship!=null && relationship.following; boolean following = relationship!=null && relationship.following;
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, account.getShortUsername())); follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, username));
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular); 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.setVisible(relationship != null && relationship.following);
manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, account.getShortUsername())); manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, username));
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow); UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow);
} }
workaroundChangingMenuItemWidths(menu, username);
}
// ugliest piece of code you'll see in a while: i measure the menu items' text widths to
// determine the biggest one, because it's probably not being displayed at first
// (before the relationship loaded). i take the largest one's size and add a space to the
// last item ("open in browser") until it takes up as much space as the largest item.
// goal: no more ugly ellipsis after the relationship loads in when opening the context menu
// of a post
private void workaroundChangingMenuItemWidths(Menu menu, String username) {
String openInBrowserText = item.parentFragment.getString(R.string.open_in_browser);
if (relationship == null) {
float largestWidth = 0;
Paint paint = new Paint();
paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
String[] otherStrings = new String[] {
item.parentFragment.getString(R.string.unfollow_user, username),
item.parentFragment.getString(R.string.unblock_user, username),
item.parentFragment.getString(R.string.unmute_user, username),
item.parentFragment.getString(R.string.sk_lists_with_user, username),
};
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
if (item.getItemId() == R.id.open_in_browser || !item.isVisible()) continue;
float width = paint.measureText(menu.getItem(i).getTitle().toString());
if (width > largestWidth) largestWidth = width;
}
for (String str : otherStrings) {
float width = paint.measureText(str);
if (width > largestWidth) largestWidth = width;
}
float textWidth = paint.measureText(openInBrowserText);
float missingWidth = Math.max(0, largestWidth - textWidth);
float singleSpaceWidth = paint.measureText("");
int howManySpaces = (int) Math.ceil(missingWidth / singleSpaceWidth);
String enlargedText = openInBrowserText + "".repeat(howManySpaces);
menu.findItem(R.id.open_in_browser).setTitle(enlargedText);
} else {
menu.findItem(R.id.open_in_browser).setTitle(openInBrowserText);
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
package org.joinmastodon.android.ui.displayitems; package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.ui.utils.MediaAttachmentViewController.altWrapPadding; import static org.joinmastodon.android.GlobalUserPreferences.*;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
@@ -55,7 +55,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
for(Attachment att:attachments){ for(Attachment att:attachments){
requests.add(new UrlImageLoaderRequest(switch(att.type){ requests.add(new UrlImageLoaderRequest(switch(att.type){
case IMAGE -> att.url; case IMAGE -> att.url;
case VIDEO, GIFV -> att.previewUrl; case VIDEO, GIFV -> att.previewUrl != null ? att.previewUrl : att.url;
default -> throw new IllegalStateException("Unexpected value: "+att.type); default -> throw new IllegalStateException("Unexpected value: "+att.type);
}, 1000, 1000)); }, 1000, 1000));
} }
@@ -183,10 +183,11 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
altTextIndex=index; altTextIndex=index;
Attachment att=item.attachments.get(index); Attachment att=item.attachments.get(index);
boolean hasAltText = !TextUtils.isEmpty(att.description); boolean hasAltText = !TextUtils.isEmpty(att.description);
altTextButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); if ((hasAltText && !showAltIndicator) || (!hasAltText && !showNoAltIndicator)) return;
noAltTextButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); altTextButton.setVisibility(hasAltText && showAltIndicator ? View.VISIBLE : View.GONE);
altText.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); noAltTextButton.setVisibility(!hasAltText && showNoAltIndicator ? View.VISIBLE : View.GONE);
noAltText.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); altText.setVisibility(hasAltText && showAltIndicator ? View.VISIBLE : View.GONE);
noAltText.setVisibility(!hasAltText && showNoAltIndicator ? View.VISIBLE : View.GONE);
altText.setText(att.description); altText.setText(att.description);
altTextWrapper.setVisibility(View.VISIBLE); altTextWrapper.setVisibility(View.VISIBLE);
altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_overlay : R.drawable.bg_image_no_alt_overlay); altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_overlay : R.drawable.bg_image_no_alt_overlay);
@@ -202,15 +203,16 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
btnL-=loc[0]; btnL-=loc[0];
btnT-=loc[1]; btnT-=loc[1];
ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) altTextWrapper.getLayoutParams();
ArrayList<Animator> anims=new ArrayList<>(); ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0)); anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(noAltTextButton, 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(altTextScroller, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofFloat(altTextClose, 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, "left", btnL+margins.leftMargin, altTextWrapper.getLeft()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1], altTextWrapper.getTop())); anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+margins.topMargin, altTextWrapper.getTop()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-altWrapPadding[2], altTextWrapper.getRight())); anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-margins.rightMargin, altTextWrapper.getRight()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-altWrapPadding[3], altTextWrapper.getBottom())); anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-margins.bottomMargin, altTextWrapper.getBottom()));
for(Animator a:anims) for(Animator a:anims)
a.setDuration(300); a.setDuration(300);
@@ -247,10 +249,14 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
altTextAnimator.cancel(); altTextAnimator.cancel();
View btn=controllers.get(altTextIndex).btnsWrap; View btn=controllers.get(altTextIndex).btnsWrap;
int i=0;
for(MediaAttachmentViewController c:controllers){ for(MediaAttachmentViewController c:controllers){
if(c.btnsWrap!=null && c.btnsWrap!=btn) { boolean hasAltText = !TextUtils.isEmpty(item.attachments.get(i).description);
c.btnsWrap.setVisibility(View.VISIBLE); if(c.btnsWrap!=null
} && c.btnsWrap!=btn
&& ((hasAltText && showAltIndicator) || (!hasAltText && showNoAltIndicator))
) c.btnsWrap.setVisibility(View.VISIBLE);
i++;
} }
int[] loc={0, 0}; int[] loc={0, 0};
@@ -260,15 +266,16 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
btnL-=loc[0]; btnL-=loc[0];
btnT-=loc[1]; btnT-=loc[1];
ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) altTextWrapper.getLayoutParams();
ArrayList<Animator> anims=new ArrayList<>(); ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1)); anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1)); anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0)); anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0));
anims.add(ObjectAnimator.ofFloat(altTextClose, 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, "left", btnL+margins.leftMargin));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1])); anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+margins.topMargin));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-altWrapPadding[2])); anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-margins.rightMargin));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-altWrapPadding[3])); anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-margins.bottomMargin));
for(Animator a:anims) for(Animator a:anims)
a.setDuration(300); a.setDuration(300);

View File

@@ -32,23 +32,23 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private CharSequence text, compactText; private CharSequence text;
@DrawableRes @DrawableRes
private int icon; private int icon;
private StatusPrivacy visibility; private StatusPrivacy visibility;
@DrawableRes @DrawableRes
private int iconEnd; private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), fullTextEmojiHelper;
private View.OnClickListener handleClick; private View.OnClickListener handleClick;
boolean belowHeader, needBottomPadding; boolean belowHeader, needBottomPadding;
ReblogOrReplyLineStatusDisplayItem extra; ReblogOrReplyLineStatusDisplayItem extra;
String contentDescription; CharSequence fullText;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) { public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, null); this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text);
} }
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, String contentDescription) { public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, CharSequence fullText) {
super(parentID, parentFragment); super(parentID, parentFragment);
SpannableStringBuilder ssb=new SpannableStringBuilder(text); SpannableStringBuilder ssb=new SpannableStringBuilder(text);
HtmlParser.parseCustomEmoji(ssb, emojis); HtmlParser.parseCustomEmoji(ssb, emojis);
@@ -59,7 +59,15 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
TypedValue outValue = new TypedValue(); TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
updateVisibility(visibility); updateVisibility(visibility);
this.contentDescription = contentDescription;
if (fullText != null) {
fullTextEmojiHelper = new CustomEmojiHelper();
SpannableStringBuilder fullTextSsb = new SpannableStringBuilder(fullText);
HtmlParser.parseCustomEmoji(fullTextSsb, emojis);
this.fullText=fullTextSsb;
fullTextEmojiHelper.setText(fullTextSsb);
}
} }
public void updateVisibility(StatusPrivacy visibility) { public void updateVisibility(StatusPrivacy visibility) {
@@ -90,31 +98,23 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{ public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView text, extraText; private final TextView text, extraText;
private final View separator; private final View separator;
private int currentOrientation = -1; private final ViewGroup parent;
public Holder(Activity activity, ViewGroup parent){ public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_reblog_or_reply_line, parent); super(activity, R.layout.display_item_reblog_or_reply_line, parent);
this.parent = parent;
text=findViewById(R.id.text); text=findViewById(R.id.text);
extraText=findViewById(R.id.extra_text); extraText=findViewById(R.id.extra_text);
separator=findViewById(R.id.separator); separator=findViewById(R.id.separator);
if (GlobalUserPreferences.replyLineAboveHeader && GlobalUserPreferences.compactReblogReplyLine) { if (GlobalUserPreferences.replyLineAboveHeader && GlobalUserPreferences.compactReblogReplyLine) {
itemView.getViewTreeObserver().addOnPreDrawListener(() -> { parent.addOnLayoutChangeListener((v, l, t, right, b, ol, ot, oldRight, ob) -> {
if (item == null) return true; if (right != oldRight) layoutLine();
int orientation = ((LinearLayout) itemView).getOrientation();
if (orientation == currentOrientation) return true; // only run once
currentOrientation = orientation;
extraText.setPaddingRelative(extraText.getPaddingStart(), item.extra != null && orientation == LinearLayout.VERTICAL ? 0 : V.dp(16), extraText.getPaddingEnd(), extraText.getPaddingBottom());
separator.setVisibility(item.extra != null && orientation == LinearLayout.HORIZONTAL ? View.VISIBLE : View.GONE);
((LinearLayout) itemView).removeView(extraText);
if (orientation == LinearLayout.VERTICAL) ((LinearLayout) itemView).addView(extraText);
else ((LinearLayout) itemView).addView(extraText, 0);
return true;
}); });
} }
} }
private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) { private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) {
if (item.contentDescription != null) text.setContentDescription(item.contentDescription); if (item.fullText != null) text.setContentDescription(item.fullText);
text.setText(item.text); text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0); text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
text.setOnClickListener(item.handleClick); text.setOnClickListener(item.handleClick);
@@ -146,6 +146,27 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
params.topMargin = item.belowHeader ? V.dp(-6) : 0; params.topMargin = item.belowHeader ? V.dp(-6) : 0;
itemView.setLayoutParams(params); itemView.setLayoutParams(params);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0); itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
layoutLine();
}
private void layoutLine() {
// layout line only if above header, compact and has extra
if (!GlobalUserPreferences.replyLineAboveHeader
|| !GlobalUserPreferences.compactReblogReplyLine
|| item.extra == null) return;
itemView.measure(
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.UNSPECIFIED);
boolean isVertical = ((LinearLayout) itemView).getOrientation() == LinearLayout.VERTICAL;
extraText.setPaddingRelative(extraText.getPaddingStart(), item.extra != null && isVertical ? 0 : V.dp(16), extraText.getPaddingEnd(), extraText.getPaddingBottom());
separator.setVisibility(item.extra != null && !isVertical ? View.VISIBLE : View.GONE);
((LinearLayout) itemView).removeView(extraText);
if (isVertical) ((LinearLayout) itemView).addView(extraText);
else ((LinearLayout) itemView).addView(extraText, 0);
text.setText(isVertical ? item.fullText : item.text);
if (item.extra != null) {
extraText.setText(isVertical ? item.extra.fullText : item.extra.text);
}
} }
@Override @Override

View File

@@ -81,18 +81,10 @@ public abstract class StatusDisplayItem{
}; };
} }
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){ 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); 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){ 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(); String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>(); ArrayList<StatusDisplayItem> items=new ArrayList<>();
@@ -102,12 +94,8 @@ public abstract class StatusDisplayItem{
args.putString("account", accountID); args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null; ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream() if (!statusForContent.filterRevealed) {
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList()); statusForContent.filterRevealed = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN).test(status);
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
if(!statusForContent.filterRevealed){
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
} }
ReblogOrReplyLineStatusDisplayItem replyLine = null; ReblogOrReplyLineStatusDisplayItem replyLine = null;
@@ -201,8 +189,6 @@ public abstract class StatusDisplayItem{
} }
if(addFooter){ if(addFooter){
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID)); items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
} }
int i=1; int i=1;
for(StatusDisplayItem item:items){ for(StatusDisplayItem item:items){
@@ -210,13 +196,16 @@ public abstract class StatusDisplayItem{
item.index=i++; item.index=i++;
} }
if (!statusForContent.filterRevealed) { ArrayList<StatusDisplayItem> result = statusForContent.filterRevealed ? items :
return new ArrayList<>(List.of( new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)));
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
)); if (addFooter && status.hasGapAfter && !(fragment instanceof ThreadFragment)) {
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
gap.index = i++;
result.add(gap);
} }
return items; return result;
} }
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){ public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){

View File

@@ -6,7 +6,6 @@ import android.graphics.drawable.Drawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Button; import android.widget.Button;
import android.widget.ScrollView; import android.widget.ScrollView;
@@ -49,9 +48,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
private CharSequence parsedSpoilerText; private CharSequence parsedSpoilerText;
public boolean textSelectable; public boolean textSelectable;
public final Status status; public final Status status;
public boolean disableTranslate; public boolean disableTranslate, translationShown;
public boolean translated = false;
public TranslatedStatus translation = null;
private AccountSession session; 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 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");
@@ -60,6 +57,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
this.text=text; this.text=text;
this.status=status; this.status=status;
this.disableTranslate=disableTranslate; this.disableTranslate=disableTranslate;
this.translationShown=status.translationShown;
emojiHelper.setText(text); emojiHelper.setText(text);
if(!TextUtils.isEmpty(status.spoilerText)){ if(!TextUtils.isEmpty(status.spoilerText)){
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis); parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
@@ -69,6 +67,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID()); session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
} }
public void setTranslationShown(boolean translationShown) {
this.translationShown = translationShown;
status.translationShown = translationShown;
}
@Override @Override
public Type getType(){ public Type getType(){
return Type.TEXT; return Type.TEXT;
@@ -131,8 +134,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void onBind(TextStatusDisplayItem item){ public void onBind(TextStatusDisplayItem item){
text.setText(item.translated text.setText(item.translationShown
? HtmlParser.parse(item.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID()) ? HtmlParser.parse(item.status.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID())
: item.text); : item.text);
text.setTextIsSelectable(item.textSelectable); text.setTextIsSelectable(item.textSelectable);
if (item.textSelectable) { if (item.textSelectable) {
@@ -169,8 +172,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
boolean translateEnabled = !item.disableTranslate && instanceInfo != null && boolean translateEnabled = !item.disableTranslate && instanceInfo != null &&
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
instanceInfo.v2.configuration.translation.enabled; instanceInfo.v2.configuration.translation.enabled;
boolean isBottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find(); String bottomText = null;
boolean translateVisible = (isBottomText || ( try {
bottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find()
? new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN)
: null;
} catch (TranslationError ignored) {}
boolean translateVisible = (bottomText != null || (
translateEnabled && translateEnabled &&
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) && !item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
item.status.language != null && item.status.language != null &&
@@ -178,17 +187,18 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
!item.status.language.equalsIgnoreCase(Locale.getDefault().getLanguage()))) !item.status.language.equalsIgnoreCase(Locale.getDefault().getLanguage())))
&& (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable); && (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable);
translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE); translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post); translateButton.setText(item.translationShown ? R.string.sk_translate_show_original : R.string.sk_translate_post);
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, isBottomText ? "bottom-java" : item.translation.provider) : ""); translateInfo.setText(item.translationShown ? itemView.getResources().getString(R.string.sk_translated_using, bottomText != null ? "bottom-java" : item.status.translation.provider) : "");
String finalBottomText = bottomText;
translateButton.setOnClickListener(v->{ translateButton.setOnClickListener(v->{
if (item.translation == null) { if (item.status.translation == null) {
if (isBottomText) { if (finalBottomText != null) {
try { try {
item.translation = new TranslatedStatus(); item.status.translation = new TranslatedStatus();
item.translation.content = new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN); item.status.translation.content = finalBottomText;
item.translated = true; item.setTranslationShown(true);
} catch (TranslationError err) { } catch (TranslationError err) {
item.translation = null; item.status.translation = null;
Toast.makeText(itemView.getContext(), err.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); Toast.makeText(itemView.getContext(), err.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
} }
rebind(); rebind();
@@ -200,8 +210,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
new TranslateStatus(item.status.id).setCallback(new Callback<>() { new TranslateStatus(item.status.id).setCallback(new Callback<>() {
@Override @Override
public void onSuccess(TranslatedStatus translatedStatus) { public void onSuccess(TranslatedStatus translatedStatus) {
item.translation = translatedStatus; item.status.translation = translatedStatus;
item.translated = true; item.setTranslationShown(true);
if (item.parentFragment.getActivity() == null) return; if (item.parentFragment.getActivity() == null) return;
translateProgress.setVisibility(View.GONE); translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true); translateButton.setClickable(true);
@@ -218,7 +228,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
} }
}).exec(item.parentFragment.getAccountID()); }).exec(item.parentFragment.getAccountID());
} else { } else {
item.translated = !item.translated; item.setTranslationShown(!item.translationShown);
rebind(); rebind();
} }
}); });
@@ -237,9 +247,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) { if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
boolean tooBig = text.getMeasuredHeight() > textMaxHeight; boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
boolean inTimeline = !item.textSelectable;
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText); boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
boolean expandable = inTimeline && tooBig && !hasSpoiler; boolean expandable = tooBig && !hasSpoiler;
item.parentFragment.onEnableExpandable(Holder.this, expandable); item.parentFragment.onEnableExpandable(Holder.this, expandable);
} }

View File

@@ -29,6 +29,16 @@ public class CustomEmojiHelper{
} }
} }
public void addText(CharSequence text) {
if(!(text instanceof Spanned))
return;
CustomEmojiSpan[] spans=((Spanned) text).getSpans(0, text.length(), CustomEmojiSpan.class);
for(List<CustomEmojiSpan> group:Arrays.stream(spans).collect(Collectors.groupingBy(s->s.emoji)).values()){
this.spans.add(group);
requests.add(group.get(0).createImageLoaderRequest());
}
}
public int getImageCount(){ public int getImageCount(){
return requests.size(); return requests.size();
} }

View File

@@ -19,7 +19,6 @@ public class MediaAttachmentViewController{
public final MediaGridStatusDisplayItem.GridItemType type; public final MediaGridStatusDisplayItem.GridItemType type;
public final ImageView photo; public final ImageView photo;
public final View altButton, noAltButton, btnsWrap; public final View altButton, noAltButton, btnsWrap;
public static int[] altWrapPadding = null;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private final Context context; private final Context context;
private boolean didClear; private boolean didClear;
@@ -37,9 +36,6 @@ public class MediaAttachmentViewController{
btnsWrap=view.findViewById(R.id.alt_badges); btnsWrap=view.findViewById(R.id.alt_badges);
this.type=type; this.type=type;
this.context=context; this.context=context;
if (altWrapPadding == null) {
altWrapPadding = new int[] { btnsWrap.getPaddingLeft(), btnsWrap.getPaddingTop(), btnsWrap.getPaddingRight(), btnsWrap.getPaddingBottom() };
}
} }
public void bind(Attachment attachment, Status status){ public void bind(Attachment attachment, Status status){

View File

@@ -137,10 +137,15 @@ public class UiUtils {
private static Handler mainHandler = new Handler(Looper.getMainLooper()); private static Handler mainHandler = new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM"); private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public static int MAX_WIDTH;
private UiUtils() { private UiUtils() {
} }
public static void loadMaxWidth(Context ctx) {
if (MAX_WIDTH == 0) MAX_WIDTH = (int) ctx.getResources().getDimension(R.dimen.layout_max_width);
}
public static void launchWebBrowser(Context context, String url) { public static void launchWebBrowser(Context context, String url) {
try { try {
if (GlobalUserPreferences.useCustomTabs) { if (GlobalUserPreferences.useCustomTabs) {
@@ -338,12 +343,21 @@ public class UiUtils {
} }
public static int getThemeColor(Context context, @AttrRes int attr) { public static int getThemeColor(Context context, @AttrRes int attr) {
if (context == null) return 0xff00ff00;
TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
int color = ta.getColor(0, 0xff00ff00); int color = ta.getColor(0, 0xff00ff00);
ta.recycle(); ta.recycle();
return color; return color;
} }
public static int getThemeColorRes(Context context, @AttrRes int attr) {
if (context == null) return 0xff00ff00;
TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
int color = ta.getResourceId(0, R.color.black);
ta.recycle();
return color;
}
public static void openProfileByID(Context context, String selfID, String id) { public static void openProfileByID(Context context, String selfID, String id) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString("account", selfID); args.putString("account", selfID);
@@ -712,7 +726,7 @@ public class UiUtils {
public void onSuccess(Relationship result) { public void onSuccess(Relationship result) {
resultCallback.accept(result); resultCallback.accept(result);
progressCallback.accept(false); progressCallback.accept(false);
if (!result.following) { if(!result.following && !result.requested){
E.post(new RemoveAccountPostsEvent(accountID, account.id, true)); E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
} }
} }

View File

@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.views;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ComposeMediaLayout extends ViewGroup{ public class ComposeMediaLayout extends ViewGroup{
private static final int MAX_WIDTH_DP=400;
private static final int GAP_DP=8; private static final int GAP_DP=8;
private static final float ASPECT_RATIO=0.5625f; private static final float ASPECT_RATIO=0.5625f;
@@ -23,6 +23,7 @@ public class ComposeMediaLayout extends ViewGroup{
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){ public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle); super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
} }
@Override @Override
@@ -30,7 +31,7 @@ public class ComposeMediaLayout extends ViewGroup{
int mode=MeasureSpec.getMode(widthMeasureSpec); int mode=MeasureSpec.getMode(widthMeasureSpec);
@SuppressLint("SwitchIntDef") @SuppressLint("SwitchIntDef")
int width=switch(mode){ int width=switch(mode){
case MeasureSpec.AT_MOST -> Math.min(V.dp(MAX_WIDTH_DP), MeasureSpec.getSize(widthMeasureSpec)); case MeasureSpec.AT_MOST -> Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
case MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec); case MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec);
default -> throw new IllegalArgumentException("unsupported measure mode"); default -> throw new IllegalArgumentException("unsupported measure mode");
}; };

View File

@@ -6,6 +6,8 @@ import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import me.grishka.appkit.utils.V;
/** /**
* A LinearLayout for TextViews. First child TextView will get truncated if it doesn't fit, remaining will always wrap content. * A LinearLayout for TextViews. First child TextView will get truncated if it doesn't fit, remaining will always wrap content.
*/ */
@@ -36,7 +38,8 @@ public class HeaderSubtitleLinearLayout extends LinearLayout{
} }
View first=getChildAt(0); View first=getChildAt(0);
if(first instanceof TextView){ if(first instanceof TextView){
((TextView) first).setMaxWidth(remainingWidth); // guaranteeing at least 64dp of width for the display name
((TextView) first).setMaxWidth(Math.max(remainingWidth, V.dp(64)));
} }
}else{ }else{
View first=getChildAt(0); View first=getChildAt(0);

View File

@@ -6,13 +6,13 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.joinmastodon.android.ui.PhotoLayoutHelper; import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class MediaGridLayout extends ViewGroup{ public class MediaGridLayout extends ViewGroup{
private static final String TAG="MediaGridLayout"; private static final String TAG="MediaGridLayout";
public static final int MAX_WIDTH=400; // dp
private static final int GAP=1; // dp private static final int GAP=1; // dp
private PhotoLayoutHelper.TiledLayoutResult tiledLayout; private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private int[] columnStarts=new int[10], columnEnds=new int[10], rowStarts=new int[10], rowEnds=new int[10]; private int[] columnStarts=new int[10], columnEnds=new int[10], rowStarts=new int[10], rowEnds=new int[10];
@@ -27,7 +27,7 @@ public class MediaGridLayout extends ViewGroup{
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){ public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle); super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
} }
@Override @Override
@@ -36,7 +36,7 @@ public class MediaGridLayout extends ViewGroup{
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 0); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 0);
return; return;
} }
int width=Math.min(V.dp(MAX_WIDTH), MeasureSpec.getSize(widthMeasureSpec)); int width=Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
int height=Math.round(width*(tiledLayout.height/(float)PhotoLayoutHelper.MAX_WIDTH)); int height=Math.round(width*(tiledLayout.height/(float)PhotoLayoutHelper.MAX_WIDTH));
int offset=0; int offset=0;
@@ -74,10 +74,9 @@ public class MediaGridLayout extends ViewGroup{
if(tiledLayout==null) if(tiledLayout==null)
return; return;
int maxWidth=V.dp(MAX_WIDTH);
int xOffset=0; int xOffset=0;
if(r-l>maxWidth){ if(r-l>UiUtils.MAX_WIDTH){
xOffset=(r-l)/2-maxWidth/2; xOffset=(r-l)/2-UiUtils.MAX_WIDTH/2;
} }
for(int i=0;i<getChildCount();i++){ for(int i=0;i<getChildCount();i++){

File diff suppressed because one or more lines are too long

View File

@@ -8,52 +8,53 @@ import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StatusFilterPredicate implements Predicate<Status>{ public class StatusFilterPredicate implements Predicate<Status>{
private final List<Filter> filters; private final List<Filter> filters;
private final Filter.FilterContext context;
private final Filter.FilterAction action;
public StatusFilterPredicate(List<Filter> filters){ public StatusFilterPredicate(List<Filter> filters, Filter.FilterContext context){
this.filters=filters; this.filters=filters;
this.context = context;
this.action = Filter.FilterAction.HIDE;
} }
/**
* @param context null makes the predicate pass automatically
*/
public StatusFilterPredicate(String accountID, Filter.FilterContext context){ public StatusFilterPredicate(String accountID, Filter.FilterContext context){
this(accountID, context, Filter.FilterAction.HIDE);
}
/**
* @param context null makes the predicate pass automatically
* @param action defines what the predicate should check:
* should not be hidden or should not display with warning
*/
public StatusFilterPredicate(String accountID, Filter.FilterContext context, Filter.FilterAction action){
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList()); filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
this.context = context;
this.action = action;
} }
@Override @Override
public boolean test(Status status){ public boolean test(Status status){
if(status.filtered!=null){ if (context == null) return true;
if (status.filtered.isEmpty()){
return true;
}
boolean matches=status.filtered.stream()
.map(filterResult->filterResult.filter)
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
.anyMatch(filter->filter.filterAction==Filter.FilterAction.HIDE);
return !matches;
}
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
}
public boolean testWithWarning(Status status) { Stream<Filter> stream = status.filtered != null
if(status.filtered!=null){ // use server-provided per-status info (status.filtered) if available
if (status.filtered.isEmpty()){ ? status.filtered.stream().map(f -> f.filter)
return true; // or fall back to cached filters
} : filters.stream().filter(filter -> filter.matches(status));
boolean matches=status.filtered.stream()
.map(filterResult->filterResult.filter) return stream
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now())) // discard expired filters
.anyMatch(filter->filter.filterAction==Filter.FilterAction.WARN); .filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now()))
return !matches; // only apply filters for given context
} .filter(filter -> filter.context.contains(context))
for(Filter filter:filters){ // treating filterAction = null (from filters list) as FilterAction.HIDE
if(filter.matches(status)) .noneMatch(filter -> filter.filterAction == null ? action == Filter.FilterAction.HIDE : filter.filterAction == action);
return false;
}
return true;
} }
} }

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSurface" android:alpha="0.12"/>
</selector>

View File

@@ -2,8 +2,14 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape> <shape>
<solid android:color="?colorWindowBackground"/> <solid android:color="?colorM3Surface"/>
<corners android:topLeftRadius="12dp" android:topRightRadius="12dp"/> <corners android:topLeftRadius="28dp" android:topRightRadius="28dp"/>
</shape>
</item>
<item>
<shape android:tint="?colorM3Primary">
<solid android:color="#0D000000"/>
<corners android:topLeftRadius="28dp" android:topRightRadius="28dp"/>
</shape> </shape>
</item> </item>
</layer-list> </layer-list>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:gravity="center" android:width="36dp" android:height="4dp"> <item android:gravity="center" android:width="32dp" android:height="4dp">
<shape> <shape android:tint="?colorM3Outline">
<solid android:color="?android:textColorSecondary"/> <solid android:color="#66000000"/>
<corners android:radius="2dp"/> <corners android:radius="2dp"/>
</shape> </shape>
</item> </item>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/m3_primary_overlay" xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector>
<item android:state_enabled="true">
<shape>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
<corners android:radius="20dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="@color/m3_on_surface_overlay" android:width="1dp"/>
<corners android:radius="20dp"/>
</shape>
</item>
</selector>
</item>
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="#1effffff" xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector>
<item android:state_enabled="true">
<shape>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
<corners android:radius="20dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="@color/m3_on_surface_overlay" android:width="1dp"/>
<corners android:radius="20dp"/>
</shape>
</item>
</selector>
</item>
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="#1effffff" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_alert_28_filled" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?toolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_alert_28_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?toolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_alert_28_filled_badged" android:state_activated="true"/>
<item android:drawable="@drawable/ic_fluent_alert_28_filled_badged" android:state_checked="true"/>
<item android:drawable="@drawable/ic_fluent_alert_28_filled_badged" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_alert_28_regular_badged"/>
</selector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M20.063 8.445c1.256 1.258 1.255 3.295-0.002 4.551l-7.114 7.102c-0.271 0.272-0.608 0.469-0.978 0.573l-4.613 1.303c-0.57 0.162-1.094-0.373-0.92-0.94l1.387-4.543c0.107-0.354 0.3-0.675 0.562-0.936l7.124-7.112c1.258-1.256 3.297-1.255 4.554 0.002zM8.15 2.37L8.2 2.475l3.253 8.249-1.157 1.155L9.556 10H5.443l-0.995 2.52c-0.14 0.354-0.518 0.542-0.876 0.454l-0.098-0.031c-0.353-0.14-0.54-0.518-0.452-0.876l0.03-0.098 3.754-9.495C7.042 1.88 7.85 1.844 8.151 2.37zM7.503 4.792L6.036 8.5h2.928L7.503 4.792z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

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