Compare commits

...

569 Commits

Author SHA1 Message Date
sk
8115578e94 boop version 2023-06-13 10:19:13 +02:00
sk
08dc122b6b tweak min/max heights 2023-06-13 10:07:30 +02:00
sk
e3199c009e hopefully fix typetoken crashes 2023-06-13 09:47:48 +02:00
sk
2e09010151 boop version 2023-06-12 20:44:25 +02:00
sk
3d79e87ec8 Merge remote-tracking branch 'upstream/l10n_master' 2023-06-12 20:43:22 +02:00
sk
09ed3c647a reduce max height for photos 2023-06-12 20:25:12 +02:00
EndermanCo
172515ba0a Translated using Weblate (Persian)
Currently translated at 23.2% (71 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-06-12 17:43:48 +00:00
Andrewblasco
a2f0fc8c87 Translated using Weblate (Spanish)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-06-12 17:43:48 +00:00
Andrewblasco
888cee4556 Translated using Weblate (Spanish)
Currently translated at 100.0% (305 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-12 17:43:48 +00:00
sk
c005e9bf18 apply upstream alt overlay changes 2023-06-12 19:43:13 +02:00
Eugen Rochko
64362968fc New translations strings.xml (Persian) 2023-06-12 19:10:22 +02:00
sk
320027ca9b merge upstream photo layout changes 2023-06-12 18:42:07 +02:00
sk
ad2678da7c Merge remote-tracking branch 'upstream/l10n_master' 2023-06-12 18:22:38 +02:00
sk22
c56c7448d0 Translated using Weblate (German)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2023-06-12 16:22:10 +00:00
sk
731e67725c fix errors in strings 2023-06-12 18:22:02 +02:00
sk
8178f81c85 update changelog 2023-06-12 18:08:40 +02:00
sk
6fbf00a132 add newline 2023-06-12 18:07:28 +02:00
sk
968cde9e4c clean up empty regional locales 2023-06-12 18:03:22 +02:00
sk
f2ab2acef7 Merge remote-tracking branch 'weblate/main' 2023-06-12 17:47:22 +02:00
sk
81d1ecc5f8 update readme, changelog 2023-06-12 17:47:10 +02:00
EndermanCo
513e6439ff Translated using Weblate (Persian)
Currently translated at 22.2% (68 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-06-12 15:35:16 +00:00
Oliebol
1f0108b14e Translated using Weblate (Dutch)
Currently translated at 84.5% (258 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-06-12 15:35:15 +00:00
Choukajohn
1433d0717e Translated using Weblate (French)
Currently translated at 100.0% (305 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-12 15:35:15 +00:00
sk
be91775f4b fix pinned posts showing post multiple times 2023-06-12 17:29:00 +02:00
sk
7b2f8d2be3 update readme 2023-06-12 16:40:52 +02:00
sk
54c386ccec update readme 2023-06-12 16:32:28 +02:00
sk
9d9e98959f change glitch url 2023-06-12 16:18:45 +02:00
sk
a3cd7224bd update readme 2023-06-12 16:17:08 +02:00
sk
6cf214f127 fix header disappearing in some cases
closes sk22#570
2023-06-12 15:39:38 +02:00
Eugen Rochko
b6976fb519 New translations strings.xml (Persian) 2023-06-12 15:36:23 +02:00
sk
871ada23ab don't display context until fragment transition finished 2023-06-12 14:33:09 +02:00
Eugen Rochko
040237de2b New translations strings.xml (Persian) 2023-06-12 14:30:32 +02:00
sk
29b2a25840 no error when opening url that looks like fedi url 2023-06-12 13:33:30 +02:00
sk
bd2f05be67 hide followers/following count if count is -1 2023-06-12 12:13:44 +02:00
sk22
ed075b276f Translated using Weblate (German)
Currently translated at 100.0% (305 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-11 23:48:58 +00:00
sk
dd25f3380a add setting to configure default value for "forward report"
closes sk22#565
2023-06-12 01:38:44 +02:00
sk22
1a2d1efa29 Added translation using Weblate (Persian) 2023-06-11 23:10:35 +00:00
sk
f3e1fa4b2b fix announcements loading infinitely
closes sk22#569
2023-06-12 01:01:45 +02:00
Eugen Rochko
fc307ff43f New translations strings.xml (Persian) 2023-06-11 21:52:27 +02:00
Eugen Rochko
04304b3397 New translations strings.xml (Persian) 2023-06-11 20:49:08 +02:00
sk
23f4b63195 Merge remote-tracking branch 'upstream/l10n_master' 2023-06-11 20:04:33 +02:00
sk22
edeae13dda Translated using Weblate (German)
Currently translated at 100.0% (304 of 304 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-11 18:04:03 +00:00
Oliebol
44558534e9 Translated using Weblate (Dutch)
Currently translated at 85.0% (256 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-06-11 17:49:51 +00:00
Linerly
2b760bb215 Translated using Weblate (Indonesian)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-11 17:49:51 +00:00
sk
44154a987d setting for "re:" reply CW prefix always or only to others
re: sk22#567
2023-06-11 19:46:18 +02:00
Eugen Rochko
7260db6668 New translations full_description.txt (Persian) 2023-06-11 19:41:48 +02:00
Eugen Rochko
1dadc51ddf New translations strings.xml (Persian) 2023-06-11 19:41:48 +02:00
Eugen Rochko
fe85351869 New translations strings.xml (Persian) 2023-06-11 18:34:16 +02:00
Eugen Rochko
74a83c6ac4 New translations strings.xml (Persian) 2023-06-11 17:37:10 +02:00
sk
acb1369e88 fix "show replies/boosts" filter not applied
closes sk22#566
2023-06-11 17:23:54 +02:00
sk
8e0d74f9c2 use appkit V.sp 2023-06-11 17:12:56 +02:00
Eugen Rochko
df77ba61ad New translations strings.xml (Persian) 2023-06-11 15:12:30 +02:00
Eugen Rochko
ed40f74d59 New translations strings.xml (Persian) 2023-06-11 13:57:15 +02:00
Eugen Rochko
42e26bef68 New translations strings.xml (Persian) 2023-06-11 12:54:18 +02:00
Eugen Rochko
af60adb55f New translations strings.xml (Indonesian) 2023-06-11 05:16:01 +02:00
sk
b94741feae boop version 2023-06-10 22:10:24 +02:00
sk
e43d6c35d8 update languages 2023-06-10 22:10:13 +02:00
sk
4a6f9e80b1 Merge remote-tracking branch 'weblate/main' 2023-06-10 22:03:41 +02:00
gallegonovato
ec02680507 Translated using Weblate (Spanish)
Currently translated at 99.6% (300 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-10 20:03:34 +00:00
sk22
5fc569a45a Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 20:03:34 +00:00
sk
4bc9c5691d Merge remote-tracking branch 'upstream/l10n_master' 2023-06-10 22:03:19 +02:00
sk
19b68855ac move permission definition to status privacy 2023-06-10 21:54:47 +02:00
sk
70fdfb612e fix issue on re-bind 2023-06-10 21:54:18 +02:00
sk
0a32c217d8 Merge branch 'main' into pr/FineFindus/557 2023-06-10 21:47:26 +02:00
Eugen Rochko
5dfa9237ad New translations short_description.txt (Persian) 2023-06-10 21:44:47 +02:00
sk
573ff75498 update ancestor when deleting post 2023-06-10 21:32:41 +02:00
sk
87c37df370 insert replied directly below status
closes sk22#558
2023-06-10 20:57:01 +02:00
sk22
7fb0944e66 Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 16:59:16 +00:00
sk
35c8a3d121 change strings 2023-06-10 18:59:05 +02:00
sk
9e58413d1a Merge remote-tracking branch 'weblate/main' 2023-06-10 18:54:32 +02:00
sk
90e60aef84 change auto-reveal cw wording
closes sk22#562
2023-06-10 18:53:38 +02:00
sk
8547ce05ed change auto-reveal cw wording
closes sk22#562
2023-06-10 18:52:01 +02:00
gicorada
0825faee5c Translated using Weblate (Italian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-06-10 16:42:23 +00:00
Linerly
d43a697df7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-10 16:42:23 +00:00
Choukajohn
3b742c4391 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
ihor_ck
a43a396043 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-10 16:42:23 +00:00
Choukajohn
bcb4fac553 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
sk
35bf858a83 auto-reveal equal spoilers in threads 2023-06-09 14:54:03 +02:00
sk
870bfaf08c don't use switch for android ids 2023-06-09 14:50:21 +02:00
sk
c4238fb19b keep revealed states when reloading
closes sk22#561
2023-06-09 13:27:25 +02:00
sk
ba7aeb358b fix thread status not clickable if filter revealed
closes sk22#554
2023-06-09 12:50:10 +02:00
sk
6f3fd4d454 fix opening browser twice
closes sk22#559
2023-06-09 12:47:32 +02:00
Eugen Rochko
c890195567 New translations strings.xml (Swedish) 2023-06-08 22:57:52 +02:00
Eugen Rochko
b50a327b17 New translations strings.xml (Swedish) 2023-06-08 17:47:06 +02:00
sk
97547f334f returning optionals for the optional god
closes sk22#555
2023-06-08 15:31:58 +02:00
sk
1ab953d819 fix spoiler button being hidden while editing
closes sk22#553
2023-06-08 15:03:04 +02:00
FineFindus
dbe7eb25ff Merge branch 'main' into feat/hide-non-boostable-boosts 2023-06-08 10:35:12 +02:00
FineFindus
45ecec09f5 feat: hide boost count on non-boostable statuses 2023-06-08 10:33:27 +02:00
FineFindus
9b4556d293 refactor: move boostable check to status 2023-06-08 10:33:27 +02:00
sk
307d483a56 add comment 2023-06-07 21:59:56 +02:00
sk
9612248695 remove unused imports 2023-06-07 21:51:17 +02:00
sk
1f63401e5b fix pixelfed post editing 2023-06-07 21:50:50 +02:00
sk
d35ec18a88 increase akkoma scheduled posts compatibility 2023-06-07 21:12:38 +02:00
sk
b93b1847c3 increase pixelfed compatibility 2023-06-07 21:12:30 +02:00
sk
cd46ed565f open browser if login redirects to website 2023-06-07 21:11:20 +02:00
sk
4a0e4edef8 load more if screen isn't full yet 2023-06-07 20:28:56 +02:00
sk
2ea7333daa only reload main status when refreshing
closes sk22#552
2023-06-07 18:45:50 +02:00
sk
fa7a66809d change profile loading error behavior 2023-06-07 16:09:41 +02:00
sk
71884ab760 don't show wrong relationship with remote accounts 2023-06-07 15:50:30 +02:00
sk
f31205c670 generalize lookup error handling 2023-06-07 15:45:22 +02:00
sk
0091ae87ce fix issues with external share
closes sk22#550
2023-06-07 15:17:44 +02:00
sk
ad13b1e927 bump version 2023-06-06 17:13:30 +02:00
sk
a354ea80ab Merge remote-tracking branch 'upstream/l10n_master' 2023-06-06 17:09:06 +02:00
sk22
9f65b8112a Translated using Weblate (German)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-06 15:08:53 +00:00
Choukajohn
6ac5d957fe Translated using Weblate (French)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-06 15:05:05 +00:00
sk
4258c55b88 implement fetching listings from remote instances 2023-06-06 17:04:29 +02:00
sk
969f29e2e9 support string res for small text item 2023-06-06 16:55:20 +02:00
sk
68921d0f0b fix footer being too close to next header 2023-06-06 14:23:34 +02:00
Eugen Rochko
c4ac4ee173 New translations strings.xml (Swedish) 2023-06-06 13:23:46 +02:00
Eugen Rochko
659b4e2fcd New translations strings.xml (Swedish) 2023-06-06 11:55:59 +02:00
sk
24e5bda8d3 fix profile options icons 2023-06-06 10:30:32 +02:00
Eugen Rochko
02b1ad8d7a New translations strings.xml (Swedish) 2023-06-06 00:56:52 +02:00
Eugen Rochko
47eeb01b75 New translations strings.xml (Turkish) 2023-06-05 21:08:28 +02:00
ihor_ck
4288814138 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-05 16:09:08 +00:00
sk
ac4458e106 fix badged toolbar icons with new appkit version 2023-06-05 18:07:22 +02:00
sk
3d24b2de10 add status counters event listener to notifications 2023-06-05 16:19:55 +02:00
sk
ed994b23e9 fix wrong views' states being modified
closes sk22#549
2023-06-05 16:12:07 +02:00
sk
8c4678aba5 fix updated main status not being applied 2023-06-05 16:05:58 +02:00
Eugen Rochko
3d5fb2dfea New translations strings.xml (Swedish) 2023-06-05 15:00:47 +02:00
Eugen Rochko
ef6238b593 New translations strings.xml (Swedish) 2023-06-05 13:45:02 +02:00
Eugen Rochko
bc9bec3d66 New translations strings.xml (Swedish) 2023-06-05 12:47:38 +02:00
sk
d16e199dd1 scroll up when posting on profile fragment
closes sk22#546
2023-06-05 11:48:40 +02:00
sk
a9c2df2e83 do copy spoilerRevealed on clone
closes sk22#547
2023-06-05 11:26:36 +02:00
sk
4673a4b9f7 add missing database column in post notification table 2023-06-05 11:22:01 +02:00
Grishka
d4a5286895 Fix #553 2023-06-04 23:47:08 +02:00
Grishka
1b4579346b Fix #548 2023-06-04 23:39:06 +02:00
sk
0665b8dd3b fix incompatibility with upstream bugfix 2023-06-04 23:33:04 +02:00
Grishka
853124e2ce Fix it again 2023-06-04 23:32:13 +02:00
Grishka
5dcd6e5a0d Fix it again 2023-06-04 23:31:12 +02:00
Grishka
6f25c8be0f Fix #583 2023-06-04 23:26:48 +02:00
sk
1db4b1319e use latest appkit version 2023-06-04 23:26:39 +02:00
Grishka
76a97fcb47 Fix #591 2023-06-04 23:20:29 +02:00
sk
4baaa39f35 bump version 2023-06-04 23:16:32 +02:00
sk
52f025ae5a Merge remote-tracking branch 'upstream/l10n_master' 2023-06-04 23:14:49 +02:00
sk22
14b805e883 Translated using Weblate (German)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-04 21:13:53 +00:00
sk
433a7b15fe change bubble string 2023-06-04 23:03:29 +02:00
sk
6c8cbbc34a Merge remote-tracking branch 'weblate/main' 2023-06-04 22:58:56 +02:00
sk
d4fbb298c1 use sp for reply line inline icons 2023-06-04 22:57:06 +02:00
sk
2aeb5f03d6 remove unused sp drawables 2023-06-04 22:32:54 +02:00
sk
6522403c37 fix footer text margins 2023-06-04 22:12:45 +02:00
sk
f090ca7f75 use sp for scaled footer 2023-06-04 21:08:45 +02:00
sk
2f02a238df refresh updated main status 2023-06-04 20:56:44 +02:00
sk
0d5fa97800 fix wrong index 2023-06-04 20:40:27 +02:00
sk
b102deaee1 don't let interaction counts go negative 2023-06-04 19:08:18 +02:00
Eryk Michalak
968b2ee460 Translated using Weblate (Polish)
Currently translated at 97.9% (286 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-06-04 10:37:37 +00:00
Andrewblasco
890340de94 Translated using Weblate (Spanish)
Currently translated at 99.6% (291 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-04 10:37:37 +00:00
sk
4ca1a7b29e fix index out of bounds exception 2023-06-04 11:45:12 +02:00
sk
5432f2590c fine-tune footer layout 2023-06-04 05:00:48 +02:00
sk
60ccf5cf0a only shift selection box if footer is present 2023-06-04 04:15:15 +02:00
sk
bc717f5b10 tweak footer margins and hitboxes 2023-06-04 04:08:38 +02:00
sk
486eef21dd responsive footer width 2023-06-04 02:16:47 +02:00
sk
44a4d02815 remove redundant suppress annotation 2023-06-04 01:36:38 +02:00
sk
336a8194bd fix settings button binding not reset visibility and events 2023-06-04 01:36:05 +02:00
sk
7859f4cd05 support parsing mailto links
i mean, why not - if github decided every @username@example.social is actually
an email address, might as well support sharing that mailto link to megalodon
2023-06-03 23:39:43 +02:00
sk
37622ba9ce generalize notification handling, open reports in browser 2023-06-03 22:47:20 +02:00
sk
7a6af89375 fix unwanted fab animation when scrolling and switching tab
closes sk22#528
2023-06-03 22:07:58 +02:00
sk
056bfaacfe fix fab being hidden when scrolling to top
closes sk22#528
2023-06-03 21:54:57 +02:00
sk
6684311ec5 fix button state/char counter not updating when empty
closes sk22#537
2023-06-03 21:24:40 +02:00
sk
11943571ad fix thread replies not added to data
closes sk22#543
2023-06-03 21:10:45 +02:00
sk
f696fcd412 simplify ancestry code 2023-06-03 21:03:47 +02:00
sk
2919e109ca remove unused member 2023-06-03 20:40:29 +02:00
sk
995f478708 allow sharing @-handles with megalodon
closes sk22#540
2023-06-03 20:31:00 +02:00
sk
fb8764bcd7 refactor ancestry, fix case regarding reply line
fix case where reply line was removed despite having no direct ancestor
2023-06-02 22:08:03 +02:00
Eugen Rochko
d7f73e02c5 New translations strings.xml (German) 2023-06-02 20:22:32 +02:00
Eugen Rochko
e897b3af57 New translations strings.xml (German) 2023-06-02 19:23:23 +02:00
sk
e04fd8a004 bump version 2023-06-02 19:10:08 +02:00
sk
ada70ae1b5 Merge remote-tracking branch 'upstream/l10n_master' 2023-06-02 19:09:44 +02:00
Espasant3
5fdec0900e Translated using Weblate (Galician)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-06-02 17:09:12 +00:00
sk
56a93288c4 reimplement thread ancestry 2023-06-02 19:05:18 +02:00
sk
02e3421f98 fix null pointer exception 2023-06-02 19:03:29 +02:00
Eugen Rochko
fdbf331432 New translations strings.xml (Bengali) 2023-06-02 18:01:03 +02:00
Eugen Rochko
aed86ac6f0 New translations strings.xml (Bengali) 2023-06-02 16:50:10 +02:00
Eugen Rochko
3a13d4d6c0 New translations strings.xml (Bengali) 2023-06-02 05:45:51 +02:00
Eugen Rochko
f5336564d0 New translations strings.xml (Bengali) 2023-06-02 04:39:21 +02:00
sk
1ce49c68fe bump version 2023-06-02 01:45:55 +02:00
sk
d37e880993 don't close sheet after logging out 2023-06-02 01:45:30 +02:00
sk
6fdb81a01f Merge remote-tracking branch 'upstream/l10n_master' 2023-06-02 01:37:14 +02:00
ihor_ck
f9d6827572 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-01 23:36:24 +00:00
Linerly
10bf72b9ff Translated using Weblate (Indonesian)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-01 23:36:24 +00:00
Choukajohn
800f929a15 Translated using Weblate (French)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-01 23:36:24 +00:00
sk
bfcff1e19f fix null pointer exception
closes sk22#539
2023-06-02 01:34:31 +02:00
sk
f373e7df3e put code in method, add todo 2023-06-02 01:16:21 +02:00
sk
3985de5b14 visually connect descendant replies in threads
closes sk22#256
closes sk22#510
2023-06-02 00:55:42 +02:00
sk
e175a721d4 remove additional padding with translate button 2023-06-02 00:17:50 +02:00
sk
d9784ebc31 use /about web uri for akkoma 2023-06-01 19:28:46 +02:00
sk
f241092277 use isInstanceAkkoma() 2023-06-01 19:22:01 +02:00
sk
0702703d78 normalize instance uri 2023-06-01 19:13:03 +02:00
sk
2c4504bad3 fix current fragment detection 2023-06-01 19:12:50 +02:00
Eugen Rochko
07ca5a8b77 New translations strings.xml (Bengali) 2023-06-01 19:07:10 +02:00
sk
798a43906f denser account switcher
this one's for @experiencersinternational
2023-06-01 18:46:53 +02:00
sk
41cb0f2e09 implement assist url in instance rules 2023-06-01 18:38:45 +02:00
sk
e12c0fb81f bump version 2023-06-01 18:10:30 +02:00
Oliebol
ac39f119e2 Translated using Weblate (Dutch)
Currently translated at 88.4% (253 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-06-01 16:08:59 +00:00
Espasant3
016faf3df0 Translated using Weblate (Galician)
Currently translated at 99.6% (285 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-06-01 16:08:59 +00:00
sk
b2d6879282 reimplement assist content 2023-06-01 18:02:33 +02:00
Eugen Rochko
6926a212f4 New translations strings.xml (Bengali) 2023-06-01 17:46:40 +02:00
sk
89afc05d5c Merge branch 'main' into pr/FineFindus/530 2023-06-01 16:32:04 +02:00
Eugen Rochko
936f39161b New translations strings.xml (German) 2023-05-31 20:11:48 +02:00
sk
ee20ee0722 include mentions in reply from notification
closes sk22#536
2023-05-31 12:42:48 +02:00
sk
02f9f8c8ea always update active account, but not others 2023-05-31 12:23:04 +02:00
sk
de3a252884 not as huge share sheet heading 2023-05-31 10:12:12 +02:00
sk
5e7a00de3e fix crash when logging out active account 2023-05-31 10:05:31 +02:00
sk
2858aeb55e only set last account id if creating new activity 2023-05-31 09:45:24 +02:00
sk
357104efa9 set checked on basis of fragment's account id
closes sk22#538
2023-05-31 09:42:29 +02:00
sk
bb8027c7ef open externally opened content in main activity
closes sk22#533
2023-05-31 01:44:00 +02:00
sk
f9dd787009 fix rules crashing the app
closes sk22#535
2023-05-31 00:19:38 +02:00
sk
e005731ba6 theming support for m3 colors 2023-05-30 23:52:26 +02:00
sk
18ae3f4f61 Merge branch 'pr/FineFindus/531'
Co-authored-by: FineFindus <63370021+finefindus@users.noreply.github.com>
2023-05-30 22:46:08 +02:00
sk
10dfe0327e use new account switcher 2023-05-30 22:42:56 +02:00
Jacoco
1d1e921137 Fix GoToSocial crash when markers are null (#529) 2023-05-30 19:07:34 +02:00
sk
0985a4c968 getInstance returns optional 2023-05-30 18:57:17 +02:00
sk
8df589c103 safer file writing 2023-05-30 18:56:55 +02:00
FineFindus
71b6b2f451 feat(share): add option open URL 2023-05-30 16:33:09 +02:00
FineFindus
d85940ded8 fix: re-add removed imports 2023-05-30 16:28:04 +02:00
LucasGGamerM
e9e491c0b0 feat: redesign account picker sheet 2023-05-30 16:25:04 +02:00
FineFindus
c73562fb75 feat(external-share): use AccountSwitcherSheet 2023-05-30 16:24:43 +02:00
FineFindus
3feacb59c8 feat(external-share): use transparent background 2023-05-30 16:19:28 +02:00
FineFindus
a033d711c1 feat: show page URL in recents 2023-05-30 15:40:20 +02:00
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
293 changed files with 9650 additions and 2807 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

@@ -10,44 +10,47 @@
&nbsp;
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly wont ever be implemented, such as the federated timeline, unlisted posting and an image description viewer.
> A fork of the [Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app, focusing on [Glitch](https://glitch-soc.github.io/docs) compatibility, a pretty UI and adding new features that I feel make using the Fediverse a more pleasent experience.
## Key features
### **Unlisted posting**
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).**
<details>
<p><summary>Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).</summary></p>
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in peoples Home timelines, but only if they follow you or someone they follow reblogged/replied to your post.
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
</details>
### **Federated timeline**
**This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.**
<details>
<p><summary>This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.</summary></p>
Despite being one of the main features of federated social media, the Federated timeline wasnt included in the official Mastodon app supposedly, because this conflicts with Googles safety requirements for apps on the Play Store.
Thats one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
</details>
### **Customizable timelines**
<details>
<p><summary>You can customize Megalodons home tab and not only add local and federated timelines, but also pin lists and hashtags.</summary></p>
Even better: You can rename every timeline however you please and pick a distinct icon for each timeline. This way, you can pin the hashtag “#Caturday”, rename your timeline to “CUTENESS OVERLOAD” and set <img src="img/ic_fluent_animal_cat_24_regular.svg" alt="Cat icon from Microsoft Fluent UI icons"> as its icon. :3 You can find the timelines editor by opening your home tab, tapping the `⋮` button in the top right and going to “Edit timelines”.
</details>
### **Draft and schedule posts**
**Allows for preparing a post and scheduling it to send it automatically at a specific time.**
<details>
<p><summary>
Allows to prepare a post and schedule it to send it automatically at a specific time.</summary></p>
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
### **Image description viewer**
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
This is important to **ensure the content youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
### **Pinning posts**
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in peoples profiles shows all the posts they pinned.**
On the Fediverse, its quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
</details>
## Installation
@@ -88,7 +91,7 @@ Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastod
## Release variants
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page.
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page. When downloading a pre-release, expect to see unfinished features and bugs. If you dont want that, just download the [latest full release](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk).
**`megalodon.apk`**
@@ -108,11 +111,11 @@ Variant without the integrated updater. This is the variant to be published to F
### Translation
As with the source code, the translation is sourced from the official project, which you can contribute to on the official “**Mastodon for Android**” Crowdin project: https://crowdin.com/project/mastodon-for-android
The translation for the base of the app is sourced from the upstream **Mastodon for Android** project, which you can contribute to on its Crowdin project: [https://crowdin.com/project/mastodon-for-android](https://crowdin.com/project/mastodon-for-android)
There's also a handful of custom strings exclusive to this projects that would need to be translated. You can help translate **Megalodon** on Weblate: https://translate.codeberg.org/projects/megalodon/
There's also a bunch of custom strings exclusive to this project that need to be translated. You can help translate **Megalodon** on Weblate: [https://translate.codeberg.org/projects/megalodon](https://translate.codeberg.org/projects/megalodon)
[![Translation status](https://translate.codeberg.org/widgets/megalodon/-/horizontal-auto.svg)](https://translate.codeberg.org/engage/megalodon/)
[![Translation status](https://translate.codeberg.org/widgets/megalodon/-/horizontal-auto.svg)](https://translate.codeberg.org/engage/megalodon)
---
@@ -154,6 +157,10 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
* Support for local-only posts
* Support for copying the URL to posts/accounts/… in Pixel launchers Recent apps view
* Compatibility for Akkoma Bubble timeline
* Listings of followers/following/favorites/boosts can be loaded from the origin instance (theres an option to disable this in in the settings)
* Allow opening posts/accounts in-app by sharing a URL/handle to Megalodon (Originally implemented in [Moshidon](https://github.com/LucasGGamerM/moshidon), [PR](https://github.com/sk22/megalodon/pull/531))
### Behavior
@@ -179,6 +186,8 @@ There's also a handful of custom strings exclusive to this projects that would n
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
* [Support admin notifications](https://github.com/sk22/megalodon/commit/c12a6eaee6b609bc53eb0a45d9199f37d5241801) and [notifications for edited reblogged posts](https://github.com/sk22/megalodon/commit/900e8fb2e9353002c16d15e06b78d2731e121601)
* [Android file opener added back in addition to image picker](https://github.com/sk22/megalodon/commit/3a6ace53d5ab01e28077c9c930cb6ed487b78031)
* [Replies are inserted below the replied-to post in thread view](https://github.com/sk22/megalodon/commit/87c37df370ec24aeea0d2dbaeb29468aa4fb5808)
* Option to auto-reveal equal content warnings in threads
### Visual
@@ -196,6 +205,7 @@ There's also a handful of custom strings exclusive to this projects that would n
* Scale text according to system settings
* Header in timeline for followed hashtags
* [Indicator for missing alt texts](https://github.com/sk22/megalodon/commit/c0c276f03e793b78c478c17dfdef24a66ef7cedb)
* Visually grouped (by removing divider lines and reducing padding) threaded replies in thread view
## Building
@@ -206,6 +216,8 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
./gradlew assembleRelease
```
Note that Megalodon might be depending on an in-development version of [AppKit](https://github.com/grishka/appkit) a library by Mastodon for Androids developer. In case the used AppKit version isnt published to Maven Central yet, you might have to clone, build and publish it to your local Maven repository. For more information, see [this GitHub issue](https://github.com/mastodon/mastodon-android/issues/375#issuecomment-1507678585).
## License
This project is released under the [GPL-3 License](./LICENSE).

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

@@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<style>
path { fill: black; }
@media (prefers-color-scheme: dark) {
path { fill: white; }
}
</style>
<path d="M15.4925 3.50673C14.652 3.58251 13.9933 4.28929 13.9933 5.15V10C13.9933 10.4142 13.6577 10.75 13.2437 10.75C11.8002 10.75 10.7863 11.3378 10.0365 12.238C9.26389 13.1656 8.7607 14.444 8.44554 15.7954C8.13254 17.1376 8.01871 18.4912 7.98453 19.5172C7.97182 19.8987 7.9702 20.2324 7.97313 20.5H14.9928V19.75C14.9928 18.5074 13.986 17.5 12.744 17.5H11.4947C11.0807 17.5 10.7451 17.1642 10.7451 16.75C10.7451 16.3358 11.0807 16 11.4947 16H12.744C14.8139 16 16.4919 17.6789 16.4919 19.75V20.5H17.2415C17.6555 20.5 17.9911 20.1642 17.9911 19.75V9.75C17.9911 9.33579 18.3267 9 18.7407 9H19.2472C20.2264 9 20.8249 7.92404 20.309 7.09132L19.6893 6.09132C19.4615 5.72367 19.0599 5.5 18.6275 5.5H16.2421C15.8281 5.5 15.4925 5.16421 15.4925 4.75V3.50673ZM6.47388 20.5C6.47098 20.2156 6.47293 19.8655 6.4862 19.4672C6.52229 18.3838 6.64271 16.9249 6.98559 15.4546C7.32631 13.9935 7.90065 12.4594 8.88484 11.2777C9.75681 10.2307 10.9399 9.47669 12.4942 9.29318V5.15C12.4942 3.4103 13.9037 2 15.6424 2C16.3876 2 16.9916 2.60442 16.9916 3.35V4H18.6275C19.5787 4 20.4622 4.49207 20.9634 5.30092L21.5831 6.30092C22.6749 8.06291 21.4985 10.32 19.4903 10.4898V19.75C19.4903 20.9926 18.4835 22 17.2415 22H7.24708L7.24537 22H5.79625C3.69964 22 2 20.2994 2 18.2016C2 17.2395 2.36489 16.3133 3.02098 15.6099L4.15612 14.393C4.92005 13.5741 5.17521 12.4027 4.82117 11.3399C4.67114 10.8896 4.41837 10.4804 4.08288 10.1447L2.96914 9.03042C2.67641 8.73753 2.67639 8.26266 2.96912 7.96976C3.26184 7.67686 3.73645 7.67685 4.02919 7.96974L5.14293 9.08405C5.643 9.58438 6.01977 10.1943 6.2434 10.8656C6.77114 12.4497 6.3908 14.1958 5.25209 15.4165L4.11695 16.6334C3.71996 17.059 3.49916 17.6195 3.49916 18.2016C3.49916 19.471 4.52761 20.5 5.79625 20.5H6.47388Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

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 94
versionName "1.2.3+fork.94"
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'
@@ -64,8 +75,8 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
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 'me.grishka.appkit:appkit:1.2.8'
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

@@ -46,3 +46,7 @@
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }
-keep class com.google.gson.reflect.TypeToken
-keep class * extends com.google.gson.reflect.TypeToken
-keep public class * implements java.lang.reflect.Type

View File

@@ -0,0 +1,113 @@
package org.joinmastodon.android.fragments;
import static org.junit.Assert.*;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.junit.Test;
import java.time.Instant;
import java.util.List;
public class ThreadFragmentTest {
private Status fakeStatus(String id, String inReplyTo) {
Status status = Status.ofFake(id, null, null);
status.inReplyToId = inReplyTo;
return status;
}
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
return new ThreadFragment.NeighborAncestryInfo(s, d, a);
}
@Test
public void mapNeighborhoodAncestry() {
StatusContext context = new StatusContext();
context.ancestors = List.of(
fakeStatus("oldest ancestor", null),
fakeStatus("younger ancestor", "oldest ancestor")
);
Status mainStatus = fakeStatus("main status", "younger ancestor");
context.descendants = List.of(
fakeStatus("first reply", "main status"),
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("another reply", "main status")
);
List<ThreadFragment.NeighborAncestryInfo> neighbors =
ThreadFragment.mapNeighborhoodAncestry(mainStatus, context);
assertEquals(List.of(
fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null),
fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)),
fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)),
fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus),
fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)),
fakeInfo(context.descendants.get(2), null, context.descendants.get(1)),
fakeInfo(context.descendants.get(3), null, null)
), neighbors);
}
@Test
public void maybeApplyMainStatus() {
ThreadFragment fragment = new ThreadFragment();
fragment.contextInitiallyRendered = true;
fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH);
Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH);
update1.editedAt = Instant.ofEpochSecond(1);
fragment.updatedStatus = update1;
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.maybeApplyMainStatus();
assertEquals("fired update event", update1, event1.status);
assertEquals("updated main status", update1, fragment.mainStatus);
Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH);
update2.favouritesCount = 123;
fragment.updatedStatus = update2;
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.maybeApplyMainStatus();
assertEquals("only fired counter update event", update2.id, event2.id);
assertEquals("updated counter is correct", 123, event2.favorites);
assertEquals("updated main status", update2, fragment.mainStatus);
Status update3 = Status.ofFake("123456", "whatever", Instant.EPOCH);
fragment.contextInitiallyRendered = false;
fragment.updatedStatus = update3;
assertNull("no update when context hasn't been rendered", fragment.maybeApplyMainStatus());
}
@Test
public void sortStatusContext() {
StatusContext context = new StatusContext();
context.ancestors = List.of(
fakeStatus("younger ancestor", "oldest ancestor"),
fakeStatus("oldest ancestor", null)
);
context.descendants = List.of(
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("first reply", "main status"),
fakeStatus("another reply", "main status")
);
ThreadFragment.sortStatusContext(
fakeStatus("main status", "younger ancestor"),
context
);
List<Status> expectedAncestors = List.of(
fakeStatus("oldest ancestor", null),
fakeStatus("younger ancestor", "oldest ancestor")
);
List<Status> expectedDescendants = List.of(
fakeStatus("first reply", "main status"),
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("another reply", "main status")
);
// TODO: ??? i have no idea how this code works. it certainly doesn't return what i'd expect
}
}

View File

@@ -0,0 +1,106 @@
package org.joinmastodon.android.ui.utils;
import static org.junit.Assert.*;
import android.util.Pair;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Optional;
public class UiUtilsTest {
@BeforeClass
public static void createDummySession() {
Instance dummyInstance = new Instance();
dummyInstance.uri = "test.tld";
Account dummyAccount = new Account();
dummyAccount.id = "123456";
AccountSessionManager.getInstance().addAccount(dummyInstance, null, dummyAccount, null, null);
}
@AfterClass
public static void cleanUp() {
AccountSessionManager.getInstance().removeAccount("test.tld_123456");
}
@Test
public void parseFediverseHandle() {
assertEquals(
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
UiUtils.parseFediverseHandle("megalodon@floss.social")
);
assertEquals(
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
UiUtils.parseFediverseHandle("@megalodon@floss.social")
);
assertEquals(
Optional.of(Pair.create("megalodon", Optional.empty())),
UiUtils.parseFediverseHandle("@megalodon")
);
assertEquals(
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
UiUtils.parseFediverseHandle("mailto:megalodon@floss.social")
);
assertEquals(
Optional.empty(),
UiUtils.parseFediverseHandle("megalodon")
);
assertEquals(
Optional.empty(),
UiUtils.parseFediverseHandle("this is not a fedi handle")
);
assertEquals(
Optional.empty(),
UiUtils.parseFediverseHandle("not@a-domain")
);
}
@Test
public void acctMatches() {
assertTrue("local account, domain not specified", UiUtils.acctMatches(
"test.tld_123456",
"someone",
"someone",
null
));
assertTrue("domain not specified", UiUtils.acctMatches(
"test.tld_123456",
"someone@somewhere.social",
"someone",
null
));
assertTrue("local account, domain specified, different casing", UiUtils.acctMatches(
"test.tld_123456",
"SomeOne",
"someone",
"Test.TLD"
));
assertFalse("username doesn't match", UiUtils.acctMatches(
"test.tld_123456",
"someone-else@somewhere.social",
"someone",
"somewhere.social"
));
assertFalse("domain doesn't match", UiUtils.acctMatches(
"test.tld_123456",
"someone@somewhere.social",
"someone",
"somewhere.else"
));
}
}

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"/>
@@ -47,7 +62,8 @@
<data android:scheme="megalodon-android-auth" android:host="callback"/>
</intent-filter>
</activity>
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
android:theme="@style/TransparentDialog">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>

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

@@ -3,20 +3,25 @@ package org.joinmastodon.android;
import android.app.Fragment;
import android.content.ClipData;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import android.widget.Toast;
import org.joinmastodon.android.api.MastodonAPIRequest;
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.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
@@ -27,18 +32,52 @@ public class ExternalShareActivity extends FragmentStackActivity{
UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);
boolean isOpenable = isFediUrl || fediHandle.isPresent();
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){
if (sessions.isEmpty()){
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
finish();
}else if(sessions.size()==1){
} else if (isOpenable || sessions.size() > 1) {
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
sheet.setOnClick((accountId, open) -> {
if (open && text.isPresent()) {
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
if (clazz == null) {
Toast.makeText(this, R.string.sk_open_in_app_failed, Toast.LENGTH_SHORT).show();
// TODO: do something about the window getting leaked
sheet.dismiss();
finish();
return;
}
args.putString("fromExternalShare", clazz.getSimpleName());
Intent intent = new Intent(this, MainActivity.class);
intent.putExtras(args);
finish();
startActivity(intent);
};
fediHandle
.<MastodonAPIRequest<?>>map(handle ->
UiUtils.lookupAccountHandle(this, accountId, handle, callback))
.or(() ->
UiUtils.lookupURL(this, accountId, text.get(), callback))
.ifPresent(req ->
req.wrapProgress(this, R.string.loading, true, d -> {
UiUtils.transformDialogForLookup(this, accountId, isFediUrl ? text.get() : null, d);
d.setOnDismissListener((x) -> finish());
}));
} else {
openComposeFragment(accountId);
}
});
sheet.show();
} else if (sessions.size() == 1) {
openComposeFragment(sessions.get(0).getID());
}else{
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
UiUtils.pickAccount(this, null, R.string.choose_account, 0,
session -> openComposeFragment(session.getID()),
b -> b.setOnCancelListener(d -> finish())
);
}
}
}
@@ -51,9 +90,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 +125,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;
@@ -39,23 +40,31 @@ public class GlobalUserPreferences{
public static boolean showAltIndicator;
public static boolean showNoAltIndicator;
public static boolean enablePreReleases;
public static boolean prefixRepliesWithRe;
public static PrefixRepliesMode prefixReplies;
public static boolean bottomEncoding;
public static boolean collapseLongPosts;
public static boolean spectatorMode;
public static boolean autoHideFab;
public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static boolean allowRemoteLoading;
public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> accountsInGlitchMode;
public static Set<String> accountsWithContentTypesEnabled;
public static Map<String, ContentType> accountsDefaultContentTypes;
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();
/**
* Pleroma
@@ -72,6 +81,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);
@@ -95,13 +114,14 @@ public class GlobalUserPreferences{
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name()));
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false);
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 +129,20 @@ 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<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
if (prefs.contains("prefixRepliesWithRe")) {
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
? PrefixRepliesMode.TO_OTHERS : PrefixRepliesMode.NEVER;
prefs.edit()
.putString("prefixReplies", prefixReplies.name())
.remove("prefixRepliesWithRe")
.apply();
}
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -140,7 +174,7 @@ public class GlobalUserPreferences{
.putBoolean("showAltIndicator", showAltIndicator)
.putBoolean("showNoAltIndicator", showNoAltIndicator)
.putBoolean("enablePreReleases", enablePreReleases)
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
.putString("prefixReplies", prefixReplies.name())
.putBoolean("collapseLongPosts", collapseLongPosts)
.putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab)
@@ -148,6 +182,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 +190,11 @@ public class GlobalUserPreferences{
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault)
.apply();
}
@@ -174,5 +214,16 @@ public class GlobalUserPreferences{
LIGHT,
DARK
}
}
public enum AutoRevealMode {
NEVER,
THREADS,
DISCUSSIONS
}
public enum PrefixRepliesMode {
NEVER,
ALWAYS,
TO_OTHERS
}
}

View File

@@ -2,11 +2,14 @@ package org.joinmastodon.android;
import android.Manifest;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
@@ -20,12 +23,13 @@ import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity{
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
@@ -35,10 +39,18 @@ public class MainActivity extends FragmentStackActivity{
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
AccountSessionManager.getInstance()
.setLastActiveAccountID(intent.getStringExtra("account"));
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
AccountSessionManager.getInstance().getLastActiveAccount());
showFragmentForExternalShare(intent.getExtras());
return;
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
@@ -52,6 +64,7 @@ public class MainActivity extends FragmentStackActivity{
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
@@ -75,11 +88,12 @@ public class MainActivity extends FragmentStackActivity{
@Override
protected void onNewIntent(Intent intent){
super.onNewIntent(intent);
if(intent.getBooleanExtra("fromNotification", false)){
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras());
else if (intent.getBooleanExtra("fromNotification", false)) {
String accountID=intent.getStringExtra("accountID");
AccountSession accountSession;
try{
accountSession=AccountSessionManager.getInstance().getAccount(accountID);
AccountSessionManager.getInstance().getAccount(accountID);
}catch(IllegalStateException x){
return;
}
@@ -103,23 +117,24 @@ public class MainActivity extends FragmentStackActivity{
}
private void showFragmentForNotification(Notification notification, String accountID){
Fragment fragment;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("_can_go_back", true);
try{
notification.postprocess();
}catch(ObjectValidationException x){
Log.w("MainActivity", x);
return;
}
if(notification.status!=null){
fragment=new ThreadFragment();
args.putParcelable("status", Parcels.wrap(notification.status));
}else{
fragment=new ProfileFragment();
args.putParcelable("profileAccount", Parcels.wrap(notification.account));
}
UiUtils.showFragmentForNotification(this, notification, accountID, null);
}
private void showFragmentForExternalShare(Bundle args) {
String className = args.getString("fromExternalShare");
Fragment fragment = switch (className) {
case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment();
default -> null;
};
if (fragment == null) return;
args.putBoolean("_can_go_back", true);
fragment.setArguments(args);
showFragment(fragment);
}
@@ -153,18 +168,40 @@ public class MainActivity extends FragmentStackActivity{
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
);
Bundle currentArgs = currentFragment.getArguments();
if (this.fragmentContainers.size() == 1
&& currentArgs != null
&& currentArgs.getBoolean("_can_go_back", false)
&& currentArgs.containsKey("account")) {
if (fragmentContainers.size() != 1
|| currentArgs == null
|| !currentArgs.getBoolean("_can_go_back", false)) {
super.onBackPressed();
return;
}
if (currentArgs.getBoolean("_finish_on_back", false)) {
finish();
} else if (currentArgs.containsKey("account")) {
Bundle args = new Bundle();
args.putString("account", currentArgs.getString("account"));
args.putString("tab", "notifications");
if (getIntent().getBooleanExtra("fromNotification", false)) {
args.putString("tab", "notifications");
}
Fragment fragment=new HomeFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
} else {
super.onBackPressed();
}
}
public Fragment getCurrentFragment() {
for (int i = fragmentContainers.size() - 1; i >= 0; i--) {
FrameLayout fl = fragmentContainers.get(i);
if (fl.getVisibility() == View.VISIBLE) {
return getFragmentManager().findFragmentById(fl.getId());
}
}
return null;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
super.onProvideAssistContent(assistContent);
Fragment fragment = getCurrentFragment();
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
}
}

View File

@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
@Override
public void onSuccess(Token token){
new GetOwnAccount()
// in case the instance (looking at pixelfed) wants to redirect to a
// website, we need to pass a context so we can launch a browser
.setContext(OAuthActivity.this)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account account){

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

@@ -1,5 +1,7 @@
package org.joinmastodon.android;
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@@ -25,6 +27,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.NotificationAction;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification;
@@ -33,6 +36,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@@ -273,16 +277,35 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
// copied from ComposeFragment - TODO: generalize?
ArrayList<String> mentions=new ArrayList<>();
Status status = notification.status;
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!status.account.id.equals(ownID))
mentions.add('@'+status.account.acct);
for(Mention mention:status.mentions){
if(mention.id.equals(ownID))
continue;
String m='@'+mention.acct;
if(!mentions.contains(m))
mentions.add(m);
}
String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
CreateStatus.Request req=new CreateStatus.Request();
req.status = input.toString();
req.status = initialText + input.toString();
req.language = preferences.postingDefaultLanguage;
req.visibility = preferences.postingDefaultVisibility;
req.inReplyToId = notification.status.id;
if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){
if (!notification.status.spoilerText.isEmpty() &&
(GlobalUserPreferences.prefixReplies == ALWAYS
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
&& !notification.status.spoilerText.startsWith("re: ")) {
req.spoilerText = "re: " + notification.status.spoilerText;
}
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<Status>() {
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
@Override
public void onSuccess(Status status) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

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;
@@ -37,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=3;
private static final int DB_VERSION=4;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
@@ -62,7 +61,7 @@ public class CacheController{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
@@ -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);
}
@@ -115,7 +112,7 @@ public class CacheController{
runOnDbThread((db)->{
if(clear)
db.delete("home_timeline", null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Status s:posts){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
@@ -123,6 +120,7 @@ public class CacheController{
if(s.hasGapAfter)
flags|=POST_FLAG_GAP_AFTER;
values.put("flags", flags);
values.put("time", s.createdAt.getEpochSecond());
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -137,7 +135,7 @@ public class CacheController{
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
@@ -148,10 +146,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 +160,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.isAkkoma())
.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));
@@ -201,7 +193,7 @@ public class CacheController{
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
db.delete(table, null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Notification n:notifications){
if(n.type==null){
continue;
@@ -209,6 +201,7 @@ public class CacheController{
values.put("id", n.id);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
values.put("time", n.createdAt.getEpochSecond());
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -305,21 +298,24 @@ public class CacheController{
CREATE TABLE `home_timeline` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0
`flags` INTEGER NOT NULL DEFAULT 0,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_all` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_mentions` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
createRecentSearchesTable(db);
createPostsNotificationsTable(db);
@@ -327,12 +323,16 @@ public class CacheController{
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
if(oldVersion==1){
if(oldVersion<2){
createRecentSearchesTable(db);
}
if(oldVersion==2){
if(oldVersion<3){
// MEGALODON-SPECIFIC
createPostsNotificationsTable(db);
}
if(oldVersion<4){
addTimeColumns(db);
}
}
private void createRecentSearchesTable(SQLiteDatabase db){
@@ -350,9 +350,21 @@ public class CacheController{
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
}
private void addTimeColumns(SQLiteDatabase db){
db.execSQL("DELETE FROM `home_timeline`");
db.execSQL("DELETE FROM `notifications_all`");
db.execSQL("DELETE FROM `notifications_mentions`");
db.execSQL("DELETE FROM `notifications_posts`");
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_posts` ADD `time` INTEGER NOT NULL DEFAULT 0");
}
}
@FunctionalInterface

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -28,6 +29,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 +53,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 +64,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 +95,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)
@@ -158,6 +162,11 @@ public class MastodonAPIController{
respObj=gson.fromJson(reader, req.respClass);
}
}catch(JsonIOException|JsonSyntaxException x){
if (req.context != null && response.body().contentType().subtype().equals("html")) {
UiUtils.launchWebBrowser(req.context, response.request().url().toString());
req.cancel();
return;
}
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
@@ -20,9 +21,11 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
@@ -44,10 +47,11 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
TypeToken<T> respTypeToken;
Call okhttpCall;
Token token;
boolean canceled;
boolean canceled, isRemote;
Map<String, String> headers;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@Nullable Context context;
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
this.path=path;
@@ -101,6 +105,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> execRemote(String domain) {
return execRemote(domain, null);
}
public MastodonAPIRequest<T> execRemote(String domain, @Nullable AccountSession remoteSession) {
this.isRemote = true;
return Optional.ofNullable(remoteSession)
.or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream()
.filter(acc -> acc.domain.equals(domain))
.findAny())
.map(AccountSession::getID)
.map(this::exec)
.orElse(this.execNoAuth(domain));
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null);
}
@@ -164,9 +183,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> setContext(Context context) {
this.context = context;
return this;
}
@Nullable
public Context getContext() {
return context;
}
@CallSuper
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){
((BaseModel) respObj).isRemote = isRemote;
((BaseModel) respObj).postprocess();
}else if(respObj instanceof List){
if(removeUnsupportedItems){
@@ -175,6 +205,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Object item=itr.next();
if(item instanceof BaseModel){
try{
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}catch(ObjectValidationException x){
Log.w(TAG, "Removing invalid object from list", x);
@@ -182,15 +213,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
}
}
// no idea why we're post-processing twice, but well, as long
// as upstream does it like this, i don't wanna break anything
for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel){
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}
}
}else{
for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel)
if(item instanceof BaseModel) {
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}
}
}
}

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

@@ -46,7 +46,7 @@ public class StatusInteractionController{
@Override
public void onSuccess(Status result){
runningFavoriteRequests.remove(status.id);
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@@ -80,7 +80,7 @@ public class StatusInteractionController{
public void onSuccess(Status reblog){
Status result = reblog.getContentStatus();
runningReblogRequests.remove(status.id);
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
public class GetAccountByHandle extends MastodonAPIRequest<Account>{
/**
* note that this method usually only returns a result if the instance already knows about an
* account - so it makes sense for looking up local users, search might be preferred otherwise
*/
public GetAccountByHandle(String acct){
super(HttpMethod.GET, "/accounts/lookup", Account.class);
addQueryParameter("acct", acct);
}
}

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

@@ -1,5 +1,7 @@
package org.joinmastodon.android.api.session;
import android.net.Uri;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
@@ -7,6 +9,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;
@@ -14,6 +17,7 @@ import org.joinmastodon.android.model.Token;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class AccountSession{
public Token token;
@@ -87,4 +91,15 @@ public class AccountSession{
pushSubscriptionManager=new PushSubscriptionManager(getID());
return pushSubscriptionManager;
}
public Optional<Instance> getInstance() {
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
}
public Uri getInstanceUri() {
return new Uri.Builder()
.scheme("https")
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
.build();
}
}

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);
@@ -118,14 +125,16 @@ public class AccountSessionManager{
}
public synchronized void writeAccountsFile(){
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~");
File file = new File(MastodonApp.context.getFilesDir(), "accounts.json");
try{
try(FileOutputStream out=new FileOutputStream(file)){
try(FileOutputStream out=new FileOutputStream(tmpFile)){
SessionsStorageWrapper w=new SessionsStorageWrapper();
w.accounts=new ArrayList<>(sessions.values());
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(w, writer);
writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}
}catch(IOException x){
Log.e(TAG, "Error writing accounts file", x);
@@ -151,6 +160,11 @@ public class AccountSessionManager{
return sessions.get(id);
}
@Nullable
public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id);
}
@Nullable
public AccountSession getLastActiveAccount(){
if(sessions.isEmpty() || lastActiveAccountID==null)
@@ -178,6 +192,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())
@@ -248,31 +263,35 @@ public class AccountSessionManager{
}
public void maybeUpdateLocalInfo(){
maybeUpdateLocalInfo(null);
}
public void maybeUpdateLocalInfo(AccountSession activeSession){
long now=System.currentTimeMillis();
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
// if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
// }
// if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session);
// }
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
}
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
updateSessionWordFilters(session);
}
updateSessionMarkers(session);
}
if(loadedInstances){
maybeUpdateCustomEmojis(domains);
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
}
}
private void maybeUpdateCustomEmojis(Set<String> domains){
private void maybeUpdateCustomEmojis(Set<String> domains, String activeDomain){
long now=System.currentTimeMillis();
for(String domain:domains){
// Long lastUpdated=instancesLastUpdated.get(domain);
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain);
// }
Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
updateInstanceInfo(domain);
}
}
}
@@ -400,7 +419,9 @@ public class AccountSessionManager{
@Override
public void onError(ErrorResponse error){
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
}
})
.execNoAuth(domain);
@@ -411,10 +432,13 @@ public class AccountSessionManager{
}
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
File file = getInstanceInfoFile(domain);
File tmpFile = new File(file.getPath() + "~");
try(FileOutputStream out=new FileOutputStream(tmpFile)){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer);
writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}catch(IOException x){
Log.w(TAG, "Error writing instance info file for "+domain, x);
}
@@ -434,7 +458,7 @@ public class AccountSessionManager{
}
if(!loadedInstances){
loadedInstances=true;
maybeUpdateCustomEmojis(domains);
maybeUpdateCustomEmojis(domains, null);
}
}
@@ -458,10 +482,6 @@ public class AccountSessionManager{
return instances.get(domain);
}
public Instance getInstanceInfoForAccount(String account) {
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
}
public void updateAccountInfo(String id, Account account){
AccountSession session=getAccount(id);
session.self=account;

View File

@@ -1,12 +1,9 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
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;
@@ -23,6 +20,7 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
@@ -64,7 +62,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 +87,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){
@@ -92,10 +96,11 @@ public class AccountTimelineFragment extends StatusListFragment{
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
return;
}else if(filter==GetAccountStatuses.Filter.MEDIA){
if(ev.status.mediaAttachments.isEmpty())
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
return;
}
prependItems(Collections.singletonList(ev.status), true);
if (isOnTop()) scrollToTop();
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
@@ -122,4 +127,19 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
// could return different uris based on filter (e.g. media -> "/media"), but i want to
// return the remote url to the user, and i don't know whether i'd need to append
// '#media' (akkoma/pleroma) or '/media' (glitch/mastodon) since i don't know anything
// about the remote instance. so, just returning the base url to the user instead
return Uri.parse(user.url);
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import static java.util.stream.Collectors.toList;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -93,14 +94,22 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
@Override
public void onSuccess(List<Announcement> result){
if (getActivity() == null) return;
List<Announcement> unread = result.stream().filter(a -> !a.read).collect(toList());
List<Announcement> read = result.stream().filter(a -> a.read).collect(toList());
onDataLoaded(unread, true);
onDataLoaded(read, false);
if (unread.isEmpty()) setResult(true, null);
else unreadIDs = unread.stream().map(a -> a.id).collect(toList());
// get unread items first
List<Announcement> data = result.stream().filter(a -> !a.read).collect(toList());
if (data.isEmpty()) setResult(true, null);
else unreadIDs = data.stream().map(a -> a.id).collect(toList());
// append read items at the end
data.addAll(result.stream().filter(a -> a.read).collect(toList()));
onDataLoaded(data, false);
}
})
.exec(accountID);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/announcements").build() : null;
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -33,6 +34,7 @@ import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
@@ -45,7 +47,7 @@ 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.ProvidesAssistContent;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
@@ -69,7 +71,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, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@@ -80,13 +82,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
protected boolean currentlyScrolling;
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;
}
@@ -101,8 +104,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
setRetainInstance(true);
}
@Override
protected RecyclerView.Adapter getAdapter(){
return adapter=new DisplayItemsAdapter();
@@ -131,7 +132,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.clear();
}
protected void prependItems(List<T> items, boolean notify){
protected int prependItems(List<T> items, boolean notify){
data.addAll(0, items);
int offset=0;
for(T s:items){
@@ -144,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
if(notify)
adapter.notifyItemRangeInserted(0, offset);
return offset;
}
protected String getMaxID(){
@@ -204,7 +206,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){
if(holder!=null && list!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
int[] pos={0, 0};
@@ -270,37 +272,63 @@ 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);
}
public boolean isScrolling() {
return currentlyScrolling;
}
@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);
if (fab!=null && GlobalUserPreferences.autoHideFab) {
View fab = getFab();
if (fab!=null && GlobalUserPreferences.autoHideFab && dy != UiUtils.SCROLL_TO_TOP_DELTA) {
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);
@@ -308,12 +336,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
currentlyScrolling = newState != RecyclerView.SCROLL_STATE_IDLE;
}
});
list.addItemDecoration(new StatusListItemDecoration());
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
@@ -325,28 +361,54 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
if(holder instanceof StatusDisplayItem.Holder<?> h){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(otherID.equals(id)){
if (firstIndex < 0) firstIndex = i;
lastIndex = i;
StatusDisplayItem item = h.getItem();
hasDescendant = item.hasDescendantNeighbor;
// no for direct descendants because main status (right above) is
// being displayed with an extended footer - no connected layout
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left);
outRect.top=Math.min(outRect.top, tmpRect.top);
outRect.right=Math.max(outRect.right, tmpRect.right);
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
isWarning = true;
}
}
}
}
}
// shifting the selection box down
// see also: FooterStatusDisplayItem#onBind (setMargins)
if (isWarning || firstIndex < 0 || lastIndex < 0 ||
!(list.getChildViewHolder(list.getChildAt(lastIndex))
instanceof FooterStatusDisplayItem.Holder)) return;
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(prevIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(nextIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4);
if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4);
}
});
list.setItemAnimator(new BetterItemAnimator());
((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 +591,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){
@@ -540,6 +610,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
warning.getItem().status.filterRevealed = true;
}
@Override
public String getAccountID(){
return accountID;
}
@@ -609,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
protected int getListWidthForMediaLayout(){
return list.getWidth();
}
@@ -655,13 +731,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);
}
@@ -673,6 +749,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return attachmentViewsPool;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
@Override
protected void onDataLoaded(List<T> d, boolean more) {
super.onDataLoaded(d, more);
// more available, but the page isn't even full yet? seems wrong, let's load some more
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@@ -734,6 +821,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}
@@ -758,7 +846,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

@@ -1,9 +1,11 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
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 +37,14 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
})
.exec(accountID);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/bookmarks").build();
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
@@ -91,6 +92,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;
@@ -148,7 +150,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -179,8 +181,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 +236,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 +245,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;
@@ -259,9 +264,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Nav.finish(this);
return;
}
if(customEmojis.isEmpty()){
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
@@ -330,6 +332,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);
@@ -353,6 +356,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} else {
mediaBtn.setOnClickListener(v -> openFilePicker(false));
}
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -364,6 +368,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 +474,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 +492,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 +542,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));
}
@@ -556,8 +581,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void afterTextChanged(Editable s){
if(s.length()==0)
if(s.length()==0){
updateCharCounter();
return;
}
int start=lastChangeStart;
int count=lastChangeCount;
// offset one char back to catch an already typed '@' or '#' or ':'
@@ -720,9 +747,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(!TextUtils.isEmpty(status.spoilerText)){
hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE);
if(GlobalUserPreferences.prefixRepliesWithRe && !status.spoilerText.startsWith("re: ")){
if ((GlobalUserPreferences.prefixReplies == ALWAYS
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
&& !status.spoilerText.startsWith("re: ")) {
spoilerEdit.setText("re: " + status.spoilerText);
}else{
} else {
spoilerEdit.setText(status.spoilerText);
}
spoilerBtn.setSelected(true);
@@ -822,12 +851,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) {
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
// editing an already published post
draftsBtn.setVisibility(View.GONE);
}
}
@Override
public String getAccountID() {
return accountID;
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -907,6 +942,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)");
@@ -973,7 +1019,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.state!=AttachmentUploadState.DONE)
nonDoneAttachmentCount++;
}
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
publishButton.setEnabled((!isInstancePixelfed() || attachments.size() > 0) && (trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
sendError.setVisibility(View.GONE);
}
@@ -1050,9 +1096,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.isAkkoma() ? 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());
@@ -1114,7 +1161,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
sendProgress.setVisibility(View.VISIBLE);
sendError.setVisibility(View.GONE);
Callback<Status> resCallback=new Callback<>(){
Callback<Status> resCallback = new Callback<>(){
@Override
public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> {
@@ -1127,7 +1174,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}else{
E.post(new StatusUpdatedEvent(result));
// pixelfed doesn't return the edited status :/
Status editedStatus = result == null ? editingStatus : result;
if (result == null) {
editedStatus.text = req.status;
editedStatus.spoilerText = req.spoilerText;
editedStatus.sensitive = req.sensitive;
editedStatus.language = req.language;
// user will have to reload to see html
editedStatus.content = req.status;
}
E.post(new StatusUpdatedEvent(editedStatus));
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
Nav.finish(ComposeFragment.this);
@@ -1547,7 +1604,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 +1762,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;
}
@@ -1848,9 +1919,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
if (isInstancePixelfed()) {
m.findItem(R.id.vis_private).setVisible(false);
}
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
if (isInstanceAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);
@@ -1894,14 +1968,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

@@ -1,9 +1,11 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
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 +37,16 @@ public class FavoritedStatusListFragment extends StatusListFragment{
})
.exec(accountID);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.encodedPath(isInstanceAkkoma()
? '/' + getSession().self.username + "#favorites"
: "/favourites").build();
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.Collections;
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -148,6 +150,16 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
smoothScrollRecyclerViewToTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/friend-requests" : "/follow_requests").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
@@ -243,6 +255,10 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id);
if(relationship == null || !relationship.followedBy){
actionWrap.setVisibility(View.GONE);

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop {
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String nextMaxID;
private String accountId;
private String accountID;
public FollowedHashtagsFragment() {
super(20);
@@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
accountID=args.getString("account");
setTitle(R.string.sk_hashtags_you_follow);
}
@@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountId);
.exec(accountID);
}
@Override
@@ -75,6 +77,16 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
smoothScrollRecyclerViewToTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/followed_tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull
@Override
@@ -109,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override
public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
}
}
}

View File

@@ -0,0 +1,27 @@
package org.joinmastodon.android.fragments;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import java.util.Optional;
public interface HasAccountID {
String getAccountID();
default AccountSession getSession() {
return AccountSessionManager.getInstance().getAccount(getAccountID());
}
default boolean isInstanceAkkoma() {
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default boolean isInstancePixelfed() {
return getInstance().map(Instance::isPixelfed).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}
}

View File

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

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
@@ -8,7 +9,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import org.joinmastodon.android.E;
@@ -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,14 @@ 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;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.graphics.Outline;
import android.os.Build;
import android.os.Bundle;
@@ -16,6 +17,11 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
@@ -30,16 +36,13 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import java.util.Optional;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.api.Callback;
@@ -52,7 +55,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID {
private FragmentRootLinearLayout content;
private HomeTabFragment homeTabFragment;
private NotificationsFragment notificationsFragment;
@@ -66,6 +69,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 +77,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
E.register(this);
accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name);
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
.map(Instance::isAkkoma)
.orElse(false);
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 +113,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 +137,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 +169,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)
@@ -216,6 +226,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
throw new IllegalArgumentException();
}
public void setCurrentTab(@IdRes int tab){
if(tab==currentTab)
return;
tabBar.selectTab(tab);
onTabSelected(tab);
}
private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){
@@ -227,8 +244,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment);
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
}
private void maybeTriggerLoading(Fragment newFragment){
@@ -255,7 +274,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new AccountSwitcherSheet(getActivity()).show();
new AccountSwitcherSheet(getActivity(), this).show();
return true;
}
return false;
@@ -288,10 +307,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
if (instance == null) return;
Optional<Instance> instance = session.getInstance();
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {
@@ -328,4 +347,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
setNotificationBadge(false);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent);
}
}

View File

@@ -10,6 +10,7 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.assist.AssistContent;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
@@ -24,6 +25,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;
@@ -53,6 +55,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection;
import java.util.HashMap;
@@ -70,7 +73,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, ProvidesAssistContent {
private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID;
@@ -98,13 +101,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 +126,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 +137,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 +289,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 +450,26 @@ 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();
}
@Override
public boolean isScrolling() {
return (fragments[pager.getCurrentItem()] instanceof HasFab fabulous)
&& fabulous.isScrolling();
}
private void updateSwitcherIcon(int i) {
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext()));
showFab();
}
@Override
@@ -657,6 +697,15 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return hashtagsItems.values();
}
public ImageButton getFab() {
return fab;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull
@Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -8,8 +9,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 +37,7 @@ public class HomeTimelineFragment extends StatusListFragment {
private String lastSavedMarkerID;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@@ -49,11 +48,13 @@ public class HomeTimelineFragment extends StatusListFragment {
loadData();
}
private boolean typeFilterPredicate(Status s) {
return (GlobalUserPreferences.showReplies || s.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || s.reblog == null);
}
private List<Status> filterPosts(List<Status> items) {
return items.stream().filter(i ->
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || i.reblog == null)
).collect(Collectors.toList());
return items.stream().filter(this::typeFilterPredicate).collect(Collectors.toList());
}
@Override
@@ -150,7 +151,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,11 +230,11 @@ 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;
if(filterPredicate.test(s)){
if(typeFilterPredicate(s) && filterPredicate.test(s)){
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
@@ -282,4 +283,14 @@ public class HomeTimelineFragment extends StatusListFragment {
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/").build();
}
}

View File

@@ -1,13 +1,13 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.Nullable;
@@ -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,15 @@ 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;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/lists/" + listID).build();
}
}

View File

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

View File

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

View File

@@ -30,4 +30,9 @@ public abstract class MastodonToolbarFragment extends ToolbarFragment{
toolbar.setNavigationContentDescription(R.string.back);
}
}
@Override
protected boolean wantsToolbarMenuIconsTinted() {
return false; // else, badged icons don't work :(
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -13,6 +14,12 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
@@ -24,12 +31,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.V;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
private TabLayout tabLayout;
private ViewPager2 pager;
@@ -47,7 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private String accountID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -227,6 +228,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
};
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -10,20 +11,27 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
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.events.StatusCountersUpdatedEvent;
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.Markers;
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.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
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;
@@ -48,8 +56,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 +91,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 +108,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 +158,17 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache()){
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && 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;
AccountSessionManager.getInstance().writeAccountsFile();
if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
}
}
});
@@ -173,30 +195,16 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else if(n.report != null){
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
Bundle args = new Bundle();
if(n.status != null && n.status.inReplyToAccountId != null && knownAccounts.containsKey(n.status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(n.status.inReplyToAccountId)));
UiUtils.showFragmentForNotification(getContext(), n, accountID, args);
}
@Override
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);
}
@@ -222,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
// copied from StatusListFragment.EventListener (just like the method above)
// (which assumes this.data to be a list of statuses...)
@Subscribe
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
for(Notification n:data){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}
}
}
}
for(Notification n:preloadedData){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
}
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
@@ -254,4 +288,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + getSession().self.username + "/interactions"
: "/notifications").build();
}
}

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

@@ -0,0 +1,52 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class PinnedPostsListFragment extends StatusListFragment{
private Account account;
public PinnedPostsListFragment() {
setListLayoutId(R.layout.recycler_fragment_no_refresh);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
setTitle(R.string.posts);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, false);
}
}).exec(accountID);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(account.url);
}
}

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

@@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
@@ -19,13 +20,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 +32,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;
@@ -47,6 +45,7 @@ import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -76,6 +75,7 @@ import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.time.LocalDateTime;
@@ -93,6 +93,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -112,7 +113,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, ProvidesAssistContent.ProvidesWebUri {
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@@ -123,7 +124,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private PinnedPostsListFragment pinnedPostsFragment;
// private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@@ -137,8 +139,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private TextView followsYouView;
private ViewGroup rolesView;
private Account account;
private Account account, remoteAccount;
private String accountID;
private String domain;
private Relationship relationship;
private int statusBarHeight;
private boolean isOwnProfile;
@@ -152,9 +155,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,13 +177,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setRetainInstance(true);
accountID=getArguments().getString("account");
if(getArguments().containsKey("profileAccount")){
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if (getArguments().containsKey("remoteAccount")) {
remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
if(!getArguments().getBoolean("noAutoLoad", false))
loadData();
} else if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
loaded=true;
if(!isOwnProfile)
loadRelationship();
else if (isInstanceAkkoma() && getInstance().isPresent())
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))
@@ -327,7 +336,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;
@@ -344,36 +353,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return sizeWrapper;
}
private void onAccountLoaded(Account result) {
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
@Override
protected void doLoadData(){
if (remoteAccount != null) {
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
if (getContext() == null) return;
if (args == null || !args.containsKey("profileAccount")) {
Toast.makeText(getContext(), getContext().getString(
R.string.sk_error_loading_profile, domain
), Toast.LENGTH_SHORT).show();
Nav.finish(this);
return;
}
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
});
return;
}
currentRequest=new GetAccountByID(profileAccountID)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(Account result){
if (getActivity() == null) return;
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
onAccountLoaded(result);
}
})
.exec(accountID);
@@ -394,8 +422,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
args.putBoolean("__is_tab", true);
pinnedPostsFragment=new PinnedPostsListFragment();
pinnedPostsFragment.setArguments(args);
// aboutFragment=new ProfileAboutFragment();
setFields(fields);
}
@@ -487,9 +521,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
if (account.roles != null && !account.roles.isEmpty()) {
rolesView.setVisibility(View.VISIBLE);
@@ -508,13 +542,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
String acct = ((isSelf || account.isRemote)
? account.getFullyQualifiedName()
: account.acct);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb.append(acct);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
@@ -523,7 +556,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('@'+acct);
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
@@ -539,6 +572,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
UiUtils.loadCustomEmojiInTextView(name);
UiUtils.loadCustomEmojiInTextView(bio);
@@ -593,6 +629,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return false;
}
@Override
protected boolean wantsToolbarMenuIconsTinted() {
return false;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){
@@ -623,8 +664,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
));
}
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
if(isOwnProfile)
if(isOwnProfile) {
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
return;
}
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
@@ -706,7 +749,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
}
Nav.go(getActivity(), ListTimelinesFragment.class, args);
Nav.go(getActivity(), ListsFragment.class, args);
}else if(id==R.id.followed_hashtags){
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -761,6 +804,22 @@ 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();
}
@Override
public boolean isScrolling() {
return getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous
&& fabulous.isScrolling();
}
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 +850,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){
@@ -1181,6 +1211,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if (adapter != null) adapter.notifyDataSetChanged();
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(account.url);
}
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
public MetadataAdapter(){
super(imgLoader);
@@ -1211,7 +1256,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 +1373,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 +1420,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

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
@@ -31,7 +32,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 +57,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 +65,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 +80,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 +97,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
@@ -179,4 +182,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
}
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
// TODO: adapt when frontends finally implement a scheduled posts list
return null;
}
}

View File

@@ -3,7 +3,8 @@ package org.joinmastodon.android.fragments;
import android.view.ViewTreeObserver;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.V;
import org.joinmastodon.android.ui.utils.UiUtils;
public interface ScrollableToTop{
void scrollToTop();
@@ -21,7 +22,7 @@ public interface ScrollableToTop{
@Override
public boolean onPreDraw(){
list.getViewTreeObserver().removeOnPreDrawListener(this);
list.scrollBy(0, V.dp(300));
list.scrollBy(0, UiUtils.SCROLL_TO_TOP_DELTA);
list.smoothScrollToPosition(0);
return true;
}

View File

@@ -7,13 +7,14 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.LruCache;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -36,7 +37,9 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -48,6 +51,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;
@@ -57,13 +61,16 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Optional;
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;
@@ -75,12 +82,13 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SettingsFragment extends MastodonToolbarFragment{
public class SettingsFragment extends MastodonToolbarFragment implements ProvidesAssistContent.ProvidesWebUri {
private UsableRecyclerView list;
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
@@ -89,7 +97,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 +109,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);
Optional<Instance> instance = session.getInstance();
String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
@@ -181,9 +191,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.showInteractionCounts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
items.add(alwaysRevealSpoilersItem = new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
GlobalUserPreferences.save();
if (list.findViewHolderForAdapterPosition(items.indexOf(autoRevealSpoilersItem)) instanceof ButtonViewHolder bvh) bvh.rebind();
}));
items.add(autoRevealSpoilersItem = new ButtonItem(R.string.sk_settings_auto_reveal_equal_spoilers, R.drawable.ic_fluent_eye_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.settings_auto_reveal_spoiler);
popupMenu.setOnMenuItemClickListener(i -> onAutoRevealSpoilerClick(i, b));
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
onAutoRevealSpoilerChanged(b);
}));
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
GlobalUserPreferences.disableSwipe=i.checked;
@@ -203,17 +222,39 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
items.add(new ButtonItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.settings_prefix_reply_mode);
popupMenu.setOnMenuItemClickListener(i -> onPrefixRepliesClick(i, b));
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
b.setText(switch(GlobalUserPreferences.prefixReplies){
case TO_OTHERS -> R.string.sk_settings_prefix_replies_to_others;
case ALWAYS -> R.string.sk_settings_prefix_replies_always;
default -> R.string.sk_settings_prefix_replies_never;
});
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 SwitchItem(R.string.sk_settings_forward_report_default, R.drawable.ic_fluent_arrow_forward_24_regular, GlobalUserPreferences.forwardReportDefault, i->{
GlobalUserPreferences.forwardReportDefault=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
GlobalUserPreferences.allowRemoteLoading=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
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 (isInstanceAkkoma()) {
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 +276,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();
}));
@@ -259,7 +300,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.collapseLongPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_star_off_24_regular, GlobalUserPreferences.spectatorMode, i->{
GlobalUserPreferences.spectatorMode=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
@@ -289,7 +330,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.save();
needAppRestart=true;
}));
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
boolean translationAvailable = instance
.map(i -> i.v2 != null && i.v2.configuration.translation != null && i.v2.configuration.translation.enabled)
.orElse(false);
items.add(new SmallTextItem(getString(translationAvailable ?
R.string.sk_settings_translation_availability_note_available :
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
@@ -314,23 +357,55 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
items.add(new HeaderItem(instanceName));
items.add(new TextItem(R.string.sk_settings_rules, ()->{
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
items.add(new TextItem(R.string.sk_settings_rules, instance.<Runnable>map(i -> () -> {
Bundle args = new Bundle();
args.putParcelable("instance", Parcels.wrap(i));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
}).orElse(null), R.drawable.ic_fluent_task_list_ltr_24_regular));
items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version)));
items.add(new SmallTextItem(instance
.map(i -> getString(R.string.sk_settings_server_version, i.version))
.orElse(getString(R.string.sk_instance_info_unavailable))));
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);
instance.ifPresent(i -> ContentType.adaptMenuToInstance(contentTypeMenu, i));
}));
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) {
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID);
if (!isInstanceAkkoma()) {
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
}
} else {
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
@@ -480,6 +555,52 @@ public class SettingsFragment extends MastodonToolbarFragment{
return true;
}
private boolean onPrefixRepliesClick(MenuItem item, Button btn) {
int id = item.getItemId();
PrefixRepliesMode mode = PrefixRepliesMode.NEVER;
if (id == R.id.prefix_replies_always) mode = PrefixRepliesMode.ALWAYS;
else if (id == R.id.prefix_replies_to_others) mode = PrefixRepliesMode.TO_OTHERS;
GlobalUserPreferences.prefixReplies = mode;
btn.setText(switch(GlobalUserPreferences.prefixReplies){
case TO_OTHERS -> R.string.sk_settings_prefix_replies_to_others;
case ALWAYS -> R.string.sk_settings_prefix_replies_always;
default -> R.string.sk_settings_prefix_replies_never;
});
return true;
}
private boolean onAutoRevealSpoilerClick(MenuItem item, Button btn) {
int id = item.getItemId();
AutoRevealMode mode = AutoRevealMode.NEVER;
if (id == R.id.auto_reveal_threads) mode = AutoRevealMode.THREADS;
else if (id == R.id.auto_reveal_discussions) mode = AutoRevealMode.DISCUSSIONS;
GlobalUserPreferences.alwaysExpandContentWarnings = false;
GlobalUserPreferences.autoRevealEqualSpoilers = mode;
GlobalUserPreferences.save();
onAutoRevealSpoilerChanged(btn);
return true;
}
private void onAutoRevealSpoilerChanged(Button b) {
if (GlobalUserPreferences.alwaysExpandContentWarnings) {
b.setText(R.string.sk_settings_auto_reveal_anyone);
} else {
b.setText(switch(GlobalUserPreferences.autoRevealEqualSpoilers){
case THREADS -> R.string.sk_settings_auto_reveal_author;
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_anyone;
default -> R.string.sk_settings_auto_reveal_nobody;
});
if (alwaysRevealSpoilersItem.checked != GlobalUserPreferences.alwaysExpandContentWarnings) {
alwaysRevealSpoilersItem.checked = GlobalUserPreferences.alwaysExpandContentWarnings;
if (list.findViewHolderForAdapterPosition(items.indexOf(alwaysRevealSpoilersItem)) instanceof SwitchViewHolder svh) svh.rebind();
}
}
}
private void onTrueBlackThemeChanged(SwitchItem item){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
@@ -496,6 +617,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 = null;
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;
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();
@@ -668,6 +817,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/about" : "/settings").build();
}
@Override
public String getAccountID() {
return accountID;
}
private static abstract class Item{
public abstract int getViewType();
}
@@ -751,7 +910,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
private class SmallTextItem extends Item {
private String text;
private final String text;
public SmallTextItem(@StringRes int text) {
this.text = getString(text);
}
public SmallTextItem(String text) {
this.text = text;
@@ -992,7 +1155,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
private final ImageView icon;
private final TextView text;
@SuppressLint("ClickableViewAccessibility")
public ButtonViewHolder(){
super(getActivity(), R.layout.item_settings_button, list);
text=findViewById(R.id.text);
@@ -1000,10 +1162,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
button=findViewById(R.id.button);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBind(ButtonItem item){
text.setText(item.text);
icon.setImageResource(item.icon);
icon.setVisibility(item.icon == 0 ? View.GONE : View.VISIBLE);
icon.setImageResource(item.icon == 0 ? 0 : item.icon);
// reset listeners before letting the button consumer consume the button
// (and potentially set some listeners, but not others)
button.setOnTouchListener(null);
button.setOnClickListener(null);
button.setOnLongClickListener(null);
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

@@ -1,11 +1,13 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
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;
@@ -23,13 +25,13 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEditHistoryFragment extends StatusListFragment{
private String id;
private String id, url;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id=getArguments().getString("id");
url=getArguments().getString("url");
loadData();
}
@@ -55,7 +57,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 +158,14 @@ public class StatusEditHistoryFragment extends StatusListFragment{
public boolean isItemEnabled(String id){
return false;
}
@Override
protected Filter.FilterContext getFilterContext() {
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(url);
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.os.Bundle;
@@ -28,15 +29,17 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
public abstract class StatusListFragment extends BaseStatusListFragment<Status> {
protected EventListener eventListener=new EventListener();
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))
@@ -65,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
status.filterRevealed = true;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
@@ -161,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void removeStatus(Status status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){
StatusDisplayItem item = displayItems.get(i);
if(status.id.equals(item.parentID)){
index=i;
break;
}
if (item.parentID.equals(status.inReplyToId)) {
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
ancestorLastIndex = i;
}
}
// did we find an ancestor that is also the status' neighbor?
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) {
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) {
StatusDisplayItem item = displayItems.get(i);
// update ancestor to have no descendant anymore
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
}
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
}
if(index==-1)
return;
int lastIndex;
@@ -180,7 +199,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
}
@Override
public void onConfigurationChanged(Configuration newConfig){
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
}

View File

@@ -1,36 +1,59 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment{
protected Status mainStatus;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus, updatedStatus;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
private StatusContext result;
protected boolean contextInitiallyRendered, transitionFinished;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -47,78 +70,235 @@ public class ThreadFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
// modifying hidden filtered items if status is displayed as a warning
List<StatusDisplayItem> itemsToModify =
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
? warning.filteredItems
: items;
for(int i = 0; i < itemsToModify.size(); i++){
StatusDisplayItem item = itemsToModify.get(i);
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
if (ancestryInfo != null) {
item.setAncestryInfo(
ancestryInfo.descendantNeighbor != null,
ancestryInfo.ancestoringNeighbor != null,
s.id.equals(mainStatus.id),
Optional.ofNullable(ancestryInfo.ancestoringNeighbor)
.map(ancestor -> ancestor.id.equals(mainStatus.id))
.orElse(false)
);
}
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
deleteTheseItems.add(i);
}
if(s.id.equals(mainStatus.id)){
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
}
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
}
return items;
}
@Override
public void onTransitionFinished() {
transitionFinished = true;
maybeApplyContext();
}
@Override
protected void doLoadData(int offset, int count){
if (refreshing) loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if (getActivity() == null) return;
if(refreshing){
data.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
}
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.pleroma != null){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=result.ancestors.size()-1; i >= 0; i--){
Status s=result.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
result.descendants=getDescendantsOrdered(mainStatus.id,
result.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
int prevCount=displayItems.size();
onAppendItems(result.descendants);
int count=displayItems.size();
if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
prependItems(result.ancestors, !refreshing);
dataLoaded();
if(refreshing){
refreshDone();
adapter.notifyDataSetChanged();
}
list.scrollToPosition(displayItems.size()-count);
ThreadFragment.this.result = result;
maybeApplyContext();
}
})
.exec(accountID);
}
private List<Status> getDescendantsOrdered(String id, List<Status> statuses){
private void loadMainStatus() {
new GetStatusByID(mainStatus.id)
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status status) {
if (getContext() == null || status == null) return;
updatedStatus = status;
// for the case that the context has already loaded (and the animation has
// already finished), falling back to applying it ourselves:
maybeApplyMainStatus();
}
@Override
public void onError(ErrorResponse error) {}
}).exec(accountID);
}
protected void maybeApplyContext() {
if (!transitionFinished || result == null || getContext() == null) return;
Map<String, Status> oldData = null;
if(refreshing){
oldData = new HashMap<>(data.size());
for (Status s : data) oldData.put(s.id, s);
data.clear();
ancestryMap.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
}
// TODO: figure out how this code works
if (isInstanceAkkoma()) sortStatusContext(mainStatus, result);
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
}
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
int prevCount=displayItems.size();
onAppendItems(result.descendants);
int count=displayItems.size();
if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
int prependedCount = prependItems(result.ancestors, !refreshing);
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
displayItems.remove(prependedCount);
adapter.notifyItemRemoved(prependedCount);
count--;
}
for (Status s : data) {
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.filterRevealed = oldStatus.filterRevealed;
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText) &&
mainStatus.spoilerRevealed) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = true;
}
}
}
dataLoaded();
if(refreshing){
refreshDone();
adapter.notifyDataSetChanged();
}
list.scrollToPosition(displayItems.size()-count);
// no animation is going to happen, so proceeding to apply right now
if (data.size() == 1) {
contextInitiallyRendered = true;
// for the case that the main status has already finished loading
maybeApplyMainStatus();
}
result = null;
}
protected Object maybeApplyMainStatus() {
if (updatedStatus == null || !contextInitiallyRendered) return null;
// restore revealed states for main status because it gets updated after doLoadData
updatedStatus.filterRevealed = mainStatus.filterRevealed;
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
// returning fired event object to facilitate testing
Object event;
if (updatedStatus.editedAt != null &&
(mainStatus.editedAt == null ||
updatedStatus.editedAt.isAfter(mainStatus.editedAt))) {
event = new StatusUpdatedEvent(updatedStatus);
} else {
event = new StatusCountersUpdatedEvent(updatedStatus);
}
mainStatus = updatedStatus;
updatedStatus = null;
E.post(event);
return event;
}
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
List<Status> statuses = new ArrayList<>(context.ancestors);
statuses.add(mainStatus);
statuses.addAll(context.descendants);
int count = statuses.size();
for (int index = 0; index < count; index++) {
Status current = statuses.get(index);
ancestry.add(new NeighborAncestryInfo(
current,
// descendant neighbor
Optional
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
.filter(s -> s.inReplyToId.equals(current.id))
.orElse(null),
// ancestoring neighbor
Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
.filter(ancestor -> Optional.ofNullable(ancestor.descendantNeighbor)
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
.orElse(false))
.map(a -> a.status)
.orElse(null)
));
}
return ancestry;
}
public static void sortStatusContext(Status mainStatus, StatusContext context) {
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:context.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=context.ancestors.size()-1; i >= 0; i--){
Status s=context.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
context.ancestors=context.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
context.descendants=getDescendantsOrdered(mainStatus.id,
context.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
private static List<Status> getDescendantsOrdered(String id, List<Status> statuses){
List<Status> out=new ArrayList<>();
for(Status s:getDirectDescendants(id, statuses)){
out.add(s);
@@ -130,14 +310,14 @@ public class ThreadFragment extends StatusListFragment{
return out;
}
private List<Status> getDirectDescendants(String id, List<Status> statuses){
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
return statuses.stream()
.filter(s -> s.inReplyToId.equals(id))
.collect(Collectors.toList());
}
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());
@@ -159,17 +339,78 @@ public class ThreadFragment extends StatusListFragment{
showContent();
if(!loaded)
footerProgress.setVisibility(View.VISIBLE);
list.setItemAnimator(new BetterItemAnimator() {
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder);
contextInitiallyRendered = true;
// for the case that both requests are already done (and thus won't apply it)
maybeApplyMainStatus();
}
});
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
onAppendItems(Collections.singletonList(ev.status));
if (ev.status.inReplyToId == null) return;
Status repliedToStatus = getStatusByID(ev.status.inReplyToId);
if (repliedToStatus == null) return;
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
int nextDisplayItemsIndex = -1, indexOfPreviousDisplayItem = -1;
for (int i = 0; i < displayItems.size(); i++) {
StatusDisplayItem item = displayItems.get(i);
if (repliedToStatus.id.equals(item.parentID)) {
// saving the replied-to status' display items index to eventually reach the last one
indexOfPreviousDisplayItem = i;
item.hasDescendantNeighbor = true;
} else if (indexOfPreviousDisplayItem >= 0 && nextDisplayItemsIndex == -1) {
// previous display item was the replied-to status' display items
nextDisplayItemsIndex = i;
// nothing left to do if there's no other reply to that status
if (ancestry.descendantNeighbor == null) break;
}
if (ancestry.descendantNeighbor != null && item.parentID.equals(ancestry.descendantNeighbor.id)) {
// existing reply shall no longer have the replied-to status as its neighbor
item.hasAncestoringNeighbor = false;
}
}
// fall back to inserting the item at the end
nextDisplayItemsIndex = nextDisplayItemsIndex >= 0 ? nextDisplayItemsIndex : displayItems.size();
int nextDataIndex = data.indexOf(repliedToStatus) + 1;
// if replied-to status already has another reply...
if (ancestry.descendantNeighbor != null) {
// update the reply's ancestry to remove its ancestoring neighbor (as we did above)
ancestryMap.get(ancestry.descendantNeighbor.id).ancestoringNeighbor = null;
// make sure the existing reply has a reply line
if (nextDataIndex < data.size() &&
!(displayItems.get(nextDisplayItemsIndex) instanceof ReblogOrReplyLineStatusDisplayItem)) {
Status nextStatus = data.get(nextDataIndex);
if (!nextStatus.account.id.equals(repliedToStatus.account.id)) {
// create reply line manually since we're not building that status' items
displayItems.add(nextDisplayItemsIndex, StatusDisplayItem.buildReplyLine(
this, nextStatus, accountID, nextStatus, repliedToStatus.account, false
));
}
}
}
// update replied-to status' ancestry
ancestry.descendantNeighbor = ev.status;
// add ancestry for newly created status before building its display items
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus));
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status));
data.add(nextDataIndex, ev.status);
adapter.notifyDataSetChanged();
}
@Override
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id);
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
}
@Override
@@ -181,4 +422,52 @@ public class ThreadFragment extends StatusListFragment{
public boolean wantsLightNavigationBar(){
return !UiUtils.isDarkTheme();
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.THREAD;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(mainStatus.url);
}
protected static class NeighborAncestryInfo {
protected Status status, descendantNeighbor, ancestoringNeighbor;
protected NeighborAncestryInfo(@NonNull Status status, Status descendantNeighbor, Status ancestoringNeighbor) {
this.status = status;
this.descendantNeighbor = descendantNeighbor;
this.ancestoringNeighbor = ancestoringNeighbor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NeighborAncestryInfo that = (NeighborAncestryInfo) o;
return status.equals(that.status)
&& Objects.equals(descendantNeighbor, that.descendantNeighbor)
&& Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor);
}
@Override
public int hashCode() {
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
}
}
@Override
protected void onErrorRetryClick(){
if(preloadingFailed){
preloadingFailed=false;
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
V.setVisibilityAnimated(footerError, View.GONE);
doLoadData();
return;
}
super.onErrorRetryClick();
}
}

View File

@@ -1,17 +1,68 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
import java.util.Optional;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
protected Account account;
protected String initialSubtitle = "";
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
if (getArguments().containsKey("remoteAccount")) {
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
}
setTitle("@"+account.acct);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + account.id
: '@' + account.acct).build();
}
@Override
public String getRemoteDomain() {
return account.getDomainFromURL();
}
@Override
public Account getCurrentInfo() {
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account;
}
@Override
protected MastodonAPIRequest<Account> loadRemoteInfo() {
return new GetAccountByHandle(account.acct);
}
@Override
protected AccountSession getRemoteSession() {
return Optional.ofNullable(remoteInfo)
.map(AccountSessionManager.getInstance()::tryGetAccount)
.orElse(null);
}
@Override
protected void onRemoteLoadingFailed() {
super.onRemoteLoadingFailed();
String prefix = initialSubtitle == null ? "" :
initialSubtitle + " " + getContext().getString(R.string.sk_separator) + " ";
String str = prefix +
getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain);
setSubtitle(str);
}
}

View File

@@ -1,6 +1,8 @@
package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
@@ -23,7 +25,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
@@ -34,6 +37,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
@@ -44,6 +48,7 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
@@ -57,7 +62,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> {
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -170,6 +175,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super.onApplyWindowInsets(insets);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@@ -230,16 +245,19 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship();
}
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
@@ -269,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@@ -387,7 +406,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
args.putString("account", accountID);
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
Nav.go(getActivity(), ListsFragment.class, args);
}
return true;
}
@@ -410,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@@ -12,11 +13,17 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowers(account.id, maxID, count);
return new GetAccountFollowers(getCurrentInfo().id, maxID, count);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath(isInstanceAkkoma() ? "#followers" : "/followers").build();
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@@ -12,11 +13,17 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowing(account.id, maxID, count);
return new GetAccountFollowing(getCurrentInfo().id, maxID, count);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath(isInstanceAkkoma() ? "#followees" : "/following").build();
}
}

View File

@@ -1,33 +1,173 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
private String nextMaxID;
private MastodonAPIRequest<T> remoteInfoRequest;
protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled;
protected int localOffset;
protected T remoteInfo;
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
protected abstract MastodonAPIRequest<T> loadRemoteInfo();
public abstract T getCurrentInfo();
public abstract String getRemoteDomain();
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// already have remote info (e.g. from arguments), so no need to fetch it again
if (remoteInfo != null) {
onRemoteInfoLoaded(remoteInfo);
return;
}
remoteDisabled = !GlobalUserPreferences.allowRemoteLoading
|| getSession().domain.equals(getRemoteDomain());
if (!remoteDisabled) {
remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() {
@Override
public void onSuccess(T result) {
if (getContext() == null) return;
onRemoteInfoLoaded(result);
}
@Override
public void onError(ErrorResponse error) {
if (getContext() == null) return;
onRemoteLoadingFailed();
}
});
remoteInfoRequest.execRemote(getRemoteDomain(), getRemoteSession());
}
}
/**
* override to provide an ideal account session (e.g. if you're logged into the author's remote
* account) to make the remote request from. if null is provided, will try to get any session
* on the remote domain, or tries the request without authentication.
*/
protected AccountSession getRemoteSession() {
return null;
}
protected void onRemoteInfoLoaded(T info) {
this.remoteInfo = info;
this.remoteInfoRequest = null;
maybeStartLoadingRemote();
}
protected void onRemoteLoadingFailed() {
this.remoteRequestFailed = true;
this.remoteInfo = null;
this.remoteInfoRequest = null;
if (doneWithHomeInstance) dataLoaded();
}
@Override
public void dataLoaded() {
super.dataLoaded();
footerProgress.setVisibility(View.GONE);
}
private void maybeStartLoadingRemote() {
if (startedRemoteLoading || remoteDisabled) return;
if (!remoteRequestFailed) {
if (data.size() == 0) showProgress();
else footerProgress.setVisibility(View.VISIBLE);
}
if (doneWithHomeInstance && remoteInfo != null) {
startedRemoteLoading = true;
loadData(localOffset, itemsPerPage * 2);
}
}
@Override
public void onRefresh() {
localOffset = 0;
doneWithHomeInstance = false;
startedRemoteLoading = false;
super.onRefresh();
}
@Override
public void loadData(int offset, int count) {
// always subtract the amount loaded through the home instance once loading from remote
// since loadData gets called with data.size() (data includes both local and remote)
if (doneWithHomeInstance) offset -= localOffset;
super.loadData(offset, count);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count)
MastodonAPIRequest<?> request = onCreateRequest(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
Collection<AccountItem> d = justRefreshed ? List.of() : data;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
if (getActivity() == null) return;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
List<AccountItem> items = result.stream()
.filter(a -> d.size() > 1000 || d.stream()
.noneMatch(i -> i.account.url.equals(a.url)))
.map(AccountItem::new)
.collect(Collectors.toList());
boolean hasMore = nextMaxID != null;
if (!hasMore && !doneWithHomeInstance) {
// only runs last time data was fetched from the home instance
localOffset = d.size() + items.size();
doneWithHomeInstance = true;
}
onDataLoaded(items, hasMore);
if (doneWithHomeInstance) maybeStartLoadingRemote();
}
})
.exec(accountID);
@Override
public void onError(ErrorResponse error) {
if (doneWithHomeInstance) {
onRemoteLoadingFailed();
onDataLoaded(Collections.emptyList(), false);
return;
}
super.onError(error);
}
});
if (doneWithHomeInstance && remoteInfo == null) return; // we are waiting
if (doneWithHomeInstance && remoteInfo != null) {
request.execRemote(getRemoteDomain(), getRemoteSession());
} else {
request.exec(accountID);
}
currentRequest = request;
}
@Override

View File

@@ -1,21 +1,36 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
updateTitle(status);
}
@Override
protected void updateTitle(Status status) {
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusFavorites(status.id, maxID, count);
return new GetStatusFavorites(getCurrentInfo().id, maxID, count);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri statusUri = super.getWebUri(base);
return isInstanceAkkoma()
? statusUri
: statusUri.buildUpon().appendPath("favourites").build();
}
}

View File

@@ -1,21 +1,36 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
updateTitle(status);
}
@Override
protected void updateTitle(Status status) {
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusReblogs(status.id, maxID, count);
return new GetStatusReblogs(getCurrentInfo().id, maxID, count);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri statusUri = super.getWebUri(base);
return isInstanceAkkoma()
? statusUri
: statusUri.buildUpon().appendPath("reblogs").build();
}
}

View File

@@ -1,13 +1,29 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{
import java.util.Optional;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
protected Status status;
protected abstract void updateTitle(Status status);
protected MastodonAPIRequest<Status> loadRemoteInfo() {
String[] parts = status.url.split("/");
if (parts.length == 0) return null;
return new GetStatusByID(parts[parts.length - 1]);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -16,6 +32,46 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
@Override
protected boolean hasSubtitle(){
return false;
return remoteRequestFailed;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base
.encodedPath(isInstanceAkkoma()
? "/notice/" + status.id
: '@' + status.account.acct + '/' + status.id)
.build();
}
@Override
public String getRemoteDomain() {
return Uri.parse(status.url).getHost();
}
@Override
public Status getCurrentInfo() {
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : status;
}
@Override
protected AccountSession getRemoteSession() {
return Optional.ofNullable(remoteInfo)
.map(s -> s.account)
.map(AccountSessionManager.getInstance()::tryGetAccount)
.orElse(null);
}
@Override
protected void onRemoteInfoLoaded(Status info) {
super.onRemoteInfoLoaded(info);
updateTitle(remoteInfo);
}
@Override
protected void onRemoteLoadingFailed() {
super.onRemoteLoadingFailed();
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain));
updateToolbar();
}
}

View File

@@ -0,0 +1,61 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.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;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/main/bubble").build() : null;
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -27,6 +28,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.Collections;
@@ -49,7 +51,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop {
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -145,6 +147,16 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/suggestions").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
@@ -231,6 +243,10 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id);
if(relationship==null){
actionWrap.setVisibility(View.GONE);

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
@@ -19,12 +20,14 @@ 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;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -36,7 +39,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop {
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent {
private TabLayout tabLayout;
private ViewPager2 pager;
@@ -49,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private ProgressBar searchProgress;
private DiscoverPostsFragment postsFragment;
private TrendingHashtagsFragment hashtagsFragment;
private DiscoverHashtagsFragment hashtagsFragment;
private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment;
@@ -117,7 +120,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment=new DiscoverPostsFragment();
postsFragment.setArguments(args);
hashtagsFragment=new TrendingHashtagsFragment();
hashtagsFragment=new DiscoverHashtagsFragment();
hashtagsFragment.setArguments(args);
newsFragment=new DiscoverNewsFragment();
@@ -238,7 +241,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 +275,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
@@ -318,6 +323,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(searchActive
? searchFragment
: getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@@ -1,5 +1,9 @@
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.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -15,6 +19,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List;
@@ -24,11 +29,11 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop {
public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public TrendingHashtagsFragment(){
public DiscoverHashtagsFragment(){
super(10);
}
@@ -73,6 +78,16 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull
@Override
@@ -105,6 +120,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

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -19,6 +20,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collections;
import java.util.List;
@@ -35,7 +37,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop {
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
@@ -88,6 +90,16 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(){
super(imgLoader);

View File

@@ -1,10 +1,10 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop {
public class DiscoverPostsFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
@Override
@@ -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);
@@ -39,7 +39,12 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/posts").build();
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -21,11 +22,10 @@ public class FederatedTimelineFragment extends StatusListFragment {
private String maxID;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
@@ -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,14 @@ public class FederatedTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/all" : "/public").build();
}
}

View File

@@ -1,9 +1,11 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -20,11 +22,10 @@ public class LocalTimelineFragment extends StatusListFragment {
private String maxID;
@Override
protected boolean withComposeButton() {
protected boolean wantsComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@@ -34,7 +35,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 +47,14 @@ public class LocalTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Activity;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -11,10 +12,10 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
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;
@@ -41,7 +42,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
@@ -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);
};
}
@@ -311,8 +312,11 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
public Uri getWebUri(Uri.Builder base) {
Uri.Builder searchUri = base.path("/search");
return isInstanceAkkoma()
? searchUri.appendQueryParameter("query", currentQuery).build()
: searchUri.build();
}
@FunctionalInterface

View File

@@ -89,13 +89,6 @@ public class AccountActivationFragment extends ToolbarFragment{
return !UiUtils.isDarkTheme();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
@@ -110,7 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity()).show();
new AccountSwitcherSheet(getActivity(), null).show();
}
@Override

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

@@ -2,7 +2,9 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.assist.AssistContent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
@@ -24,6 +26,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.NonNull;
@@ -38,7 +41,7 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{
public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAssistContent {
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@@ -130,6 +133,15 @@ public class InstanceRulesFragment extends ToolbarFragment{
}
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(new Uri.Builder()
.scheme("https")
.authority(instance.normalizedUri)
.path("/about")
.build());
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull

View File

@@ -5,6 +5,7 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseIntArray;
@@ -21,6 +22,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 +263,16 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected boolean wantsOverlaySystemNavigation(){
return false;
}
@Override
protected Filter.FilterContext getFilterContext() {
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
if (reportStatus != null) return Uri.parse(reportStatus.url);
if (reportAccount != null) return Uri.parse(reportAccount.url);
return null;
}
}

View File

@@ -15,6 +15,7 @@ import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.reports.SendReport;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
@@ -39,7 +40,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
private TextView forwardReportText;
private Switch forwardReportSwitch;
private EditText commentEdit;
private boolean forwardReport;
private boolean forwardReport = GlobalUserPreferences.forwardReportDefault;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -89,7 +90,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
} else {
forwardReportItem.setOnClickListener(this::onForwardReportClick);
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
forwardReportSwitch.setChecked(forwardReport = true);
forwardReportSwitch.setChecked(forwardReport);
}
return view;
}

View File

@@ -1,7 +1,10 @@
package org.joinmastodon.android.model;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@@ -62,7 +65,6 @@ public class Account extends BaseModel implements Searchable{
/**
* An image banner that is shown above the profile and in profile cards.
*/
@RequiredField
public String header;
/**
* A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.
@@ -135,6 +137,8 @@ public class Account extends BaseModel implements Searchable{
public List<Role> roles;
public @Nullable String fqn; // akkoma has this, mastodon't
@Override
public String getQuery() {
return url;
@@ -162,6 +166,7 @@ public class Account extends BaseModel implements Searchable{
moved.postprocess();
if(TextUtils.isEmpty(displayName))
displayName=username;
if(fqn == null) fqn = getFullyQualifiedName();
}
public boolean isLocal(){
@@ -173,6 +178,10 @@ public class Account extends BaseModel implements Searchable{
return parts.length==1 ? null : parts[1];
}
public String getDomainFromURL() {
return Uri.parse(url).getHost();
}
public String getDisplayUsername(){
return '@'+acct;
}
@@ -181,6 +190,10 @@ public class Account extends BaseModel implements Searchable{
return '@'+acct.split("@")[0];
}
public String getFullyQualifiedName() {
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
}
@Override
public String toString(){
return "Account{"+

View File

@@ -8,8 +8,17 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
public abstract class BaseModel implements Cloneable{
/**
* indicates the profile has been fetched from a foreign instance.
*
* @see MastodonAPIRequest#execRemote
*/
public transient boolean isRemote;
public abstract class BaseModel{
@CallSuper
public void postprocess() throws ObjectValidationException{
try{
@@ -23,4 +32,14 @@ public abstract class BaseModel{
}
}catch(IllegalAccessException ignore){}
}
@NonNull
@Override
public Object clone(){
try{
return super.clone();
}catch(CloneNotSupportedException x){
throw new RuntimeException(x);
}
}
}

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);

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