Compare commits

..

351 Commits

Author SHA1 Message Date
sk
32081b71f5 throw exception if no instance for session 2023-05-29 13:31:42 +02:00
sk
7849c34d1f fix account list alignment 2023-05-29 13:25:14 +02:00
sk
24977ec613 bump version 2023-05-29 13:03:18 +02:00
sk
786bbab0d5 Merge remote-tracking branch 'weblate/main' 2023-05-29 13:01:36 +02:00
sk
1facb07c28 Merge remote-tracking branch 'upstream/l10n_master' 2023-05-29 13:01:24 +02:00
sk
bba5aba22d settings icons not important for accessibility 2023-05-29 12:56:36 +02:00
sk
d7b85d6eba move invalid strings 2023-05-29 12:47:38 +02:00
sk
6832bfb95c don't display blocked_by relationship
closes sk22#526
2023-05-29 12:34:53 +02:00
sk
4c379b67a3 only auto-open search on pleroma instances 2023-05-29 03:33:31 +02:00
sk
3a2ae1ce71 clean up preferences when removing account 2023-05-29 02:40:15 +02:00
sk
c80afaf9c0 avoid sessions without instance info 2023-05-29 02:40:02 +02:00
sk
31d22bac47 remove unused method 2023-05-29 02:38:52 +02:00
Jacoco
b5f6687925 More Akkoma improvements (#524)
* Only open account if domain matches
Akkoma will seemingly show results that don't match well. This checks if the domain matches before continuing

* Add "RE:" for quotes where it's missing

* Fix no hashtag history in search

* Skip not implemented discovery and select search on Pleroma

* Set proper max account fields for Pleroma

* Use Pleroma's non-standard poll limits

* Mark notifications as read properly on Pleroma

* Akkoma bubble timeline

* Respect Reply Visibility preference on all timelines

* vertically center if hashtag has no history

* only open account search result if uri equals

* add getInstance and isPleroma methods

* change timelines api, support compatibility checks

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-05-29 02:37:46 +02:00
taniamarquessilva
b3f25af923 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (286 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_PT/
2023-05-28 20:19:30 +00:00
taniamarquessilva
78c141e946 Translated using Weblate (Portuguese)
Currently translated at 14.6% (42 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt/
2023-05-28 20:19:30 +00:00
taniamarquessilva
83d36ce736 Translated using Weblate (Portuguese)
Currently translated at 52.9% (9 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt/
2023-05-28 20:19:30 +00:00
taniamarquessilva
b928357ff1 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (286 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_PT/
2023-05-28 20:19:30 +00:00
ihor_ck
c074bc57bc Translated using Weblate (Ukrainian)
Currently translated at 100.0% (286 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-05-28 20:19:30 +00:00
Linerly
0e80c88b7d Translated using Weblate (Indonesian)
Currently translated at 100.0% (286 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-05-28 20:19:30 +00:00
Choukajohn
5ffa5b01fc Translated using Weblate (French)
Currently translated at 100.0% (286 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-05-28 20:19:30 +00:00
gallegonovato
61d9929485 Translated using Weblate (Spanish)
Currently translated at 100.0% (286 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-05-28 20:19:30 +00:00
sk
231f19d113 ugly workaround for sk22#520
it's really, really ugly :(
2023-05-28 22:14:03 +02:00
sk
bb41f62db5 Merge remote-tracking branch 'upstream/master' 2023-05-28 19:55:27 +02:00
Gregory K
47edc3180b Merge pull request #586 from sk22/fix/hashtags-crash-akkoma
Fix crash when searching for Hashtags on Akkoma servers
2023-05-28 20:55:04 +03:00
sk
9939d99c4b Merge branch 'fix/hashtags-crash-akkoma' 2023-05-28 19:54:39 +02:00
sk
8053e8bb05 fix hashtag search crash on akkoma servers
closes mastodon#468
closes sk22#523
2023-05-28 19:51:29 +02:00
sk
b7e9380bc4 enable unspecified as default formatting option
closes sk22#521
2023-05-28 14:55:53 +02:00
Jacoco
83600087e1 Fix images being stretched on Pleroma (#522)
Closes sk22#488

* Update image bounds after load when metadata is null

* Fix broken image layout in some scenarios

* Transition when image dimensions update

* Replace blurhash with accent color on Pleroma

* fall back to solid color regardless of server

* use fragment's context instead of passing it down

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-05-28 14:44:23 +02:00
Eugen Rochko
fe84dc4823 New translations strings.xml (Chinese Simplified) 2023-05-27 16:39:55 +02:00
sk
c38eb545b1 use matched filter for determining warning title
fixes a bug where, when multiple filters apply, the
WarningFilteredStatusDisplayItem would not check if the warning applies to the
current context. now, matched filter is determined through the predicate
(though not exactly what a predicate is supposed to do, i guess) and passed
down to the WarningFilteredStatusDisplayItem. cc @LucasGGamerM
2023-05-27 13:09:36 +02:00
sk
1fc2f81dab fix creating posts on other people's account timelines
closes sk22#508
2023-05-27 01:50:10 +02:00
LucasGGamerM
69ddc95c2c fix crash when notification markers are null
This would happen when an account had 0 notifications and received one.
After which, the user would tap on the notification icon on the tab bar
and the app would crash.
2023-05-27 01:40:05 +02:00
sk
a6ac68499c use url as fallback for remote url 2023-05-27 01:37:10 +02:00
sk
c10d7cfee4 use remote url; file name as fallback for alt text 2023-05-27 01:32:49 +02:00
sk
f933bdbc53 button with ripple for files and instance picker 2023-05-27 01:13:32 +02:00
LucasGGamerM
274bca84d9 Add display item for unknown/file attachments
Co-authored-by: LucasGGamerM <lucassggabriel@gmail.com>
2023-05-27 01:11:26 +02:00
sk
6abfe6ddd7 add unit tests for status filter predicate 2023-05-26 17:02:39 +02:00
sk
ab7489a049 bump version 2023-05-26 02:37:32 +02:00
sk
a6fd6ae135 add javadoc 2023-05-26 02:37:25 +02:00
Espasant3
b30d4a025f Translated using Weblate (Galician)
Currently translated at 99.6% (274 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-05-26 00:16:40 +00:00
Daudix_UFO
5b747bfc74 Translated using Weblate (Russian)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-05-26 00:16:40 +00:00
gallegonovato
a410d19114 Translated using Weblate (Spanish)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-05-26 00:16:39 +00:00
gicorada
a8589cc5b0 Translated using Weblate (Italian)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/it/
2023-05-26 00:16:39 +00:00
Eryk Michalak
b057c9f7a8 Translated using Weblate (Polish)
Currently translated at 100.0% (17 of 17 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2023-05-26 00:16:39 +00:00
ihor_ck
96e4a4933c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-05-26 00:16:39 +00:00
Eryk Michalak
630064500d Translated using Weblate (Polish)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-05-26 00:16:39 +00:00
gicorada
9543294996 Translated using Weblate (Italian)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-05-26 00:16:39 +00:00
Linerly
56e9cc3406 Translated using Weblate (Indonesian)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-05-26 00:16:39 +00:00
Choukajohn
be569cbe72 Translated using Weblate (French)
Currently translated at 100.0% (275 of 275 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-05-26 00:16:39 +00:00
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
Eugen Rochko
2e1795dc6f New translations strings.xml (Russian) 2023-05-20 13:04:15 +02:00
Eugen Rochko
cd1be782fa New translations strings.xml (Armenian) 2023-05-16 18:44:56 +02:00
Eugen Rochko
67059f3d71 New translations strings.xml (Armenian) 2023-05-16 17:30:41 +02:00
Eugen Rochko
15f4d3326b New translations strings.xml (Russian) 2023-05-15 08:47:41 +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
Eugen Rochko
e65404a466 New translations strings.xml (Dutch) 2023-05-13 20:26:36 +02:00
Eugen Rochko
3d47d1b4db New translations full_description.txt (Dutch) 2023-05-13 19:05:00 +02:00
Eugen Rochko
d1749ab610 New translations strings.xml (Dutch) 2023-05-13 19:04:59 +02:00
Eugen Rochko
806c264686 New translations strings.xml (Vietnamese) 2023-05-13 17:46:17 +02:00
Eugen Rochko
34a9cb5a74 New translations strings.xml (Dutch) 2023-05-13 17:46:16 +02:00
Eugen Rochko
64fad2e871 New translations strings.xml (Vietnamese) 2023-05-13 16:36:00 +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
Eugen Rochko
9c89c26097 New translations strings.xml (Dutch) 2023-05-13 01:52:41 +02:00
Eugen Rochko
e3b6a5d389 New translations strings.xml (Dutch) 2023-05-13 00:57:05 +02:00
Eugen Rochko
0fb54efde5 New translations strings.xml (Dutch) 2023-05-12 23:58:21 +02:00
Eugen Rochko
a4a3f32dba New translations strings.xml (Dutch) 2023-05-12 22:45:52 +02:00
Eugen Rochko
03a1e29e0c New translations strings.xml (Dutch) 2023-05-12 19:43:17 +02:00
Eugen Rochko
eda9ff272b New translations strings.xml (Dutch) 2023-05-12 18:35:16 +02:00
Eugen Rochko
b3728e06ac New translations strings.xml (Swedish) 2023-05-11 12:28:06 +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
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
212 changed files with 5379 additions and 1518 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

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Megalodon</title>
<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">
</head>
<body class="markdown-body">

View File

@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
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
// 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
android.useAndroidX=true
# 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
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
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
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");
# 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
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# 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"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
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 [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
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."
fi
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.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +137,109 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# 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" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --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
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
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" "$@"

15
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -2,6 +2,12 @@ plugins {
id 'com.android.application'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
android {
compileSdk 33
defaultConfig {
@@ -9,11 +15,11 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 83
versionName "1.2.0+fork.83"
versionCode 85
versionName "1.2.3+fork.85"
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 {
release {
@@ -49,14 +55,19 @@ android {
setRoot "src/github"
}
}
lintOptions{
checkReleaseBuilds false
namespace 'org.joinmastodon.android'
lint {
abortOnError false
checkReleaseBuilds false
}
buildFeatures {
buildConfig true
}
}
dependencies {
api 'androidx.annotation:annotation:1.3.0'
api 'androidx.annotation:annotation:1.6.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'me.grishka.litex:recyclerview:1.2.1.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:viewpager2:1.0.0'
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 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'

View File

@@ -0,0 +1,81 @@
package org.joinmastodon.android.utils;
import static org.joinmastodon.android.model.Filter.FilterAction.*;
import static org.joinmastodon.android.model.Filter.FilterContext.*;
import static org.junit.Assert.*;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.junit.Test;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
public class StatusFilterPredicateTest {
private static final Filter hideMeFilter = new Filter(), warnMeFilter = new Filter();
private static final List<Filter> allFilters = List.of(hideMeFilter, warnMeFilter);
private static final Status
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
static {
hideMeFilter.phrase = "hide me";
hideMeFilter.filterAction = HIDE;
hideMeFilter.context = EnumSet.of(PUBLIC, HOME);
warnMeFilter.phrase = "warning";
warnMeFilter.filterAction = WARN;
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
}
@Test
public void testHide() {
assertFalse("should not pass because matching filter applies to given context",
new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic));
}
@Test
public void testHideRegardlessOfContext() {
assertTrue("filters without context should always pass",
new StatusFilterPredicate(allFilters, null).test(hideInHomePublic));
}
@Test
public void testHideInDifferentContext() {
assertTrue("should pass because matching filter does not apply to given context",
new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic));
}
@Test
public void testHideWithWarningText() {
assertTrue("should pass because matching filter is for warnings",
new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic));
}
@Test
public void testWarn() {
assertFalse("should not pass because filter applies to given context",
new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic));
}
@Test
public void testWarnRegardlessOfContext() {
assertTrue("filters without context should always pass",
new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic));
}
@Test
public void testWarnInDifferentContext() {
assertTrue("should pass because filter does not apply to given context",
new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic));
}
@Test
public void testWarnWithHideText() {
assertTrue("should pass because matching filter is for hiding",
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
}
}

View File

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

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.joinmastodon.android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -39,6 +38,22 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</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">
<intent-filter>
<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.fragments.ComposeFragment;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
@@ -51,9 +52,15 @@ public class ExternalShareActivity extends FragmentStackActivity{
String subject = "";
if (intent.hasExtra(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();
List<Uri> mediaUris;
if(Intent.ACTION_SEND.equals(intent.getAction())){
@@ -80,8 +87,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
args.putString("account", accountID);
if(!TextUtils.isEmpty(text))
args.putString("prefilledText", text);
if(!subject.isBlank())
args.putInt("selectionEnd", subject.length());
args.putInt("selectionStart", StringUtil.isBlank(subject) ? 0 : subject.length());
if(mediaUris!=null && !mediaUris.isEmpty())
args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris));
Fragment fragment=new ComposeFragment();

View File

@@ -8,6 +8,7 @@ import android.content.SharedPreferences;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
@@ -46,16 +47,20 @@ public class GlobalUserPreferences{
public static boolean autoHideFab;
public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> accountsInGlitchMode;
public static Set<String> accountsWithContentTypesEnabled;
public static Map<String, ContentType> accountsDefaultContentTypes;
/**
* Pleroma
@@ -72,6 +77,16 @@ public class GlobalUserPreferences{
catch (JsonSyntaxException ignored) { return orElse; }
}
public static void removeAccount(String accountId) {
recentLanguages.remove(accountId);
pinnedTimelines.remove(accountId);
accountsInGlitchMode.remove(accountId);
accountsWithLocalOnlySupport.remove(accountId);
accountsWithContentTypesEnabled.remove(accountId);
accountsDefaultContentTypes.remove(accountId);
save();
}
public static void load(){
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
@@ -102,6 +117,7 @@ public class GlobalUserPreferences{
autoHideFab=prefs.getBoolean("autoHideFab", true);
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
@@ -109,6 +125,8 @@ public class GlobalUserPreferences{
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
replyVisibility=prefs.getString("replyVisibility", null);
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -148,6 +166,7 @@ public class GlobalUserPreferences{
.putString("publishButtonText", publishButtonText)
.putBoolean("bottomEncoding", bottomEncoding)
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
.putInt("theme", theme.ordinal())
.putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages))
@@ -155,6 +174,8 @@ public class GlobalUserPreferences{
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.apply();
}
@@ -175,4 +196,3 @@ public class GlobalUserPreferences{
DARK
}
}

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

@@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -74,10 +73,8 @@ public class CacheController{
int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id;
for(Filter filter:filters){
if(filter.matches(status))
continue outer;
}
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
continue outer;
result.add(status);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
@@ -92,7 +89,7 @@ public class CacheController{
.setCallback(new Callback<>(){
@Override
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);
}
@@ -148,10 +145,8 @@ public class CacheController{
ntf.postprocess();
newMaxID=ntf.id;
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status))
continue outer;
}
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
continue outer;
}
result.add(ntf);
}while(cursor.moveToNext());
@@ -164,17 +159,13 @@ public class CacheController{
}
}
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isPleroma())
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status)){
return false;
}
}
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
}
return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));

View File

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

View File

@@ -5,8 +5,6 @@ import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{
@@ -22,7 +20,7 @@ public class MastodonErrorResponse extends ErrorResponse{
@Override
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);
}

View File

@@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.notifications;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
private String maxID;
public PleromaMarkNotificationsRead(String maxID) {
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
this.maxID = maxID;
}
@Override
public RequestBody getRequestBody() {
MultipartBody.Builder builder=new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if(!TextUtils.isEmpty(maxID))
builder.addFormDataPart("max_id", maxID);
return builder.build();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
@@ -46,6 +47,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public String language;
public String quoteId;
public ContentType contentType;
public static class Poll{
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.MastodonAPIRequest;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
import org.joinmastodon.android.model.ContentType;
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
public GetStatusSourceText(String id){
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
}
@AllFieldsAreRequired
public static class Response extends BaseModel{
@RequiredField
public String id;
@RequiredField
public String text;
@RequiredField
public String spoilerText;
public ContentType contentType;
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.timelines;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
public GetBubbleTimeline(String maxID, int limit) {
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
addQueryParameter("limit", ""+limit);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@@ -4,6 +4,7 @@ import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@@ -7,6 +7,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
@@ -87,4 +88,8 @@ public class AccountSession{
pushSubscriptionManager=new PushSubscriptionManager(getID());
return pushSubscriptionManager;
}
public Instance getInstance() {
return AccountSessionManager.getInstance().getInstanceInfo(domain);
}
}

View File

@@ -15,6 +15,7 @@ import android.util.Log;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -110,6 +111,12 @@ public class AccountSessionManager{
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
// write initial instance info to file immediately to avoid sessions without instance info
InstanceInfoStorageWrapper wrapper = new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
@@ -178,6 +185,7 @@ public class AccountSessionManager{
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db");
GlobalUserPreferences.removeAccount(id);
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty())
@@ -455,11 +463,12 @@ public class AccountSessionManager{
}
public Instance getInstanceInfo(String domain){
return instances.get(domain);
}
public Instance getInstanceInfoForAccount(String account) {
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
Instance instance = instances.get(domain);
if (instance == null) {
throw new IllegalStateException("Cannot get instance for " + domain + ". Sessions: "
+ String.join(", ", instances.keySet()));
}
return instance;
}
public void updateAccountInfo(String id, Account account){

View File

@@ -3,10 +3,6 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.TranslateAnimation;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -64,7 +60,12 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
public void onSuccess(List<Status> result){
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());
}
})
@@ -84,7 +85,8 @@ public class AccountTimelineFragment extends StatusListFragment{
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
AccountSessionManager asm = AccountSessionManager.getInstance();
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user))
return;
if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){
@@ -122,4 +124,10 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// 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.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
@@ -69,7 +68,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<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 DisplayItemsAdapter adapter;
protected String accountID;
@@ -83,16 +82,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public BaseStatusListFragment(){
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;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
UiUtils.loadMaxWidth(getContext());
if(GlobalUserPreferences.disableMarquee){
setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false);
@@ -101,8 +101,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
setRetainInstance(true);
}
@Override
protected RecyclerView.Adapter getAdapter(){
return adapter=new DisplayItemsAdapter();
@@ -270,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
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(currentPhotoViewer!=null)
currentPhotoViewer.offsetView(-dx, -dy);
View fab = getFab();
if (fab!=null && GlobalUserPreferences.autoHideFab) {
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
TranslateAnimation animate = new TranslateAnimation(
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
fab.startAnimation(animate);
fab.setVisibility(View.INVISIBLE);
scrollDiff = 0;
hideFab();
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) {
fab.setVisibility(View.VISIBLE);
TranslateAnimation animate = new TranslateAnimation(
0,
0,
fab.getHeight() * 2,
0);
animate.setDuration(300);
fab.startAnimation(animate);
showFab();
scrollDiff = 0;
} else {
scrollDiff += Math.abs(dy);
@@ -343,10 +363,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
updateToolbar();
if (withComposeButton()) {
if (wantsComposeButton() && !getArguments().getBoolean("__disable_fab", false)) {
fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(this::onFabLongClick);
} else if (fab != null) {
fab.setVisibility(View.GONE);
}
}
@@ -529,6 +551,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
holder.rebind();
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
@@ -655,13 +685,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer.onPause();
}
protected void onFabClick(View v){
public void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
}
protected boolean onFabLongClick(View v) {
public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID);
}
@@ -758,7 +788,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!imgHolder.getItem().status.spoilerRevealed){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH));
int width=Math.min(listWidth, UiUtils.MAX_WIDTH);
if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32));
c.save();

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import org.joinmastodon.android.R;
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.Status;
@@ -35,4 +36,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
})
.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.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Instance;
@@ -179,8 +180,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private int charCount, charLimit, trimmedCharCount;
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss, contentTypeBtn;
private ImageView sensitiveIcon;
private ComposeMediaLayout attachmentsView;
private TextView replyText;
@@ -234,6 +235,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private Runnable updateUploadEtaRunnable;
private String language, encoding;
private ContentType contentType;
private MastodonLanguage.LanguageResolver languageResolver;
@Override
@@ -242,6 +244,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
setRetainInstance(true);
accountID=getArguments().getString("account");
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
instanceDomain=session.domain;
@@ -330,6 +334,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
contentTypeBtn=view.findViewById(R.id.btn_content_type);
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
@@ -364,6 +369,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
buildContentTypePopup(contentTypeBtn);
contentTypeBtn.setOnClickListener(v->contentTypePopup.show());
contentTypeBtn.setOnTouchListener(contentTypePopup.getDragToOpenListener());
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
@@ -466,8 +475,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
}
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
if (savedInstanceState != null) {
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
} else if (editingStatus != null && editingStatus.visibility != null) {
statusVisibility = editingStatus.visibility;
} else {
loadDefaultStatusVisibility(savedInstanceState);
}
@@ -482,6 +493,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}).setChecked(true);
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.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
View autocompleteView=autocompleteViewController.getView();
@@ -518,6 +543,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putParcelableArrayList("attachments", serializedAttachments);
}
outState.putSerializable("visibility", statusVisibility);
outState.putSerializable("contentType", contentType);
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
}
@@ -907,6 +933,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) {
if (menu.findItem(allLanguages.size()) == null) {
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
@@ -1050,9 +1087,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
req.status=text;
req.localOnly=localOnly;
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.visibility=localOnly && instance.isPleroma() ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.contentType=contentType;
req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
@@ -1547,7 +1585,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.isUploadingOrProcessing())
att.cancelUpload();
attachments.remove(att);
uploadNextQueuedAttachment();
if(!areThereAnyUploadingAttachments())
uploadNextQueuedAttachment();
attachmentsView.removeView(att.view);
if(getMediaAttachmentsCount()==0)
attachmentsView.setVisibility(View.GONE);
@@ -1704,11 +1743,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollChanged=true;
updatePublishButtonState();
}));
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
int maxCharactersPerOption = 50;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption;
else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
maxCharactersPerOption = instance.pollLimits.maxOptionChars;
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)});
pollOptionsView.addView(option.view);
pollOptions.add(option);
if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
int maxPollOptions = 4;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
maxPollOptions = instance.configuration.polls.maxOptions;
else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
maxPollOptions = instance.pollLimits.maxOptions;
if(pollOptions.size()==maxPollOptions)
addPollOptionBtn.setVisibility(View.GONE);
return option;
}
@@ -1850,7 +1902,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
if (instance.isPleroma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);
@@ -1894,14 +1946,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) {
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();
Preferences prefs = asm.getAccount(accountID).preferences;
if (prefs != null) {

View File

@@ -30,8 +30,11 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.DividerItemDecoration;
@@ -164,7 +167,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
@@ -190,7 +193,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
updateOptionsMenu();
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import org.joinmastodon.android.R;
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.Status;
@@ -35,4 +36,9 @@ public class FavoritedStatusListFragment extends StatusListFragment{
})
.exec(accountID);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
}

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;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -123,7 +123,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override
public void onSuccess(List<Status> result){
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());
}
})
@@ -138,12 +138,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
}
@Override
protected boolean onFabLongClick(View v) {
public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
}
@Override
protected void onFabClick(View v){
public void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", '#'+hashtag+' ');
@@ -154,4 +154,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
}

View File

@@ -66,6 +66,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private int currentTab=R.id.tab_home;
private String accountID;
private boolean isPleroma;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -73,16 +74,20 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
E.register(this);
accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name);
Instance instance = AccountSessionManager.getInstance().getAccount(accountID).getInstance();
isPleroma = instance.isPleroma();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
// TODO: clean up
if(savedInstanceState==null){
Bundle args=new Bundle();
args.putString("account", accountID);
homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("disableDiscover", isPleroma);
args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment();
searchFragment.setArguments(args);
@@ -104,7 +109,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
content.setOrientation(LinearLayout.VERTICAL);
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));
inflater.inflate(R.layout.tab_bar, content);
@@ -128,10 +133,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(savedInstanceState==null){
getChildFragmentManager().beginTransaction()
.add(R.id.fragment_wrap, homeTabFragment)
.add(R.id.fragment_wrap, searchFragment).hide(searchFragment)
.add(R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(R.id.fragment_wrap, profileFragment).hide(profileFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit();
String defaultTab=getArguments().getString("tab");
@@ -160,6 +165,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab");
tabBar.selectTab(currentTab);
Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction()
.hide(homeTabFragment)
@@ -227,8 +233,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment);
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
}
private void maybeTriggerLoading(Fragment newFragment){
@@ -288,10 +296,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
Instance instance = session.getInstance();
if (instance == null) return;
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.isPleroma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {

View File

@@ -24,6 +24,7 @@ import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
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.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 String accountID;
@@ -98,13 +99,14 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private PopupMenu overflowPopup;
private View overflowActionView = null;
private boolean announcementsBadged, settingsBadged;
private ImageButton fab;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
E.register(this);
accountID = getArguments().getString("account");
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
assert timelineDefinitions != null;
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
count = timelineDefinitions.size();
@@ -122,6 +124,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
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());
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();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
args.putBoolean("__disable_fab", true);
args.putBoolean("onlyPosts", true);
for (int i = 0; i < timelineDefinitions.size(); i++) {
@@ -280,6 +287,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}).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() {
Context ctx = getContext();
listsMenu.clear();
@@ -427,9 +448,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
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) {
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext()));
showFab();
}
@Override
@@ -657,6 +689,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return hashtagsItems.values();
}
public ImageButton getFab() {
return fab;
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull
@Override

View File

@@ -8,8 +8,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -38,7 +36,7 @@ public class HomeTimelineFragment extends StatusListFragment {
private String lastSavedMarkerID;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -150,7 +148,7 @@ public class HomeTimelineFragment extends StatusListFragment {
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
@@ -229,7 +227,7 @@ public class HomeTimelineFragment extends StatusListFragment {
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
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){
if(idsBelowGap.contains(s.id))
break;
@@ -282,4 +280,9 @@ public class HomeTimelineFragment extends StatusListFragment {
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
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;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -137,7 +137,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override
public void onSuccess(List<Status> result) {
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());
}
})
@@ -152,7 +152,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
}
@Override
protected void onFabClick(View v){
public void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
@@ -162,4 +162,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
protected void onSetFabBottomInset(int inset) {
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
}
}

View File

@@ -9,21 +9,26 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
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.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
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.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -39,6 +44,7 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
@@ -48,8 +54,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
@Override
protected boolean withComposeButton() {
return true;
protected boolean wantsComposeButton() {
return false;
}
@Override
@@ -83,6 +89,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null :
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){
case FOLLOW -> getString(R.string.user_followed_you);
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
@@ -93,8 +106,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
case UPDATE -> getString(R.string.sk_post_edited);
case SIGN_UP -> getString(R.string.sk_signed_up);
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){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
if(titleItem!=null)
@@ -141,12 +156,16 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache()){
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && AccountSessionManager.getInstance().getAccount(accountID).markers.notifications != null){
E.post(new AllNotificationsSeenEvent());
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
if (AccountSessionManager.getInstance().getAccount(accountID).markers != null)
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (AccountSessionManager.getInstance().getAccount(accountID).getInstance().isPleroma())
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
}
});
@@ -196,7 +215,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
if (getParentFragment() instanceof NotificationsFragment) fab.setVisibility(View.GONE);
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
}

View File

@@ -20,7 +20,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
}
@Override

View File

@@ -337,7 +337,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.onSelectedChanged(viewHolder, actionState);
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.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();
draggedViewHolder=viewHolder;
}

View File

@@ -19,13 +19,11 @@ import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
@@ -33,7 +31,6 @@ import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
@@ -53,6 +50,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
@@ -60,6 +58,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
@@ -112,7 +111,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@@ -139,6 +138,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private Account account;
private String accountID;
private String domain;
private Relationship relationship;
private int statusBarHeight;
private boolean isOwnProfile;
@@ -152,9 +152,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
protected int scrollDiff = 0;
private static final int MAX_FIELDS=4;
private int maxFields = 4;
// from ProfileAboutFragment
public UsableRecyclerView list;
@@ -175,6 +174,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setRetainInstance(true);
accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
@@ -182,6 +182,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
loaded=true;
if(!isOwnProfile)
loadRelationship();
else {
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(domain);
if (instance.isPleroma()) {
maxFields = instance.pleroma.metadata.fieldsLimits.maxFields;
}
}
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))
@@ -327,7 +333,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setOnLongClickListener(v->{
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
usernameString+="@"+domain;
}
UiUtils.copyText(username, '@'+usernameString);
return true;
@@ -513,7 +519,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
ssb.append(domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
@@ -523,7 +529,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
@@ -761,6 +767,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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){
int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatarBorder.getTop()-topBarsH){
@@ -791,35 +807,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
}
if (GlobalUserPreferences.autoHideFab) {
int dy = scrollY - oldScrollY;
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
TranslateAnimation animate = new TranslateAnimation(
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
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){
@@ -1211,7 +1198,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public int getItemCount(){
if(isInEditMode){
int size=metadataListData.size();
if(size<MAX_FIELDS)
if(size<maxFields)
size++;
return size;
}
@@ -1328,7 +1315,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onClick(){
metadataListData.add(new AccountField());
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
if(metadataListData.size()==maxFields){ // replace this row with new row
adapter.notifyItemChanged(metadataListData.size()-1);
}else{
adapter.notifyItemInserted(metadataListData.size()-1);
@@ -1375,7 +1362,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.onSelectedChanged(viewHolder, actionState);
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.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();
draggedViewHolder=viewHolder;
}

View File

@@ -31,7 +31,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -56,7 +56,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
}
@Override
protected void onFabClick(View v) {
public void onFabClick(View v) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
@@ -64,7 +64,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
}
@Override
protected boolean onFabLongClick(View v) {
public boolean onFabLongClick(View v) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
@@ -79,7 +79,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override
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
@@ -96,6 +96,8 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
args.putString("sourceText", status.text);
args.putString("sourceSpoiler", status.spoilerText);
args.putBoolean("redraftStatus", true);
args.putString("sourceContentType", scheduledStatus.params.contentType != null ?
scheduledStatus.params.contentType.name() : null);
setResult(true, null);
// 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.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
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.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.PushNotification;
@@ -64,6 +66,7 @@ import java.util.function.Consumer;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -80,7 +83,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
private ButtonItem defaultContentTypeButtonItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
@@ -89,7 +93,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ImageView themeTransitionWindowView;
private TextItem checkForUpdateItem, clearImageCacheItem;
private ImageCache imageCache;
private Menu contentTypeMenu;
@SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -99,7 +105,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
imageCache = ImageCache.getInstance(getActivity());
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
Instance instance = session.getInstance();
String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
@@ -207,13 +213,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
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 SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
if (instance.pleroma != null) {
if (instance.isPleroma()) {
items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.reply_visibility);
@@ -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->{
GlobalUserPreferences.loadNewPosts=i.checked;
showNewPostsButtonItem.enabled = i.checked;
showNewPostsItem.enabled = i.checked;
if (!i.checked) {
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();
}));
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.save();
}));
@@ -326,6 +336,34 @@ public class SettingsFragment extends MastodonToolbarFragment{
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version)));
items.add(new HeaderItem(R.string.sk_instance_features));
items.add(new SwitchItem(R.string.sk_settings_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);
}));
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->{
glitchModeItem.enabled = i.checked;
if (i.checked) {
@@ -496,6 +534,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){
String pref = null;
int id = item.getItemId();
@@ -1003,7 +1069,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
@Override
public void onBind(ButtonItem item){
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);
}
}

View File

@@ -1,43 +1,47 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Button;
import org.joinmastodon.android.MastodonApp;
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.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.parceler.Parcels;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
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.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class SplashFragment extends AppKitFragment{
private static final String DEFAULT_SERVER="mastodon.social";
private SizeListenerFrameLayout contentView;
private View artContainer, blueFill, greenFill;
private ViewPager2 pager;
private ViewGroup pagerDots;
private InterpolatingMotionEffect motionEffect;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
}
@Nullable
@@ -46,44 +50,26 @@ public class SplashFragment extends AppKitFragment{
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_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);
artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant);
artRightHill=contentView.findViewById(R.id.art_right_hill);
artLeftHill=contentView.findViewById(R.id.art_left_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);
blueFill=contentView.findViewById(R.id.blue_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(){
@Override
@@ -109,6 +95,38 @@ public class SplashFragment extends AppKitFragment{
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){
float scale=w/(float)V.dp(360);
artContainer.setScaleX(scale);
@@ -139,60 +157,15 @@ public class SplashFragment extends AppKitFragment{
return true;
}
private class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder>{
@NonNull
@Override
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;
}
@Override
protected void onShown(){
super.onShown();
motionEffect.activate();
}
private class PagerViewHolder extends RecyclerView.ViewHolder{
public PagerViewHolder(int page){
super(new LinearLayout(getActivity()));
LinearLayout ll=(LinearLayout) itemView;
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));
}
@Override
protected void onHidden(){
super.onHidden();
motionEffect.deactivate();
}
}

View File

@@ -6,6 +6,7 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -55,7 +56,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
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);
if(idx>=0){
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){
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){
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME);
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
}
protected abstract Filter.FilterContext getFilterContext();
@Override
protected void addAccountToKnown(Status s){
if(!knownAccounts.containsKey(s.account.id))

View File

@@ -74,7 +74,7 @@ public class ThreadFragment extends StatusListFragment{
}
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.pleroma != null){
if(instance.isPleroma()){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){
@@ -137,7 +137,7 @@ public class ThreadFragment extends StatusListFragment{
}
private List<Status> filterStatuses(List<Status> statuses){
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
return statuses.stream()
.filter(statusFilterPredicate)
.collect(Collectors.toList());
@@ -181,4 +181,10 @@ public class ThreadFragment extends StatusListFragment{
public boolean wantsLightNavigationBar(){
return !UiUtils.isDarkTheme();
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.THREAD;
}
}

View File

@@ -0,0 +1,55 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class BubbleTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE);
private String maxID;
@Override
protected boolean wantsComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
}

View File

@@ -19,6 +19,7 @@ import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.ui.SimpleViewHolder;
@@ -238,7 +239,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
else scrollToTop();
}
private void selectSearch() {
public void selectSearch() {
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
@@ -272,6 +273,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
if (getArguments().getBoolean("disableDiscover"))
((HomeFragment) getParentFragment()).onBackPressed();
}
@Override

View File

@@ -26,7 +26,7 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
@Override
public void onSuccess(List<Status> result){
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());
}
}).exec(accountID);
@@ -42,4 +42,10 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
public boolean isOnTop() {
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;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -35,7 +35,7 @@ public class FederatedTimelineFragment extends StatusListFragment {
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
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());
}
})
@@ -47,4 +47,9 @@ public class FederatedTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
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;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -34,7 +34,7 @@ public class LocalTimelineFragment extends StatusListFragment {
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
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());
}
})
@@ -46,4 +46,9 @@ public class LocalTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
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.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults;
@@ -80,7 +81,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
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);
};
}

View File

@@ -1,5 +1,8 @@
package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -105,6 +108,14 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override
public void onBind(Hashtag item){
title.setText('#'+item.name);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;

View File

@@ -124,6 +124,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
list.setItemAnimator(new BetterItemAnimator());
((UsableRecyclerView) list).setSelector(null);
}
@Override

View File

@@ -21,6 +21,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
@@ -261,4 +262,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected boolean wantsOverlaySystemNavigation(){
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 androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@Parcel
public class Filter extends BaseModel{
@RequiredField
public String id;
@RequiredField
public String phrase;
public String title;
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);

View File

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

View File

@@ -86,6 +86,8 @@ public class Instance extends BaseModel{
public Pleroma pleroma;
public PleromaPollLimits pollLimits;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@@ -134,6 +136,10 @@ public class Instance extends BaseModel{
return ci;
}
public boolean isPleroma() {
return pleroma != null;
}
@Parcel
public static class Rule{
public String id;
@@ -198,6 +204,28 @@ public class Instance extends BaseModel{
@Parcel
public static class Pleroma extends BaseModel {
// metadata etc
public Pleroma.Metadata metadata;
@Parcel
public static class Metadata {
public List<String> features;
public Pleroma.Metadata.FieldsLimits fieldsLimits;
@Parcel
public static class FieldsLimits {
public int maxFields;
public int maxRemoteFields;
public int nameLength;
public int valueLength;
}
}
}
@Parcel
public static class PleromaPollLimits {
public int maxExpiration;
public int maxOptionChars;
public int maxOptions;
public int minExpiration;
}
}

View File

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

View File

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

View File

@@ -50,6 +50,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public long favouritesCount;
public long repliesCount;
public Instant editedAt;
// might not be provided (by older mastodon servers),
// so megalodon will use the locally cached filters if filtered == null
public List<FilterResult> filtered;
public String url;
@@ -101,6 +103,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
card.postprocess();
if(reblog!=null)
reblog.postprocess();
if(filtered!=null)
for(FilterResult fr : filtered)
fr.postprocess();
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
@@ -177,7 +182,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
s.filtered = List.of();
return s;
}

View File

@@ -8,17 +8,20 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.discover.BubbleTimelineFragment;
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class TimelineDefinition {
private TimelineType type;
@@ -58,6 +61,14 @@ public class TimelineDefinition {
this.type = type;
}
public boolean isCompatible(AccountSession session) {
return true;
}
public boolean wantsDefault(AccountSession session) {
return true;
}
public String getTitle(Context ctx) {
return title != null ? title : getDefaultTitle(ctx);
}
@@ -78,6 +89,7 @@ public class TimelineDefinition {
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
case LIST -> listTitle;
case HASHTAG -> hashtagName;
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
};
}
@@ -89,6 +101,7 @@ public class TimelineDefinition {
case POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
case LIST -> Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
case BUBBLE -> Icon.BUBBLE;
};
}
@@ -100,6 +113,7 @@ public class TimelineDefinition {
case LIST -> new ListTimelineFragment();
case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment();
case BUBBLE -> new BubbleTimelineFragment();
};
}
@@ -156,7 +170,7 @@ public class TimelineDefinition {
return args;
}
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG }
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, BUBBLE }
public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
@@ -219,7 +233,8 @@ public class TimelineDefinition {
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true);
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
public final int iconRes, nameRes;
public final boolean hidden;
@@ -239,14 +254,49 @@ public class TimelineDefinition {
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
@Override
public boolean isCompatible(AccountSession session) {
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of
// at least one instance that supports it, but doesn't list "bubble_timeline"
return session.getInstance().isPleroma();
}
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE.copy(),
LOCAL_TIMELINE.copy(),
FEDERATED_TIMELINE.copy(),
POSTS_TIMELINE.copy()
@Override
public boolean wantsDefault(AccountSession session) {
Instance instance = session.getInstance();
return instance.isPleroma() && instance.pleroma.metadata.features.contains("bubble_timeline");
}
};
public static List<TimelineDefinition> getDefaultTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return DEFAULT_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session) && tl.wantsDefault(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
public static List<TimelineDefinition> getAllTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return ALL_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
private static final List<TimelineDefinition> DEFAULT_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
BUBBLE_TIMELINE,
FEDERATED_TIMELINE
);
private static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POSTS_TIMELINE,
BUBBLE_TIMELINE
);
}

View File

@@ -18,13 +18,15 @@ package org.joinmastodon.android.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.ViewPropertyAnimator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import java.util.ArrayList;
@@ -358,7 +360,14 @@ public class BetterItemAnimator extends SimpleItemAnimator{
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
float alpha = 0;
if (holder instanceof MediaGridStatusDisplayItem.Holder mediaItemHolder) {
if (mediaItemHolder.isSizeUpdating()) {
alpha = 1; // Image will flicker out and then in if alpha is 0
mediaItemHolder.sizeUpdated();
}
}
oldViewAnim.alpha(alpha).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.oldHolder, true);

View File

@@ -1,17 +1,25 @@
package org.joinmastodon.android.ui;
import android.annotation.SuppressLint;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.PathInterpolator;
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 WindowManager wm;
@@ -20,6 +28,34 @@ public class InterpolatingMotionEffect implements SensorEventListener{
private Sensor accelerometer;
private boolean accelerometerEnabled;
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){
sm=context.getSystemService(SensorManager.class);
@@ -50,8 +86,8 @@ public class InterpolatingMotionEffect implements SensorEventListener{
float z=event.values[2]/SensorManager.GRAVITY_EARTH;
float 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);
pitch=(float) (Math.atan2(x, Math.sqrt(y*y+z*z))/Math.PI*2.0);
roll=(float) (Math.atan2(y, Math.sqrt(x*x+z*z))/Math.PI*2.0);
switch(rotation){
case Surface.ROTATION_0:
@@ -88,9 +124,7 @@ public class InterpolatingMotionEffect implements SensorEventListener{
}else if(roll<-1f){
roll=-2f-roll;
}
for(ViewEffect view:views){
view.update(pitch, roll);
}
updateEffects();
}
@Override
@@ -110,6 +144,62 @@ public class InterpolatingMotionEffect implements SensorEventListener{
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{
private View view;
private float minX, maxX, minY, maxY;

View File

@@ -0,0 +1,64 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
public class FileStatusDisplayItem extends StatusDisplayItem{
private final Attachment attachment;
public FileStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Attachment attachment) {
super(parentID, parentFragment);
this.attachment=attachment;
}
@Override
public Type getType() {
return Type.FILE;
}
public static class Holder extends StatusDisplayItem.Holder<FileStatusDisplayItem> {
private final TextView title, domain;
public Holder(Context context, ViewGroup parent) {
super(context, R.layout.display_item_file, parent);
title=findViewById(R.id.title);
domain=findViewById(R.id.domain);
findViewById(R.id.inner).setOnClickListener(this::onClick);
}
@Override
public void onBind(FileStatusDisplayItem item) {
Uri url = Uri.parse(getUrl());
title.setText(item.attachment.description != null
? item.attachment.description
: url.getLastPathSegment());
title.setEllipsize(item.attachment.description != null ? TextUtils.TruncateAt.END : TextUtils.TruncateAt.MIDDLE);
domain.setText(url.getHost());
}
private void onClick(View v) {
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), getUrl());
}
private String getUrl() {
return item.attachment.remoteUrl == null ? item.attachment.url : item.attachment.remoteUrl;
}
}
}

View File

@@ -200,6 +200,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onBoostClick(View v){
if (GlobalUserPreferences.confirmBeforeReblog) {
v.startAnimation(opacityIn);
onBoostLongClick(v);
return;
}
boost.setSelected(!item.status.reblogged);
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);
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);
itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE);
itemFollowers.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PRIVATE) ? View.GONE : View.VISIBLE);
itemPublic.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
itemUnlisted.setVisibility(item.status.reblogged ? 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 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);
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);
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);

View File

@@ -1,7 +1,9 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -25,6 +27,13 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
private final TextView title, subtitle;
private final HashtagChartView chart;
public static final RelativeLayout.LayoutParams
withHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
withoutHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
static {
withoutHistoryParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
}
public Holder(Context context, ViewGroup parent){
super(context, R.layout.item_trending_hashtag, parent);
@@ -37,6 +46,14 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public void onBind(HashtagStatusDisplayItem _item){
Hashtag item=_item.tag;
title.setText('#'+item.name);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;

View File

@@ -44,6 +44,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
@@ -83,13 +84,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private boolean hasVisibilityToggle;
boolean needBottomPadding;
private String extraText;
private CharSequence extraText;
private Notification notification;
private ScheduledStatus scheduledStatus;
private Announcement announcement;
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);
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
this.user=user;
@@ -114,6 +115,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
}
this.extraText=extraText;
emojiHelper.addText(extraText);
}
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
@@ -216,6 +218,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void onSuccess(GetStatusSourceText.Response result){
args.putString("sourceText", result.text);
args.putString("sourceSpoiler", result.spoilerText);
if (result.contentType != null) {
args.putString("sourceContentType", result.contentType.name());
}
if (redraft) {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);

View File

@@ -1,12 +1,13 @@
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.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
@@ -17,7 +18,6 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
@@ -40,7 +40,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private static final String TAG="MediaGridDisplayItem";
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments;
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
@@ -55,7 +55,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
for(Attachment att:attachments){
requests.add(new UrlImageLoaderRequest(switch(att.type){
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);
}, 1000, 1000));
}
@@ -98,6 +98,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private int altTextIndex=-1;
private Animator altTextAnimator;
private boolean sizeUpdating = false;
public Holder(Activity activity, ViewGroup parent){
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
wrapper=(FrameLayout)itemView;
@@ -126,6 +128,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
layout.removeAllViews();
controllers.clear();
int i=0;
for(Attachment att:item.attachments){
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
@@ -158,6 +161,19 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
@Override
public void setImage(int index, Drawable drawable){
Rect bounds=drawable.getBounds();
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
if(item.attachments.get(index).meta==null){
Attachment.Metadata metadata = new Attachment.Metadata();
metadata.width=drawable.getIntrinsicWidth();
metadata.height=drawable.getIntrinsicHeight();
item.attachments.get(index).meta=metadata;
item.tiledLayout=PhotoLayoutHelper.processThumbs(item.attachments);
sizeUpdating = true;
item.parentFragment.onImageUpdated(this, index);
}
controllers.get(index).setImage(drawable);
}
@@ -183,10 +199,11 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
altTextIndex=index;
Attachment att=item.attachments.get(index);
boolean hasAltText = !TextUtils.isEmpty(att.description);
altTextButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE);
noAltTextButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE);
altText.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE);
noAltText.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE);
if ((hasAltText && !showAltIndicator) || (!hasAltText && !showNoAltIndicator)) return;
altTextButton.setVisibility(hasAltText && showAltIndicator ? View.VISIBLE : View.GONE);
noAltTextButton.setVisibility(!hasAltText && 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);
altTextWrapper.setVisibility(View.VISIBLE);
altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_overlay : R.drawable.bg_image_no_alt_overlay);
@@ -202,15 +219,16 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
btnL-=loc[0];
btnT-=loc[1];
ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) altTextWrapper.getLayoutParams();
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0], altTextWrapper.getLeft()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1], altTextWrapper.getTop()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-altWrapPadding[2], altTextWrapper.getRight()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-altWrapPadding[3], altTextWrapper.getBottom()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+margins.leftMargin, altTextWrapper.getLeft()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+margins.topMargin, altTextWrapper.getTop()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-margins.rightMargin, altTextWrapper.getRight()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-margins.bottomMargin, altTextWrapper.getBottom()));
for(Animator a:anims)
a.setDuration(300);
@@ -249,8 +267,11 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
View btn=controllers.get(altTextIndex).btnsWrap;
int i=0;
for(MediaAttachmentViewController c:controllers){
if(c.btnsWrap!=null && c.btnsWrap!=btn && !TextUtils.isEmpty(item.attachments.get(i).description))
c.btnsWrap.setVisibility(View.VISIBLE);
boolean hasAltText = !TextUtils.isEmpty(item.attachments.get(i).description);
if(c.btnsWrap!=null
&& c.btnsWrap!=btn
&& ((hasAltText && showAltIndicator) || (!hasAltText && showNoAltIndicator))
) c.btnsWrap.setVisibility(View.VISIBLE);
i++;
}
@@ -261,15 +282,16 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
btnL-=loc[0];
btnT-=loc[1];
ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) altTextWrapper.getLayoutParams();
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-altWrapPadding[2]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-altWrapPadding[3]));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+margins.leftMargin));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+margins.topMargin));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-margins.rightMargin));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-margins.bottomMargin));
for(Animator a:anims)
a.setDuration(300);
@@ -308,5 +330,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
layout.setClipChildren(clip);
wrapper.setClipChildren(clip);
}
public boolean isSizeUpdating() {
return sizeUpdating;
}
public void sizeUpdated() {
sizeUpdating = false;
}
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -9,6 +10,7 @@ import android.view.ViewGroup;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
@@ -20,12 +22,14 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
@@ -78,21 +82,14 @@ public abstract class StatusDisplayItem{
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
case FILE -> new FileStatusDisplayItem.Holder(activity, parent);
};
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
@@ -102,14 +99,6 @@ public abstract class StatusDisplayItem{
args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream()
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList());
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
if(!statusForContent.filterRevealed){
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
}
ReblogOrReplyLineStatusDisplayItem replyLine = null;
boolean threadReply = statusForContent.inReplyToAccountId != null &&
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
@@ -177,14 +166,32 @@ public abstract class StatusDisplayItem{
items.add(replyLine);
}
if (statusForContent.quote != null) {
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
if (!hasQuoteInlineTag) {
String quoteUrl = statusForContent.quote.url;
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
}
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)
replyLine.needBottomPadding=true;
else
header.needBottomPadding=true;
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream()
.filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN))
.collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorAccentLightest);
for (Attachment att : imageAttachments) {
if (att.blurhashPlaceholder == null) {
att.blurhashPlaceholder = new ColorDrawable(color);
}
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
}
@@ -193,6 +200,12 @@ public abstract class StatusDisplayItem{
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
}
}
statusForContent.mediaAttachments.stream()
.filter(att->att.type.equals(Attachment.Type.UNKNOWN))
.map(att -> new FileStatusDisplayItem(parentID, fragment, att))
.forEach(items::add);
if(statusForContent.poll!=null){
buildPollItems(parentID, fragment, statusForContent.poll, items);
}
@@ -201,8 +214,6 @@ public abstract class StatusDisplayItem{
}
if(addFooter){
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
}
int i=1;
for(StatusDisplayItem item:items){
@@ -210,13 +221,23 @@ public abstract class StatusDisplayItem{
item.index=i++;
}
Filter applyingFilter = null;
if (!statusForContent.filterRevealed) {
return new ArrayList<>(List.of(
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
));
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN);
statusForContent.filterRevealed = predicate.test(status);
applyingFilter = predicate.getApplyingFilter();
}
return items;
ArrayList<StatusDisplayItem> result = statusForContent.filterRevealed ? items :
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items, applyingFilter)));
if (addFooter && status.hasGapAfter && !(fragment instanceof ThreadFragment)) {
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
gap.index = i++;
result.add(gap);
}
return result;
}
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){
@@ -241,7 +262,8 @@ public abstract class StatusDisplayItem{
GAP,
EXTENDED_FOOTER,
MEDIA_GRID,
WARNING
WARNING,
FILE
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@@ -65,6 +65,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
UiUtils.loadMaxWidth(parentFragment.getContext());
}
public void setTranslationShown(boolean translationShown) {
@@ -241,8 +242,19 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setVisibility(View.GONE);
}
// incredibly ugly workaround for https://github.com/sk22/megalodon/issues/520
// i am so, so sorry. FIXME
// attempts to use OnPreDrawListener, OnGlobalLayoutListener and .post have failed -
// the view didn't want to reliably update after calling .setVisibility etc :(
int width = parent.getWidth() != 0 ? parent.getWidth()
: item.parentFragment.getView().getWidth() != 0
? item.parentFragment.getView().getWidth()
: item.parentFragment.getParentFragment() != null && item.parentFragment.getParentFragment().getView().getWidth() != 0
? item.parentFragment.getParentFragment().getView().getWidth() // YIKES
: UiUtils.MAX_WIDTH;
text.measure(
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {

View File

@@ -7,6 +7,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import java.util.List;
@@ -15,11 +16,13 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
public final Status status;
public List<StatusDisplayItem> filteredItems;
public Filter applyingFilter;
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems){
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, Filter applyingFilter){
super(parentID, parentFragment);
this.status=status;
this.filteredItems = filteredItems;
this.applyingFilter = applyingFilter;
}
@Override
@@ -41,7 +44,7 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(WarningFilteredStatusDisplayItem item) {
filteredItems = item.filteredItems;
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.status.filtered.get(item.status.filtered.size() -1).filter.title));
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.applyingFilter.title));
}
@Override

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(){
return requests.size();
}

View File

@@ -38,6 +38,7 @@ public class DiscoverInfoBannerHelper{
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
case BUBBLE_TIMELINE -> R.string.sk_bubble_timeline_info_banner;
});
}
}
@@ -63,6 +64,7 @@ public class DiscoverInfoBannerHelper{
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POST_NOTIFICATIONS,
// ACCOUNTS
// ACCOUNTS,
BUBBLE_TIMELINE
}
}

View File

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

View File

@@ -99,6 +99,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -137,10 +138,15 @@ public class UiUtils {
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");
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public static int MAX_WIDTH;
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) {
try {
if (GlobalUserPreferences.useCustomTabs) {
@@ -628,9 +634,9 @@ public class UiUtils {
if (relationship.blocking) {
button.setText(R.string.button_blocked);
secondaryStyle = true;
} else if (relationship.blockedBy) {
button.setText(R.string.button_follow);
secondaryStyle = false;
// } else if (relationship.blockedBy) {
// button.setText(R.string.button_follow);
// secondaryStyle = false;
} else if (relationship.requested) {
button.setText(R.string.button_follow_pending);
secondaryStyle = true;
@@ -644,7 +650,8 @@ public class UiUtils {
if (keepText) button.setText(textBefore);
button.setEnabled(!relationship.blockedBy);
// https://github.com/sk22/megalodon/issues/526
// button.setEnabled(!relationship.blockedBy);
int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
int styleRes = ta.getResourceId(0, 0);
@@ -941,7 +948,7 @@ public class UiUtils {
public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
Instance instance = session.getInstance();
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
}
@@ -1107,14 +1114,20 @@ public class UiUtils {
if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args);
} else if (!results.accounts.isEmpty()) {
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
if (launchBrowser) launchWebBrowser(context, url);
else
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
return;
}
Optional<Account> account = results.accounts.stream()
.filter(a -> uri.equals(Uri.parse(a.url))).findAny();
if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
Nav.go((Activity) context, ProfileFragment.class, args);
return;
}
if (launchBrowser) {
launchWebBrowser(context, url);
return;
}
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
@Override

View File

@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V;
public class ComposeMediaLayout extends ViewGroup{
private static final int MAX_WIDTH_DP=400;
private static final int GAP_DP=8;
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){
super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
}
@Override
@@ -30,7 +31,7 @@ public class ComposeMediaLayout extends ViewGroup{
int mode=MeasureSpec.getMode(widthMeasureSpec);
@SuppressLint("SwitchIntDef")
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);
default -> throw new IllegalArgumentException("unsupported measure mode");
};

View File

@@ -6,6 +6,8 @@ import android.view.View;
import android.widget.LinearLayout;
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.
*/
@@ -36,7 +38,8 @@ public class HeaderSubtitleLinearLayout extends LinearLayout{
}
View first=getChildAt(0);
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{
View first=getChildAt(0);

View File

@@ -6,13 +6,13 @@ import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V;
public class MediaGridLayout extends ViewGroup{
private static final String TAG="MediaGridLayout";
public static final int MAX_WIDTH=400; // dp
private static final int GAP=1; // dp
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
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){
super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
}
@Override
@@ -36,7 +36,7 @@ public class MediaGridLayout extends ViewGroup{
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 0);
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 offset=0;
@@ -74,10 +74,9 @@ public class MediaGridLayout extends ViewGroup{
if(tiledLayout==null)
return;
int maxWidth=V.dp(MAX_WIDTH);
int xOffset=0;
if(r-l>maxWidth){
xOffset=(r-l)/2-maxWidth/2;
if(r-l>UiUtils.MAX_WIDTH){
xOffset=(r-l)/2-UiUtils.MAX_WIDTH/2;
}
for(int i=0;i<getChildCount();i++){

File diff suppressed because one or more lines are too long

View File

@@ -6,54 +6,80 @@ import org.joinmastodon.android.model.Status;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StatusFilterPredicate implements Predicate<Status>{
private final List<Filter> filters;
private final Filter.FilterContext context;
private final Filter.FilterAction action;
private Filter applyingFilter;
public StatusFilterPredicate(List<Filter> filters){
this.filters=filters;
/**
* @param context null makes the predicate pass automatically
* @param action defines what the predicate should check:
* status should not be hidden or should not display with warning
*/
public StatusFilterPredicate(List<Filter> filters, Filter.FilterContext context, Filter.FilterAction action){
this.filters = filters;
this.context = context;
this.action = action;
}
public StatusFilterPredicate(String accountID, Filter.FilterContext context){
public StatusFilterPredicate(List<Filter> filters, Filter.FilterContext context){
this(filters, context, Filter.FilterAction.HIDE);
}
/**
* @param context null makes the predicate pass automatically
* @param action defines what the predicate should check:
* status 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());
this.context = context;
this.action = action;
}
/**
* @param context null makes the predicate pass automatically
*/
public StatusFilterPredicate(String accountID, Filter.FilterContext context){
this(accountID, context, Filter.FilterAction.HIDE);
}
/**
* @return whether the status should be displayed without being hidden/warned about.
* will always return true if the context is null.
* true = display this status,
* false = filter this status
*/
@Override
public boolean test(Status status){
if(status.filtered!=null){
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;
if (context == null) return true;
Stream<Filter> matchingFilters = status.filtered != null
// use server-provided per-status info (status.filtered) if available
? status.filtered.stream().map(f -> f.filter)
// or fall back to cached filters
: filters.stream().filter(filter -> filter.matches(status));
Optional<Filter> applyingFilter = matchingFilters
// discard expired filters
.filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now()))
// only apply filters for given context
.filter(filter -> filter.context.contains(context))
// treating filterAction = null (from filters list) as FilterAction.HIDE
.filter(filter -> filter.filterAction == null ? action == Filter.FilterAction.HIDE : filter.filterAction == action)
.findAny();
this.applyingFilter = applyingFilter.orElse(null);
return applyingFilter.isEmpty();
}
public boolean testWithWarning(Status status) {
if(status.filtered!=null){
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.WARN);
return !matches;
}
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
public Filter getApplyingFilter() {
return applyingFilter;
}
}

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">
<item>
<shape>
<solid android:color="?colorWindowBackground"/>
<corners android:topLeftRadius="12dp" android:topRightRadius="12dp"/>
<solid android:color="?colorM3Surface"/>
<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>
</item>
</layer-list>

View File

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

View File

@@ -0,0 +1,9 @@
<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="M11.772,3.744C14.114,1.4 17.913,1.4 20.256,3.744C22.539,6.027 22.597,9.692 20.431,12.046L20.243,12.243L11.443,21.041L11.407,21.072C9.945,22.388 7.691,22.344 6.284,20.936C4.965,19.617 4.843,17.555 5.918,16.098C5.941,16.052 5.969,16.009 6.002,15.968L6.056,15.908L6.143,15.82L6.284,15.672L6.287,15.675L13.723,8.221C13.989,7.954 14.405,7.93 14.699,8.147L14.783,8.22C15.05,8.485 15.075,8.902 14.857,9.196L14.785,9.28L7.19,16.893C6.473,17.769 6.522,19.063 7.34,19.881C8.169,20.71 9.488,20.749 10.364,19.999L19.197,11.168C20.952,9.411 20.952,6.562 19.195,4.804C17.493,3.102 14.766,3.049 12.999,4.645L12.831,4.804L12.819,4.819L3.282,14.355C2.989,14.648 2.515,14.648 2.222,14.355C1.955,14.089 1.931,13.672 2.149,13.378L2.222,13.294L11.771,3.744L11.772,3.744Z"
android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

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>

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.123 7.112c-0.263 0.262-0.587 0.455-0.943 0.562l-4.293 1.29c-0.529 0.159-1.086-0.141-1.245-0.67-0.058-0.194-0.056-0.402 0.006-0.594l1.361-4.228c0.11-0.34 0.3-0.65 0.552-0.903l7.133-7.121c1.258-1.256 3.297-1.255 4.554 0.002zm-3.494 1.06l-7.133 7.12c-0.084 0.085-0.147 0.188-0.184 0.301l-1.07 3.323 3.382-1.015c0.119-0.036 0.227-0.1 0.314-0.188L19 11.936c0.672-0.671 0.672-1.76 0.002-2.43-0.672-0.672-1.76-0.673-2.433-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>

View File

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

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