Compare commits

..

1210 Commits

Author SHA1 Message Date
Jacocococo
c26df5762f Still set desired height 2024-08-04 14:44:08 -03:00
Jacocococo
2021c335ac None-square emoji for reactions 2024-08-04 14:44:08 -03:00
Jacocococo
d121f14d30 Non-square emoji in text views 2024-08-04 14:44:08 -03:00
LucasGGamerM
d1a2a70cdc Merge pull request #495 from FineFindus/feat/trending-links-timeline-improvements
feat(Timeline/TrendingLinks): display URL, update icon
2024-08-04 12:16:42 -03:00
FineFindus
89ef482e2e feat(Timeline/TrendingLink): use open icon for open action
The previous icon made it hard to recognize what the action was supposed
to do. Additionally, the new one also indicate that it will take the
user to an external website.
2024-08-04 15:58:35 +02:00
FineFindus
9918649d7c feat(Timeline/TrendingLink): provide WebURL
Since the Web version now has a user-visible timeline, we can provide a
URL to that.
2024-08-04 15:56:32 +02:00
LucasGGamerM
09185faf9a Merge remote-tracking branch 'refs/remotes/FineFindus/feat/quote-filter-hide'
# Conflicts:
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
2024-08-04 09:33:55 -03:00
LucasGGamerM
bed201a2f7 docs: add 107 changelog 2024-08-03 10:01:56 -03:00
LucasGGamerM
5e7a4c0136 build: bump version number 2024-08-03 09:55:17 -03:00
gallegonovato
bcb8717d5f Translated using Weblate (Spanish)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/es/
2024-08-03 12:50:50 +00:00
gallegonovato
ed1c1bd097 Translated using Weblate (Spanish)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-08-03 12:50:50 +00:00
joenepraat
f480532fd6 Translated using Weblate (Dutch)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/nl/
2024-08-03 12:50:50 +00:00
Vaclovas Intas
cc056cef08 Translated using Weblate (Lithuanian)
Currently translated at 23.0% (9 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/lt/
2024-08-03 12:50:50 +00:00
Hayny
9e7445b8d8 Translated using Weblate (French)
Currently translated at 5.1% (2 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/fr/
2024-08-03 12:50:50 +00:00
Lefteris T
e2d96d3bc7 Translated using Weblate (Greek)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/el/
2024-08-03 12:50:50 +00:00
Lefteris T
4f5c99be21 Translated using Weblate (Greek)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/el/
2024-08-03 12:50:50 +00:00
LucasGGamerM
0388f9d9be fix(toggle-expanded): fix crash when headers happen to be empty 2024-08-03 09:40:28 -03:00
LucasGGamerM
c45128ced0 fix(unofficial-quotes): fix crash when results.statuses is null 2024-08-03 09:36:16 -03:00
LucasGGamerM
f404d2f9cd Merge pull request #491 from FineFindus/fix/discover-scroll-regression
fix(Discover): switch post and hashtag fragments everywhere
2024-08-03 08:05:10 -03:00
FineFindus
2dada69eb8 fix(Discover): switch post and hashtag fragments everywhere
Fixes a regression in 5edbe9b826, whcih
did not switch the fragments everywhere. This caused the scroll-to-top
functionality to not work and the posts to not immediatly load.

Closes https://github.com/LucasGGamerM/moshidon/issues/483.
2024-08-03 11:30:58 +02:00
FineFindus
b7e0596014 feat(StatusDisplayItem): do not hide self-quoted posts 2024-08-03 11:22:40 +02:00
FineFindus
dbef984908 feat(StatusDisplayItem): hide statuses with quotes of muted/blocked
accounts

Hides Statuses with non-official quotes of accounts that are
blocked/muted. This is equivalent to how misskey handles muted quotes.

Closes https://github.com/LucasGGamerM/moshidon/issues/488.
2024-08-03 10:59:51 +02:00
FineFindus
55259f103d feat(Quote): hide filtered quotes
Hides quote of that would have been hidden by a filter, essentially
reverting back to the previous behaviour.

(Partially) Closes: https://github.com/LucasGGamerM/moshidon/issues/488
2024-08-03 00:08:45 +02:00
LucasGGamerM
81519fe906 fix(f-droid): remove f-droid version suffix 2024-08-02 16:43:55 -03:00
LucasGGamerM
07ab3c394a Merge pull request #485 from FineFindus/feat/draft-improvements
feat(Draft): display ScheduledStatus highlighted and formatted
2024-08-02 16:15:05 -03:00
LucasGGamerM
620cc94351 fix(pixelfed): make pixelfed login work again 2024-08-02 16:02:04 -03:00
LucasGGamerM
2494918171 fix(self-updater): export receiver for android 13 and plus 2024-08-02 15:30:27 -03:00
Grishka
a0bed5e739 fix: cherrypick a patch for the Sdk 34 from upstream 2024-08-02 15:24:03 -03:00
FineFindus
a42bf86a1e feat: display ScheduledStatus rendered
Fakes the highlighting and formatting of ScheduledStatus by injecting
the correct HTML tags.

Fixes https://github.com/LucasGGamerM/moshidon/issues/478.
2024-08-01 19:32:53 +02:00
LucasGGamerM
9c7ae9653b Merge pull request #487 from FineFindus/fix/uri-crash
fix: check if uri is hierarchical
2024-08-01 14:27:05 -03:00
FineFindus
44473705b9 feat(Settings/About): hide pre-release option in nightly 2024-08-01 14:07:54 +02:00
FineFindus
f1d40f8963 fix(Tacking): check if uri is hierarchical
Checks if the given uri is hierarchical, as otherwise the
`getQueryParameterNames` function will throw an exception.
2024-08-01 14:07:38 +02:00
FineFindus
fbae5d8816 feat(Draft): only hide media preview if status is senstive
Closes https://github.com/LucasGGamerM/moshidon/issues/478.
2024-07-31 22:39:26 +02:00
LucasGGamerM
43afbb7523 Merge pull request #484 from FineFindus/fix/quote
fix: correctly render more unofficial quotes
2024-07-30 20:05:23 -03:00
LucasGGamerM
080815846f Merge pull request #482 from FineFindus/feat/GNOME-icon
feat(Timelines): add GNOME icon
2024-07-30 20:03:43 -03:00
FineFindus
4b6c6cbcfe refactor(Quotes): inlcude URL scheme in quote regex
This should increase the performance, whilst rejecting more incorrect
URLs and allowing more correct ones.
2024-07-30 21:51:36 +02:00
FineFindus
117037e7e8 feat(Quote): only show quotes for status without attachments 2024-07-30 20:47:37 +02:00
FineFindus
05972fc702 fix(Quotes): increase TLD max length 2024-07-30 20:47:08 +02:00
LucasGGamerM
28084b9f9e Merge remote-tracking branch 'refs/remotes/weblate/master' 2024-07-29 19:43:19 -03:00
joenepraat
02010df408 Translated using Weblate (Dutch)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/nl/
2024-07-29 22:34:59 +00:00
joenepraat
38f77c69d1 Translated using Weblate (Dutch)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/nl/
2024-07-29 22:34:58 +00:00
joenepraat
d0a8c26b65 Translated using Weblate (Dutch)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/nl/
2024-07-29 22:34:58 +00:00
FineFindus
401602e5bc feat(Timelines): add GNOME icon 2024-07-29 22:38:56 +02:00
LucasGGamerM
ccd9dbed13 docs: add 106 changelog
Also removed 110 changelog from megalodon that got here somehow
2024-07-29 16:52:06 -03:00
LucasGGamerM
736d5d9f3e build: bump version 2024-07-29 16:38:32 -03:00
LucasGGamerM
32451c0eea build: bump targetSdk to 34 2024-07-29 16:38:13 -03:00
LucasGGamerM
e7ed8d5590 Merge remote-tracking branch 'refs/remotes/weblate/master' 2024-07-29 16:32:12 -03:00
joenepraat
79d04a949b Translated using Weblate (Dutch)
Currently translated at 96.9% (407 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/nl/
2024-07-29 18:03:32 +00:00
joenepraat
5cd99b9763 Translated using Weblate (Dutch)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/nl/
2024-07-29 18:03:32 +00:00
Vaclovas Intas
3f30c2f3be Translated using Weblate (Lithuanian)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/lt/
2024-07-28 23:14:47 +00:00
Vaclovas Intas
db8187bbc9 Translated using Weblate (Lithuanian)
Currently translated at 12.8% (5 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/lt/
2024-07-28 23:14:46 +00:00
Vaclovas Intas
4e1632aa19 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/lt/
2024-07-28 23:14:46 +00:00
LucasGGamerM
a813f961af Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pt_BR/
2024-07-28 23:14:46 +00:00
joenepraat
f6417662b9 Translated using Weblate (Dutch)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/nl/
2024-07-28 23:14:45 +00:00
aei
2d1bc09616 Translated using Weblate (Arabic)
Currently translated at 79.5% (334 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/ar/
2024-07-28 12:40:12 +00:00
Vaclovas Intas
d9e5ea5b80 Translated using Weblate (Lithuanian)
Currently translated at 62.5% (75 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/lt/
2024-07-27 21:03:14 +00:00
Vaclovas Intas
1ab6bc3663 Translated using Weblate (Lithuanian)
Currently translated at 5.1% (2 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/lt/
2024-07-27 21:03:14 +00:00
Vaclovas Intas
effe3a079f Translated using Weblate (Lithuanian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/lt/
2024-07-27 21:03:14 +00:00
Hayny
7d65563096 Translated using Weblate (French)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/fr/
2024-07-27 21:03:13 +00:00
Vaclovas Intas
857c5b9a55 Translated using Weblate (Lithuanian)
Currently translated at 7.5% (9 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/lt/
2024-07-26 21:18:24 +00:00
Vaclovas Intas
e49760c5a0 Translated using Weblate (Lithuanian)
Currently translated at 65.9% (277 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/lt/
2024-07-26 21:18:24 +00:00
alextecplayz
93b97e99a8 Translated using Weblate (Romanian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/ro/
2024-07-26 21:18:23 +00:00
Lefteris T
6d148b1f7a Translated using Weblate (Greek)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/el/
2024-07-26 21:18:23 +00:00
alextecplayz
4d24e4e846 Translated using Weblate (Romanian)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ro/
2024-07-26 21:18:23 +00:00
LucasGGamerM
9f5c420e66 fix(account-cards): add the progressbar for the accept and decline follow request actions 2024-07-26 14:54:21 -03:00
LucasGGamerM
ca07240a70 feat(unoficial-quote-posts): add caching to unofficial quote toots 2024-07-26 14:03:51 -03:00
LucasGGamerM
1b6978bb93 fix(filtered-with-a-warning): make the roundness nicer in quote toots 2024-07-25 19:11:31 -03:00
LucasGGamerM
d4b20fc5f7 Merge pull request #475
feat: apply inset to filter warning
2024-07-25 19:02:47 -03:00
LucasGGamerM
d3d95c7963 fix(compose-shortcut): make it update the shortcut. AKA make it work 2024-07-25 18:23:17 -03:00
Vaclovas Intas
98c5baecad Translated using Weblate (Lithuanian)
Currently translated at 45.2% (190 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/lt/
2024-07-25 17:49:00 +00:00
gallegonovato
766b7b8c45 Translated using Weblate (Spanish)
Currently translated at 99.7% (419 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/es/
2024-07-25 17:49:00 +00:00
gallegonovato
896ded9ff3 Translated using Weblate (Spanish)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-07-25 17:49:00 +00:00
FineFindus
7b31543d7a feat(StatusDisplayItem/WarningFiltered): apply equal inset vertically 2024-07-25 17:55:17 +02:00
pinklimes
ff61c3c02e Translated using Weblate (Italian)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/it/
2024-07-25 12:47:04 +00:00
pinklimes
aa8562dc88 Translated using Weblate (Spanish)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-07-25 12:47:03 +00:00
FineFindus
ec495750fe feat(StatusDisplayItem/WarningFiltered): apply inset 2024-07-25 13:53:39 +02:00
FineFindus
af33c593b5 refactor(filter/AltText): set filter title 2024-07-25 13:21:02 +02:00
rossetnocpes
4586e42459 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/uk/
2024-07-25 08:14:52 +00:00
pinklimes
2a45b7d13d Translated using Weblate (Italian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/it/
2024-07-25 08:14:52 +00:00
pinklimes
60d573de58 Translated using Weblate (Italian)
Currently translated at 41.0% (16 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/it/
2024-07-25 08:14:52 +00:00
pinklimes
2d7499e8cc Translated using Weblate (Spanish)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-07-25 08:14:51 +00:00
wineTGH
9ec82ae090 Translated using Weblate (Russian)
Currently translated at 92.5% (111 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ru/
2024-07-25 08:14:51 +00:00
Languages add-on
da783c3771 Added translation using Weblate (Bengali) 2024-07-25 06:45:05 +00:00
Vaclovas Intas
9869581515 Translated using Weblate (Lithuanian)
Currently translated at 6.6% (8 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/lt/
2024-07-25 06:45:03 +00:00
Vaclovas Intas
f45fb87ea5 Translated using Weblate (Lithuanian)
Currently translated at 37.6% (158 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/lt/
2024-07-25 06:45:02 +00:00
wineTGH
d80ac7557e Translated using Weblate (Russian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/ru/
2024-07-25 06:45:02 +00:00
Matteo Mucchetti
58403fef59 Translated using Weblate (Italian)
Currently translated at 84.2% (354 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/it/
2024-07-25 06:45:02 +00:00
Linerly
87ca8b1ad7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/id/
2024-07-25 06:45:02 +00:00
ghose
04e1f9e148 Translated using Weblate (Galician)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/gl/
2024-07-25 06:45:02 +00:00
rcarrillodev
1e1fe47638 Translated using Weblate (Spanish)
Currently translated at 99.5% (418 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/es/
2024-07-25 06:45:02 +00:00
Lyfja
c567e264de Translated using Weblate (German)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/de/
2024-07-25 06:45:01 +00:00
aei
c142f82fd1 Translated using Weblate (Arabic)
Currently translated at 79.0% (332 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/ar/
2024-07-25 06:45:01 +00:00
aei
c0cf5b40fa Translated using Weblate (Arabic)
Currently translated at 34.1% (41 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ar/
2024-07-25 06:45:01 +00:00
balaraz
b45e87b271 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/uk/
2024-07-25 06:45:01 +00:00
wineTGH
958243e65d Translated using Weblate (Russian)
Currently translated at 80.8% (97 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ru/
2024-07-25 06:45:01 +00:00
Matteo Mucchetti
8cc91b0f02 Translated using Weblate (Italian)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/it/
2024-07-25 06:45:01 +00:00
pinklimes
0ac7d3530e Translated using Weblate (Italian)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/it/
2024-07-25 06:45:01 +00:00
ghose
10d42264c8 Translated using Weblate (Galician)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/gl/
2024-07-25 06:45:00 +00:00
NicoCharrua
72fee62472 Translated using Weblate (Spanish)
Currently translated at 99.1% (119 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-07-25 06:45:00 +00:00
Languages add-on
9b4528b69a Added translation using Weblate (Lithuanian) 2024-07-24 21:36:42 +00:00
lucasmz.dev
4b0cf4311d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/pt_BR/
2024-07-24 21:36:37 +00:00
Matteo Mucchetti
4ceea9100d Translated using Weblate (Italian)
Currently translated at 82.6% (347 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/it/
2024-07-24 21:36:37 +00:00
NicoCharrua
2522cd26d1 Translated using Weblate (Spanish)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/es/
2024-07-24 21:36:37 +00:00
Lefteris T
294bcef5f6 Translated using Weblate (Greek)
Currently translated at 100.0% (420 of 420 strings)

Translation: Moshidon/megalodon_values
Translate-URL: https://translate.codeberg.org/projects/moshidon/megalodon_values/el/
2024-07-24 21:36:36 +00:00
lucasmz.dev
e61618bf2c Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.6% (116 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pt_BR/
2024-07-24 21:36:36 +00:00
Lefteris T
70e5030fe1 Translated using Weblate (Greek)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/el/
2024-07-24 21:36:36 +00:00
SteffoSpieler
7c270aadda Translated using Weblate (German)
Currently translated at 100.0% (120 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/de/
2024-07-24 21:36:36 +00:00
bgta
30eaeb006d Translated using Weblate (Catalan)
Currently translated at 28.3% (34 of 120 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ca/
2024-07-24 21:36:36 +00:00
Vaclovas Intas
5e11b3fb7a Added translation using Weblate (Lithuanian) 2024-07-24 21:36:34 +00:00
Languages add-on
d6089d0c1e Added translation using Weblate (Interlingua) 2024-07-24 20:44:55 +00:00
LucasGGamerM
1bb288e565 Merge remote-tracking branch 'refs/remotes/megalodon_weblate/main'
# Conflicts:
#	metadata/ar/full_description.txt
#	metadata/de-DE/full_description.txt
#	metadata/es/changelogs/110.txt
#	metadata/es/full_description.txt
#	metadata/gl-ES/full_description.txt
#	metadata/pl/changelogs/94.txt
#	metadata/pl/full_description.txt
#	metadata/ro/full_description.txt
#	metadata/uk/changelogs/62.txt
#	metadata/uk/full_description.txt
#	metadata/zh-Hans/changelogs/62.txt
#	metadata/zh-Hans/changelogs/63.txt
#	metadata/zh-Hans/changelogs/65.txt
#	metadata/zh-Hans/full_description.txt
2024-07-24 17:44:09 -03:00
alextecplayz
d42eb934d5 Translated using Weblate (Romanian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ro/
2024-07-24 17:10:57 -03:00
Lefteris T
2fecd6f0a3 Translated using Weblate (Greek)
Currently translated at 53.8% (21 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/el/
2024-07-24 17:10:57 -03:00
Fitik
c3a2b5a6e1 Translated using Weblate (Esperanto)
Currently translated at 87.2% (103 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/eo/
2024-07-24 17:10:57 -03:00
trlef19
ccff874bcf Translated using Weblate (Greek)
Currently translated at 30.7% (12 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/el/
2024-07-24 17:10:57 -03:00
Eryk Michalak
9e7f351174 Translated using Weblate (Polish)
Currently translated at 94.0% (111 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pl/
2024-07-24 17:10:57 -03:00
trlef19
a9e7fab029 Translated using Weblate (Greek)
Currently translated at 25.6% (10 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/el/
2024-07-24 17:10:57 -03:00
trlef19
aad8abd3bf Translated using Weblate (Greek)
Currently translated at 10.2% (4 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/el/
2024-07-24 17:10:57 -03:00
trlef19
d938c8c470 Translated using Weblate (Greek)
Currently translated at 100.0% (118 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/el/
2024-07-24 17:10:57 -03:00
gallegonovato
124ad8cb0e Translated using Weblate (Spanish)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-07-24 17:10:57 -03:00
gallegonovato
a17c3293b5 Translated using Weblate (Spanish)
Currently translated at 100.0% (118 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-07-24 17:10:57 -03:00
SomeTr
5868da3337 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (39 of 39 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/uk/
2024-07-24 17:10:57 -03:00
SomeTr
731ee17d6d Translated using Weblate (Ukrainian)
Currently translated at 89.8% (106 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/uk/
2024-07-24 17:10:57 -03:00
ghose
edddc297dd Translated using Weblate (Galician)
Currently translated at 100.0% (118 of 118 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/gl/
2024-07-24 17:10:57 -03:00
SomeTr
85152102fd Translated using Weblate (Ukrainian)
Currently translated at 88.1% (89 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/uk/
2024-07-24 17:10:57 -03:00
alextecplayz
fba4c1c6d6 Translated using Weblate (Romanian)
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ro/
2024-07-24 17:10:57 -03:00
Eryk Michalak
593e8d0eb7 Translated using Weblate (Polish)
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pl/
2024-07-24 17:10:57 -03:00
poesty
bafb1ba8f8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/zh_Hans/
2024-07-24 17:10:57 -03:00
hazardaj_nombroj
36124db2aa Translated using Weblate (Esperanto)
Currently translated at 99.0% (100 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/eo/
2024-07-24 17:10:57 -03:00
gallegonovato
155a093eb7 Translated using Weblate (Spanish)
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-07-24 17:10:57 -03:00
Dirk
ddee29bf03 Translated using Weblate (German)
Currently translated at 100.0% (38 of 38 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/de/
2024-07-24 17:10:57 -03:00
edxkl
99e2958649 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pt_BR/
2024-07-24 17:10:57 -03:00
Dirk
519afb6259 Translated using Weblate (German)
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/de/
2024-07-24 17:10:54 -03:00
SomeTr
6ab8991c45 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (38 of 38 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/uk/
2024-07-24 17:10:03 -03:00
SomeTr
44200a4d56 Translated using Weblate (Ukrainian)
Currently translated at 88.1% (89 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/uk/
2024-07-24 17:10:03 -03:00
Oliebol
e929478b6a Translated using Weblate (Dutch)
Currently translated at 92.0% (93 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/nl/
2024-07-24 17:10:03 -03:00
ghose
cf98aa4939 Translated using Weblate (Galician)
Currently translated at 100.0% (101 of 101 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/gl/
2024-07-24 17:10:03 -03:00
LucasGGamerM
22585a2ec5 Merge pull request #470 from FineFindus/feat/tracking-urls
feat: remove tracking parameter from URLs
2024-07-24 15:33:17 -03:00
LucasGGamerM
fa6abd44c3 feat(compose-shortcut): allow user to choose account 2024-07-24 15:22:18 -03:00
LucasGGamerM
1d7cbcc4e1 fix(network): use MoshidonAndroid as user agent 2024-07-23 15:37:10 -03:00
LucasGGamerM
5edbe9b826 fix(discover-fragment): put Posts fragment in the first place
Fixes #472
2024-07-22 17:21:12 -03:00
Linerly
b5027ee66f Translated using Weblate (Indonesian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2024-07-22 10:18:24 +00:00
FineFindus
499baeb496 fix(Tracking): add null check 2024-07-19 20:58:45 +02:00
FineFindus
72d486e992 feat(Compose): remove tracking params in URLs 2024-07-19 20:58:45 +02:00
FineFindus
3020c826ed feat(Settings): allow disabling removing tracking params 2024-07-19 20:58:36 +02:00
FineFindus
34f3e33efc feat: remove tracking params from URLs 2024-07-19 18:45:53 +02:00
LucasGGamerM
5b25168eb7 fix(unofficial-quote-toots): add a check for the end index in the early return 2024-07-19 11:13:15 -03:00
LucasGGamerM
c785bbb2d7 Merge pull request #467 from FineFindus/fix/hashtag-mute-name
fix(HashtagTimeline): show hashtag name in mute icon tooltip
2024-07-19 09:47:42 -03:00
LucasGGamerM
45324a5598 fix(unofficial-quote-toots): maybe. Just maybe fix a bug. 2024-07-18 20:09:04 -03:00
LucasGGamerM
55ad624209 fix(unofficial-quote-toots): try and fix the adapter madness
I ended up adapting the poll items logic for this. I hope this is stable enough.
2024-07-17 16:10:44 -03:00
FineFindus
ed0fe1e803 fix(HashtagTimeline): show hashtag name in mute icon tooltip 2024-07-17 18:44:45 +02:00
LucasGGamerM
18079454a9 fix(post-with-other-account): put an icon for it
Leaving an empty icon causes the app to crash, which is not fun :(
2024-07-17 12:23:32 -03:00
LucasGGamerM
87cb80867a fix(custom-emoji-helper): don't do anything if the spans list is empty 2024-07-17 12:09:32 -03:00
LucasGGamerM
1829dc1d9d fix(unofficial-quotes): try to fix the sudden reload to the top (again) 2024-07-17 12:09:07 -03:00
LucasGGamerM
519cb672d2 fix(unofficial-quotes): try to fix the sudden reload to the top 2024-07-17 10:23:55 -03:00
Lefteris T
e0a5e259f7 Translated using Weblate (Greek)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/el/
2024-07-17 09:18:23 +00:00
LucasGGamerM
86512e237e fix(b9efdbbb40): adapt the ComposeFragment's take picture button to that commit 2024-07-15 18:00:09 -03:00
LucasGGamerM
b9efdbbb40 Merge pull request #458 from FineFindus/feat/share-profile-picture
fix: show profile picture in share sheet
2024-07-15 17:16:11 -03:00
LucasGGamerM
d369129ac7 fix(mastodon-language-resolver): fix a null pointer exception
Fixes #464
2024-07-14 08:48:31 -03:00
LucasGGamerM
c01135d822 Merge pull request #459 from FineFindus/fix/lookup-local-account
fix(UiUtils): correctly lookup local account
2024-07-13 11:58:33 -03:00
trlef19
653a66bd87 Translated using Weblate (Greek)
Currently translated at 83.5% (350 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/el/
2024-07-13 13:18:28 +00:00
LucasGGamerM
ffc2990b32 Update FUNDING.yml 2024-07-11 15:58:55 -03:00
FineFindus
8b26fb3184 fix(UiUtils): correctly lookup local account
Fixes a regression in f590fde7a4,
where links to local accounts would be opened in the browser.
2024-07-11 20:43:50 +02:00
FineFindus
3fec39835c refactor(UiUtils): remove unused function 2024-07-11 20:43:15 +02:00
pixelcode
5402e78342 Translated using Weblate (German)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2024-07-10 13:05:02 +00:00
pixelcode
8995cfcc9d Translated using Weblate (German)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2024-07-10 13:05:02 +00:00
LucasGGamerM
8d3b1f40a3 Merge pull request #461 from realpixelcode/realpixelcode-patch-1
Fix minor spelling and grammar mistakes in German
2024-07-10 08:45:16 -03:00
Pixelcode
f775bae93e minor stylistic fix in full_description.txt 2024-07-09 15:35:20 +00:00
Pixelcode
ca84bc36e3 fix additional "Follower" in strings_mo.xml 2024-07-09 15:33:02 +00:00
Pixelcode
2a775aba70 fix spelling, grammar in German strings_mo.xml 2024-07-09 15:50:28 +02:00
Pixelcode
7cd65dcb32 fix German short_description.txt 2024-07-09 15:45:24 +02:00
Pixelcode
4d694b2725 fix minor spelling and grammar mistakes in German full_description.txt 2024-07-09 15:44:27 +02:00
FineFindus
2e39f81c36 fix: show profile picture in share sheet
Shows the profile picture of the shared account in the share sheet. This
was already done upstream and intended here, but was bugged due to an
additional file provider.
2024-07-08 14:36:14 +02:00
LucasGGamerM
803e66f999 fix(heart-icon): use heart icon in all places
Fixes #446
2024-07-07 10:02:27 -03:00
LucasGGamerM
ed22d3b4ed Merge pull request #457 from FineFindus/fix/more-fixes
fix(quotes): more fixes
2024-07-07 09:00:30 -03:00
LucasGGamerM
ec72653dba Merge pull request #455 from FineFindus/fix/quote=whitespace
fix(StatusDisplayItem/Quote): allow whitespace in closing br tag
2024-07-07 08:58:32 -03:00
FineFindus
9b1e79eba8 fix(StatusDisplayItem/Quote): notify adapter separately
Only notifying the adapter once could lead to cases where the quoting
status was merged with or replaced by the quote status. Separately
notifying it seems to trigger the issue less often.
2024-07-07 09:03:38 +02:00
FineFindus
ca4a1d461a fix(TextStatusDisplayItem): expand non-quoted texts
Using the adapter to udpate the TextStatusDisplayItem does not work for
non-quoted posts.
Ref: 1832de3aab
2024-07-07 09:03:37 +02:00
FineFindus
b90607582a docs: keep comment inline with code changes 2024-07-07 09:03:37 +02:00
FineFindus
0c95f6db1b fix(StatusDisplayItem/Quote): allow whitespace in closing br tag 2024-07-07 08:21:17 +02:00
FineFindus
4caa6cf650 revert: using string replacement for whitespace checking
Ref: bc08c149b7.
2024-07-07 08:21:16 +02:00
LucasGGamerM
bc08c149b7 fix(preview-quote-toots): find the RE: in all the cases I experienced
vmst.io added a <br /> tag that the regex didn't catch. I manually check for it, because it drives me nuts. But it's in the plans to put it all on the regex just because I hate this.
2024-07-06 21:14:11 -03:00
LucasGGamerM
4a783957ed fix(thread-fragments): never filter with a warning the main status 2024-07-06 20:59:53 -03:00
LucasGGamerM
113b47d9e2 refactor(preview-quote-toots): make the updateStatusWithQuote generic, and also readd the notification support
The notification support is needed because of the post notifications, which would benefit from this
2024-07-06 20:50:40 -03:00
LucasGGamerM
96ccb14a59 Merge pull request #454 from FineFindus/fix/quote-improvements
feat: quote improvements
2024-07-06 19:40:29 -03:00
FineFindus
bc8b0e192c feat(StatusDisplayItem/Quote): allow quotes to reference themselves 2024-07-06 22:56:55 +02:00
FineFindus
72400703ab fix(StatusDisplayItem/Quote): only update non-empty adapter
Fixes a crash when updating an empty adapter. This was the case when
opening a status from a notification.
2024-07-06 22:55:59 +02:00
FineFindus
91345268e8 fix(StatusDisplayItem/Quote): use correct method
ChildFragments overwrite the buildDisplayItems to provide the correct
parameters, e.g. flags, additional items, etc. Call those instead of the
default one.
2024-07-06 22:51:14 +02:00
LucasGGamerM
bff6ac4a14 Merge pull request #452 from FineFindus/fix/re-quote-br
fix(StatusDisplayItem): allow closing linebreak tag in quote regex
2024-07-06 14:04:23 -03:00
FineFindus
75183f5625 fix(StatusDisplayItem): allow closing linebreak tag in quote regex 2024-07-06 17:47:48 +02:00
LucasGGamerM
7654b869ba Merge pull request #451
fix(StatusDisplayItem): hide 'RE:' with linebreak in quote mention
2024-07-06 11:54:36 -03:00
LucasGGamerM
f176384bcc Merge pull request #445
feat: use AccountSwitcher for open with other account
2024-07-06 11:52:47 -03:00
FineFindus
a4f2a733b5 fix(StatusDisplayItem): hide 'RE:' with linebreak in quote mention 2024-07-06 16:51:16 +02:00
LucasGGamerM
9ea48fa0ab Merge pull request #447 from FineFindus/fix/crash
fix(StatusDisplayItem): check if headerlist is empty
2024-07-06 11:45:10 -03:00
LucasGGamerM
cc2076ec10 Merge pull request #449
feat: improve non-official quote posts
2024-07-06 11:16:41 -03:00
LucasGGamerM
b5a0c293c5 Merge pull request #438
Fix/mastodon social redirect
2024-07-06 11:03:28 -03:00
LucasGGamerM
3265cfe772 Merge pull request #450 from FineFindus/refactor/version-checking
refactor(Instance): improve compatible version checking
2024-07-06 10:53:54 -03:00
LucasGGamerM
857d0ce539 Merge pull request #448 from FineFindus/fix/error-crash
fix(ErrorStatusDisplayItem): disable button in onBind
2024-07-06 09:33:31 -03:00
FineFindus
31a52c2790 refactor(Instance): improve compatible version checking 2024-07-06 14:30:25 +02:00
FineFindus
94ce329f49 fix(ErrorStatusDisplayItem): disable button in onBind
Fixes a NullPointerException, where the `item` was null in the
constructor, ironically causing the ErrorStatusDisplayItem to
crash immediately.
2024-07-06 13:35:59 +02:00
FineFindus
a67c8b36b1 refactor(StatusDisplayItem/quote): use regex to find last URL 2024-07-06 13:35:14 +02:00
FineFindus
ff90e21e86 feat(StatusDisplayItem/Quote): hide non-official quote mentions
Hides the URL, with optional 'RE:' prefix and whitespace, that is used to construct non-official quotes.
2024-07-06 13:30:25 +02:00
FineFindus
5fd2e322f6 fix(StatusDisplayItem): don't display self-referential quotes 2024-07-06 12:53:38 +02:00
FineFindus
cdd9b0553f refactor(StatusDisplayItem): rebuild StatusItems with quote 2024-07-06 12:47:40 +02:00
FineFindus
6157d4942a refactor(StatusDisplayItem): compile patter outside of function 2024-07-06 11:07:43 +02:00
FineFindus
e68e870a7c fix(StatusDisplayItem): check if headerlist is empty 2024-07-06 08:25:11 +02:00
LucasGGamerM
0788b03828 fix(preview-quote-toots): fix the regex, and also use stripped text
I sometimes forget how things should work
2024-07-05 19:28:49 -03:00
LucasGGamerM
b670da04ed docs(readme): liberapay works now 2024-07-05 18:36:09 -03:00
FineFindus
f70abbbb73 feat: use AccountSwitcher for open with other account 2024-07-05 17:19:42 +02:00
LucasGGamerM
a0dd75890c fix(preview-quote-toots): Allow for some quote tooting in the notifications.
It might need some more love though
2024-07-04 21:16:27 -03:00
LucasGGamerM
38df70cd9e Merge remote-tracking branch 'refs/remotes/Jacocococo/quote-display-fixes'
# Conflicts:
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
2024-07-04 20:22:10 -03:00
softinterlingua
e18fa57d73 Translated using Weblate (Interlingua)
Currently translated at 5.0% (1 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ia/
2024-07-04 19:18:22 +00:00
softinterlingua
51f6264534 Translated using Weblate (Interlingua)
Currently translated at 4.7% (20 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ia/
2024-07-04 19:18:22 +00:00
LucasGGamerM
feff45721f Merge pull request #442 from FineFindus/feat/account-switcher
feat: use AccountSwitcherSheet for account picker
2024-07-04 16:15:19 -03:00
LucasGGamerM
20558f0a19 fix(preview-quote-toots): only preview the quote toots if the statusForContent.quote parameter is null 2024-07-04 16:01:06 -03:00
FineFindus
e97a479e65 feat: use AccountSwitcherSheet for account picker 2024-07-04 20:59:00 +02:00
FineFindus
f590fde7a4 feat(LinkCard): skip redirects to accounts 2024-07-04 20:55:45 +02:00
FineFindus
77c5173014 feat(LinkCard): generalize skipping redirect links 2024-07-04 20:55:45 +02:00
FineFindus
dd4bed0027 feat(LinkCard): open redirected URL 2024-07-04 20:55:45 +02:00
FineFindus
229c0b359f fix(LinkCard): skip mastodon.social redirect page
Skips the mastodon.social exclusive link redirect warning page, by
manually replacing the link card link.
2024-07-04 20:55:40 +02:00
LucasGGamerM
0d4158a612 refactor(preview-quote-toots): remove unneeded code 2024-07-04 15:35:41 -03:00
LucasGGamerM
cfde4425b7 fix(preview-quote-toots): make code nicer, and add the "IS_FOR_QUOTE" flag, so it works better 2024-07-04 15:34:44 -03:00
LucasGGamerM
15f84af757 feat(preview-quote-toots): preview quote toots nicely on mastodon
Still missing it on notifications, but it should be there soon
2024-07-04 14:40:05 -03:00
softinterlingua
39895ff79a Added translation using Weblate (Interlingua) 2024-07-03 18:18:30 +00:00
trlef19
3d2b67efc5 Translated using Weblate (Greek)
Currently translated at 37.9% (159 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/el/
2024-07-02 15:18:23 +00:00
LucasGGamerM
ebd637546f Merge pull request #439 from FineFindus/fix/lemmy-post-trailing-lines
fix(HtmlParser): remove trailing line breaks
2024-06-28 09:25:34 -03:00
FineFindus
618946a8c6 fix(HtmlParser): remove trailing line breaks
Some fediverse servers (e.g. lemmy) add a trailing line break to the
content. Since we add them as well, this can cause up to three line
breaks at the end of a post, resulting in a blank space.
This removes the trailing line breaks before parsing the content.
2024-06-27 21:40:52 +02:00
LucasGGamerM
e8ce2a7e35 fix(account-sheet): properly check if the account is active
I LOVE RACE CONDITIONS :D
2024-06-25 16:53:22 -03:00
LucasGGamerM
f8dbecc3e1 Merge pull request #433 from FineFindus/fix/blocks
fix: block domains in block list
2024-06-21 16:36:11 -03:00
FineFindus
76030c041c fix: block domains in block list
Although a request is checked for a blocked domain, it is not actually
blocked.
2024-06-21 21:23:00 +02:00
LucasGGamerM
998e186f8b Merge pull request #432 from FineFindus/feat/update-blocks
feat: update internal block list
2024-06-21 15:26:08 -03:00
FineFindus
75bc0aa052 feat: update internal block list 2024-06-21 19:05:32 +02:00
LucasGGamerM
edb4b7152b chore: update bug_report.md
Swap Megalodon for Moshidon in issue template.
2024-06-17 09:14:49 -03:00
LucasGGamerM
66c9e0d908 fix(wrong-tab-selected-on-back): set the correct tab in after going back from a notification
Fixes #388
2024-06-16 10:37:50 -03:00
LucasGGamerM
0bdb23e462 Merge pull request #429 from FineFindus/feat/trending-links-timeline
feat(Discover): add Timeline to trending links
2024-06-16 09:44:13 -03:00
LucasGGamerM
d9ce0e6d31 Merge pull request #428 from FineFindus/fix/draft-error
fix(StatusDisplayItem): explictly copy filter list
2024-06-16 09:42:38 -03:00
FineFindus
aa3c8b5812 feat(Discover/TrendingLinkTimelineFragment): support prefilled compose text 2024-06-15 19:02:34 +02:00
FineFindus
4392ce20b6 feat(Discover/TrendingLinks): disable timeline on non 4.3.0 servers 2024-06-15 18:57:59 +02:00
FineFindus
d5085c5899 feat(Discover): add Timeline to trending links
Adds a timeline of statuses that posted about a trending link.
See https://github.com/mastodon/mastodon/pull/30381 for more details.
2024-06-15 18:49:47 +02:00
FineFindus
9a1668a29a fix(StatusDisplayItem): explictly copy filter list
Fixes an issue, where the app could crash when trying to add client-side
filters to an immutable list. This was the case for viewing scheduled
statuses
2024-06-15 18:44:38 +02:00
LucasGGamerM
4d598bd2fe Merge pull request #425 from FineFindus/fix/account-switch-reload
fix(AccountSwitcherSheet): only restart on different accounts
2024-06-14 14:10:24 -03:00
LucasGGamerM
57911ce070 Merge pull request #427 from FineFindus/fix/erro-null-url
fix(ErrorStatusDisplayItem): disable open in browser button on null URL
2024-06-13 16:29:35 -03:00
FineFindus
f9f8c4a9ef fix(ErrorStatusDisplayItem): disable open in browser button on null URL
Disables the Open in Browser, if the URL is null, as otherwise the app
would crash when trying to open the null URL.
2024-06-13 21:25:14 +02:00
LucasGGamerM
6ad8a85044 Merge pull request #426 from FineFindus/feat/error-displayitem
feat(ErrorDisplayItem): improve UI/UX with new design
2024-06-13 15:48:11 -03:00
FineFindus
14e6187efc feat(ErrorDisplayItem): improve UI/UX with new design
Updates the design of the ErrorStatusDisplayItem to be more
user-friendly. The new design displays an error message indicating that
an error has occurred while attempting to display the item. It then
offers the choice of either view the item in the browser or copy the
error details.
2024-06-13 20:43:29 +02:00
FineFindus
bd88606c48 fix(AccountSwitcherSheet): only restart on different accounts
Changes the AccountSwitcherSheet from always restarting the application
to only restarting if a different account is selected. This reduces the
friction of accidentally clicking on the same account.
2024-06-13 19:22:45 +02:00
LucasGGamerM
b38c78c50a Merge pull request #424 from FineFindus/patch-1
fix(HashtagTimelineFragment): display correct URL in recents menu
2024-06-13 08:20:40 -03:00
FineFindus
4c9f7fc8be fix(HashtagTimelineFragment): display correct URL in recents menu
Fixes an issue, where the hashtag, instead of the hashtagName was displayed in the recents menu, causing the toString() function of the hashtag to be called, which inlcuded all the hashtag data, producing a faulty URL.
2024-06-13 09:34:56 +02:00
LucasGGamerM
4f11a79d2a Merge pull request #422 from FineFindus/fix/honor-group-divider
fix: disable GroupDivider on Honor's MagicOS
2024-06-09 16:02:53 -03:00
FineFindus
7ab920d943 fix: disable GroupDivider on Honor's MagicOS
They have the same invisibility bug as EMUI.
2024-06-09 08:40:45 +02:00
LucasGGamerM
c8f2e7a752 fix(purple-theme): just straight up rewriting the purple theme 2024-05-30 17:26:50 -03:00
LucasGGamerM
cdcc428e86 fix(#421): make out of screen poll items update when clicking the "Show results" button 2024-05-29 15:24:59 -03:00
LucasGGamerM
7bb5584dd9 fix(haptic-feedback): readd the haptic feedback settings item to all android versions 2024-05-28 13:17:30 -03:00
LucasGGamerM
0c5c51dc17 Merge pull request #420
revert: readd haptic feedback setting on Android 11 and lower
2024-05-28 13:10:32 -03:00
FineFindus
b17b7afd03 revert: readd haptic feedback setting on Android 11 and lower
Some OEMs do not implement a systemwide setting for touch feedback.

This reverts commit e0a793e176.
2024-05-28 07:28:48 +02:00
LucasGGamerM
e2e8173db6 fix(image-viewer): add downloading toast when sharing videos/gifs 2024-05-27 16:42:24 -03:00
LucasGGamerM
5e7f4bda82 fix(image-viewer): put the download and share buttons back on a sensible place 2024-05-27 16:38:06 -03:00
LucasGGamerM
38996d8921 Merge pull request #419 from FineFindus/fix/bot-icon
fix(Search): display bot icon only for bots
2024-05-27 09:43:51 -03:00
FineFindus
6cb8961639 fix(Search): display bot icon only for bots
Due to the way Android handles lists, the icon could be wrongly shown on
non-bot accounts.

Fixes https://github.com/LucasGGamerM/moshidon/issues/418.
2024-05-27 13:41:25 +02:00
LucasGGamerM
18ac0423c0 Merge pull request #414 from TheMemeSniper/fix-autoreveal-cw
fix: make autoexpand content warning option also expand cws that start with "re:"
2024-05-26 11:49:02 -03:00
LucasGGamerM
d2704c1f0d Merge pull request #416 from FineFindus/refactor/remote-interaction
refactor: deduplicate remote interactions
2024-05-26 11:47:06 -03:00
LucasGGamerM
ed23b7cc13 Merge pull request #417 from FineFindus/feat/cleanup-settings
feat: cleanup settings
2024-05-26 11:44:50 -03:00
FineFindus
47ab6b5a08 fix(FooterStatusDisplayItem): implement applyInteraction 2024-05-26 16:39:37 +02:00
LucasGGamerM
70686bbbd0 Merge pull request #415 from FineFindus/refactor/sharing-img
refactor(PhotoViewer): use getFileProviderUri
2024-05-26 11:28:00 -03:00
LucasGGamerM
b53997261e fix(akkoma-hashtags): an issue where akkoma for some reason needs this for hashtags to open properly 2024-05-26 10:17:26 -03:00
FineFindus
efd9b1e916 feat: remove default forward reports setting 2024-05-25 11:46:25 +02:00
FineFindus
b51033a421 feat: remove unused loadRemoteAccountFollowers setting 2024-05-25 11:36:17 +02:00
FineFindus
e0a793e176 feat: remove haptic feedback setting
Haptic feedback behaviour can already be controlled from Anddroid
settings.
2024-05-25 11:31:58 +02:00
FineFindus
542c24ff75 refactor: deduplicate remote interactions 2024-05-25 11:26:45 +02:00
FineFindus
965f7c6d1d refactor(PhotoViewer): use getFileProviderUri 2024-05-25 11:11:41 +02:00
Kaitlin
2df6d9ce60 fix: make autoexpand content warning option also expand cws that start with re: or variations 2024-05-23 20:13:28 -05:00
LucasGGamerM
5d3afc1b0e Merge pull request #413
refactor: share img/show preview
2024-05-23 14:16:03 -03:00
Grishka
0c8f903eb6 Share sheet previews (AND-139) 2024-05-23 14:09:45 -03:00
Grishka
ef23734b22 Fix #842 2024-05-22 15:34:05 -03:00
FineFindus
c0ab3a47ae feat(PhotoViewer): rich previews for image sharing 2024-05-22 19:53:57 +02:00
FineFindus
f4a94bc42e refactor(PhotoViewer): deduplicate file sharing code 2024-05-22 19:51:10 +02:00
rex07
69b95c27ec Translated using Weblate (Arabic)
Currently translated at 15.0% (3 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ar/
2024-05-19 21:18:22 +00:00
LucasGGamerM
c64d6db859 Merge pull request #409 from FineFindus/feat/improve-poll-animation
feat(Poll): scale animation based on votes
2024-05-19 10:59:38 -03:00
LucasGGamerM
730adc34dd Merge pull request #410 from FineFindus/feat/fdroid-notification-warning
feat: warn if UP is disabled on FDroid variant
2024-05-19 10:57:31 -03:00
LucasGGamerM
a082a3d325 feat(crash-status-display-item): makes the app not crash when creating the status display items
This is the first part of a 2 part patch, because crashes can still happpen when the item is being "binded", which makes it necessary to handle those errors as well, which we currently DON'T do.

Also the Error item is still needing to be better, so there is also that to work on
2024-05-19 10:20:15 -03:00
FineFindus
c7820ddac8 feat: warn if UP is disabled on FDroid variant
Displays a warning in the notifications settings fragment, if
UnifiedPush is disabled (or cannot be enabled). The warning will be only
displayed on the FDroid build variant, since other versions use FCM by
default.
2024-05-19 13:36:45 +02:00
FineFindus
169fbc2d52 feat(Poll): scale animation based on votes
Updates the animation timing, to be based on the amount of votes a
option received relative to the other options. This means a option with
more votes will run longer than one with less votes. Overall this makes
the animation appear more dynamic and smoother.
2024-05-19 08:42:02 +02:00
LucasGGamerM
44e3e5faaf fix(compose-content-type-menu): hopefully fix a crash on this thing being null
Fuck java
2024-05-18 13:43:06 -03:00
LucasGGamerM
711c70af2f build: add name suffixes for F-Droid and GitHub versions 2024-05-18 13:28:30 -03:00
LucasGGamerM
1d405d9e48 Merge pull request #406
fix(ThreadFragment): pass correct account to ComposeView
2024-05-18 13:24:18 -03:00
LucasGGamerM
892ce130ca Merge pull request #405
feat(PollOptions): animate view results
2024-05-18 13:22:24 -03:00
LucasGGamerM
fea9d6e761 Merge pull request #408 from FineFindus/feat/recents
feat: display URL in recents for more fragments
2024-05-15 16:17:06 -03:00
FineFindus
88e11f25a7 feat(settings): display filter URL in recents 2024-05-15 15:38:03 +02:00
FineFindus
6faa497569 feat(settings): display notifications URL in recents 2024-05-15 15:37:33 +02:00
FineFindus
1d45899f8c feat(settings): display URL in recents overview 2024-05-15 15:36:47 +02:00
FineFindus
938643f9e2 fix(discover): provide WebUri for fragments
Fixes an issue, where the discover fragments did not display their URL
in the recents overview.
2024-05-15 15:35:30 +02:00
FineFindus
1ccf9bf4b7 fix(ThreadFragment): pass correct account to ComposeView
Fixes an issue, where the wrong account could be passed to the
ComposeView, when longpressing the replyBar and choosing an account.
2024-05-14 18:50:01 +02:00
FineFindus
ad9b5f028d feat(PollOptions): adjust animation curve 2024-05-12 21:29:50 +02:00
LucasGGamerM
e52154fd17 Merge pull request #404 from FineFindus/fix/custom-timeline-recents
fix(CustomLocalTimeline): set WebUri with scheme
2024-05-12 15:42:42 -03:00
FineFindus
54202f3e8d feat(PollOptions): animate view results 2024-05-12 20:42:28 +02:00
FineFindus
d4b8c350dc fix(CustomLocalTimeline): set WebUri with scheme 2024-05-12 13:59:47 +02:00
LucasGGamerM
daaf467168 style(browser-select-setting): remove unnecessary commented out code 2024-05-12 08:11:16 -03:00
LucasGGamerM
eda52d5a55 fix(browser-select-setting): remove unnecessary dialog option subtitle 2024-05-12 08:09:24 -03:00
LucasGGamerM
0700274d6b fix(browser-select-setting): don't query user's browser (excessive-permissions) 2024-05-12 08:07:36 -03:00
LucasGGamerM
faee3e3dd6 fix(custom-local-timelines-filters): check if filtered status is null before iterating on them 2024-05-11 15:06:37 -03:00
LucasGGamerM
129ce09c9f fix(get-client-filters): only add client filters if status is not previously filtered 2024-05-11 14:49:13 -03:00
LucasGGamerM
368e226257 fix(get-client-filters): make client filters List modifiable, avoiding a crash when trying to modify it 2024-05-11 14:25:44 -03:00
LucasGGamerM
93321720e1 Merge pull request #399 from FineFindus/fix/apply-filter-highlight
fix: apply filter highlight
2024-05-11 14:08:29 -03:00
LucasGGamerM
96c1c036a8 feat(save-backup): use ACTION_CREATE_DOCUMENT instead of "Sharing" the backup file 2024-05-11 14:05:48 -03:00
Espasant3
edffe0fd42 Translated using Weblate (Galician)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2024-05-09 20:18:22 +00:00
FineFindus
d1d8f2ef45 refactor(StatusDisplayItem): move client filters to AccountSession 2024-05-09 06:39:50 +02:00
LucasGGamerM
95ba52b761 fix(import-backup): check if json is not null before importing 2024-05-08 20:09:00 -03:00
LucasGGamerM
02c8a56c17 Merge pull request #400 from FineFindus/feat/import-export
feat: implement import/export of settings
2024-05-08 19:21:20 -03:00
LucasGGamerM
b34a855150 Merge pull request #396 from FineFindus/fix/hashtag-timeline
fix: fallback to hashtag name
2024-05-08 19:01:12 -03:00
FineFindus
b736cf2925 refactor(settings): remove debug log 2024-05-08 22:32:46 +02:00
FineFindus
eea78302ab feat(settings): implement settings import 2024-05-08 22:26:26 +02:00
FineFindus
09a7da2952 feat(settings): implement settings export 2024-05-08 22:25:01 +02:00
FineFindus
ebf3b075b8 fix(StatusDisplayItem): apply filter highlight
The filter highlight was not correctly shown, as the source text was
re-parsed when creating the TextStatusDisplayItem.
2024-05-08 17:08:18 +02:00
FineFindus
28c851a630 refactor: remove StatusFilterPredicate
Removes the deprecated StatusFilterPredicate class, as it has been
replaced upstream. Client-side filters are now directly applied in the
when building a StatusDisplayItem.
2024-05-08 16:43:33 +02:00
Espasant3
44194e5d43 Translated using Weblate (Galician)
Currently translated at 90.0% (18 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/gl/
2024-05-08 07:52:40 +00:00
Espasant3
58bb492461 Translated using Weblate (Galician)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2024-05-08 07:52:39 +00:00
FineFindus
00726abec1 feat(CustomLocalTimeline): set Public FitlterContext 2024-05-07 18:50:25 +02:00
FineFindus
c9e93bb6a6 fix: apply filters only in appropriate context
Currently Filters in AccountSession are applied regardless of the
FilterContext.
2024-05-07 18:49:27 +02:00
FineFindus
f980bba7cd fix: fallback to hashtag name
Fixes an issue, where Hashtag timeline where created with an empty
hashtag.
2024-05-07 18:28:47 +02:00
LucasGGamerM
d87d656002 chore: add 105 changelog 2024-05-05 16:52:55 -03:00
LucasGGamerM
9e9061e29c build: bump version number 2024-05-05 16:49:48 -03:00
LucasGGamerM
70d0ba88e4 fix(custom-tabs-setting): use upstream implementation
Seriously, why do we do this?
2024-05-05 16:47:12 -03:00
LucasGGamerM
9cb48e2f25 fix(mute-confirmation-sheet-timer-dialog): use Ok instead of Save 2024-05-05 16:21:44 -03:00
LucasGGamerM
6f89dd7331 fix(photo-viewer-menu-icons): readd share button on non-status media viewers 2024-05-05 16:18:11 -03:00
LucasGGamerM
a59c20d239 fix(menu-icons): disable them for android 14 and higher devices
This is due to a shitty bug with android 14 and InsetDrawables which I couldn´t fix yet
2024-05-05 16:07:26 -03:00
LucasGGamerM
3e36a72852 Merge pull request #386 from FineFindus/feat/open-about-links
feat(about): try to open links in-app
2024-05-05 16:06:22 -03:00
FineFindus
7801d28a23 refactor(SettingsServerAboutFragment): always use UiUtils.openURL 2024-05-05 21:02:40 +02:00
LucasGGamerM
c2e6c802a1 Merge pull request #385 from FineFindus/feat/remote-edit-history
feat(StatusEditHistory): load remote history
2024-05-05 16:02:19 -03:00
FineFindus
2dbfe88397 feat(StatusEditHistory): display subtitle on failed remote loading 2024-05-05 21:00:07 +02:00
LucasGGamerM
60f0a3d5ee Merge pull request #384 from FineFindus/fix/save-lists
fix(lists): send requests for list membership
2024-05-04 09:59:27 -03:00
FineFindus
86506f9ba4 feat(about): try to open links in-app
The About screen often contains links the local accounts (e.g.
moderators, admins) instead of always opening them in the browser, try
to open them in-app first.
2024-04-30 19:56:29 +02:00
FineFindus
7269788831 feat(StatusEditHistory): load remote history
Sometimes the server sends only the latest status instead of the full
history. Try to avoid this by requesting the history directly from the
server.
2024-04-30 19:10:03 +02:00
FineFindus
f7d0bda90f fix(lists): send requests for list membership
Fixes a regression in 0af8dbf09b, where
lists memberships where not saved, as the requests where not send to the
server.
2024-04-30 18:21:23 +02:00
0ko
eea350f84e Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2024-04-27 06:18:23 +00:00
lucasmz
44bec713ae Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2024-04-27 06:18:23 +00:00
LucasGGamerM
d5cd016776 fix(push-notification-receiver): check if status is null before doing stuff 2024-04-18 13:44:17 -03:00
LucasGGamerM
849172ad7f Merge pull request #377 from FineFindus/feat/post-menu
fix(compose): disable group divider on EMUI
2024-04-17 20:17:10 -03:00
LucasGGamerM
de57ceb939 fix(thread-fragment): use itemsToModify instead of items when adding the extended footer to the ThreadFragment 2024-04-17 07:44:16 -03:00
FineFindus
bf36f12f79 fix(compose): disable group divider on EMUI 2024-04-16 09:15:48 +02:00
LucasGGamerM
e9684441c0 Merge remote-tracking branch 'weblate/master' 2024-04-15 15:55:54 -03:00
Eugen Rochko
aa4bd9d5a3 New translations strings.xml (French) 2024-04-15 15:53:22 -03:00
Eugen Rochko
86f82f88da New translations strings.xml (French) 2024-04-15 15:53:22 -03:00
Eugen Rochko
fb5c8504ab New translations strings.xml (Portuguese, Brazilian) 2024-04-15 15:53:21 -03:00
Eugen Rochko
cff824e745 New translations strings.xml (Portuguese, Brazilian) 2024-04-15 15:53:21 -03:00
Eugen Rochko
200583f492 New translations strings.xml (Japanese) 2024-04-15 15:53:21 -03:00
Eugen Rochko
49e8083c9a New translations strings.xml (Turkish) 2024-04-15 15:53:21 -03:00
Eugen Rochko
15fe3595a3 New translations strings.xml (Portuguese, Brazilian) 2024-04-15 15:53:20 -03:00
Eugen Rochko
dd7e9be803 New translations strings.xml (Turkish) 2024-04-15 15:53:03 -03:00
Eugen Rochko
8ce7987591 New translations strings.xml (Vietnamese) 2024-04-15 15:52:54 -03:00
Eugen Rochko
0b7f5f7f64 New translations strings.xml (Basque) 2024-04-15 15:52:47 -03:00
Eugen Rochko
db7d223be6 New translations strings.xml (Thai) 2024-04-15 15:52:46 -03:00
Eugen Rochko
cabbc934ec New translations strings.xml (Icelandic) 2024-04-15 15:52:46 -03:00
Eugen Rochko
c277a13d39 New translations strings.xml (Italian) 2024-04-15 15:52:46 -03:00
Eugen Rochko
404de46eee New translations strings.xml (French) 2024-04-15 15:52:38 -03:00
Eugen Rochko
b5f062ca99 New translations strings.xml (French) 2024-04-15 15:52:37 -03:00
Eugen Rochko
6d1ef8b59d New translations strings.xml (Chinese Traditional) 2024-04-15 15:52:37 -03:00
Eugen Rochko
83ced914ef New translations strings.xml (Slovenian) 2024-04-15 15:52:37 -03:00
Eugen Rochko
08a468c56f New translations strings.xml (Dutch) 2024-04-15 15:52:31 -03:00
Eugen Rochko
66cb74249e New translations strings.xml (French) 2024-04-15 15:52:31 -03:00
Eugen Rochko
2d10387404 New translations strings.xml (Dutch) 2024-04-15 15:52:31 -03:00
Eugen Rochko
60de667feb New translations strings.xml (Icelandic) 2024-04-15 15:52:25 -03:00
Eugen Rochko
d01fa27f85 New translations strings.xml (Icelandic) 2024-04-15 15:52:24 -03:00
Eugen Rochko
56560e741c New translations strings.xml (French) 2024-04-15 15:52:17 -03:00
Eugen Rochko
3d57954866 New translations strings.xml (French) 2024-04-15 15:52:17 -03:00
Eugen Rochko
dab3ad79ed New translations strings.xml (Lithuanian) 2024-04-15 15:51:20 -03:00
Eugen Rochko
0d9ac99313 New translations strings.xml (Basque) 2024-04-15 15:51:12 -03:00
Eugen Rochko
2883881402 New translations strings.xml (Armenian) 2024-04-15 15:51:04 -03:00
Eugen Rochko
760cfa0b9f New translations strings.xml (Armenian) 2024-04-15 15:51:04 -03:00
Eugen Rochko
ceb89e4dd7 New translations strings.xml (Armenian) 2024-04-15 15:51:04 -03:00
Eugen Rochko
f3c9b4e2cf New translations strings.xml (Armenian) 2024-04-15 15:51:03 -03:00
Eugen Rochko
cc07c539e0 New translations strings.xml (Japanese) 2024-04-15 15:50:50 -03:00
Eugen Rochko
57ff23e8c1 New translations strings.xml (Chinese Traditional) 2024-04-15 15:50:45 -03:00
Eugen Rochko
bfe6559690 New translations strings.xml (Russian) 2024-04-15 15:50:32 -03:00
Eugen Rochko
138e633acd New translations strings.xml (Czech) 2024-04-15 15:50:27 -03:00
Eugen Rochko
68ab66bed7 New translations strings.xml (Czech) 2024-04-15 15:50:26 -03:00
Eugen Rochko
f22de651c9 New translations strings.xml (Greek) 2024-04-15 15:50:20 -03:00
Eugen Rochko
5a46ae8d7b New translations strings.xml (Greek) 2024-04-15 15:50:19 -03:00
Eugen Rochko
297c37a72e New translations strings.xml (Hungarian) 2024-04-15 15:50:14 -03:00
Eugen Rochko
a6f654cb36 New translations strings.xml (Hungarian) 2024-04-15 15:50:14 -03:00
Eugen Rochko
f6a844138b New translations strings.xml (Thai) 2024-04-15 15:50:09 -03:00
Eugen Rochko
2d857f4f1b New translations strings.xml (Chinese Traditional) 2024-04-15 15:38:04 -03:00
Eugen Rochko
915f2ca108 New translations strings.xml (Slovenian) 2024-04-15 15:38:04 -03:00
Eugen Rochko
cd49b5f0b4 New translations strings.xml (Japanese) 2024-04-15 15:38:04 -03:00
Eugen Rochko
7d22f93b5d New translations strings.xml (Spanish) 2024-04-15 15:38:04 -03:00
Eugen Rochko
da74ae0a7c New translations strings.xml (Spanish) 2024-04-15 15:37:51 -03:00
Eugen Rochko
d67ab02443 New translations strings.xml (Spanish) 2024-04-15 15:37:50 -03:00
Eugen Rochko
c821326326 New translations strings.xml (Slovenian) 2024-04-15 15:37:44 -03:00
Eugen Rochko
464c80ed8f New translations strings.xml (Japanese) 2024-04-15 15:37:37 -03:00
Eugen Rochko
4adf250645 New translations strings.xml (Japanese) 2024-04-15 15:37:37 -03:00
Eugen Rochko
190755d2cc New translations strings.xml (Chinese Traditional) 2024-04-15 15:37:31 -03:00
Eugen Rochko
6deae2341b New translations strings.xml (Chinese Traditional) 2024-04-15 15:37:30 -03:00
Eugen Rochko
a23b39255d New translations strings.xml (Kabyle) 2024-04-15 15:37:23 -03:00
Eugen Rochko
20378cc1c5 New translations strings.xml (Scottish Gaelic) 2024-04-15 15:37:22 -03:00
Eugen Rochko
0c5f7552e0 New translations strings.xml (Bosnian) 2024-04-15 15:37:22 -03:00
Eugen Rochko
99e382c9f8 New translations strings.xml (Filipino) 2024-04-15 15:37:22 -03:00
Eugen Rochko
ec332cb867 New translations strings.xml (Burmese) 2024-04-15 15:37:22 -03:00
Eugen Rochko
4e3ddaf782 New translations strings.xml (Croatian) 2024-04-15 15:37:21 -03:00
Eugen Rochko
4a4fec06f6 New translations strings.xml (Thai) 2024-04-15 15:37:21 -03:00
Eugen Rochko
43d4978b1b New translations strings.xml (Bengali) 2024-04-15 15:37:21 -03:00
Eugen Rochko
7fbfae7388 New translations strings.xml (Indonesian) 2024-04-15 15:37:20 -03:00
Eugen Rochko
1772655c8c New translations strings.xml (Portuguese, Brazilian) 2024-04-15 15:37:20 -03:00
Eugen Rochko
6631de85e3 New translations strings.xml (Icelandic) 2024-04-15 15:37:20 -03:00
Eugen Rochko
a19a39ff51 New translations strings.xml (Galician) 2024-04-15 15:37:20 -03:00
Eugen Rochko
89f96cba52 New translations strings.xml (Vietnamese) 2024-04-15 15:37:19 -03:00
Eugen Rochko
dae32e2645 New translations strings.xml (Chinese Traditional) 2024-04-15 15:37:19 -03:00
Eugen Rochko
3af3993e1d New translations strings.xml (Chinese Simplified) 2024-04-15 15:37:19 -03:00
Eugen Rochko
9cd0b90c9a New translations strings.xml (Ukrainian) 2024-04-15 15:37:18 -03:00
Eugen Rochko
798325339f New translations strings.xml (Turkish) 2024-04-15 15:37:18 -03:00
Eugen Rochko
d17e25e7eb New translations strings.xml (Swedish) 2024-04-15 15:37:18 -03:00
Eugen Rochko
73f15caae2 New translations strings.xml (Slovenian) 2024-04-15 15:37:18 -03:00
Eugen Rochko
76c64195b0 New translations strings.xml (Portuguese) 2024-04-15 15:37:17 -03:00
Eugen Rochko
e1315eafde New translations strings.xml (Norwegian) 2024-04-15 15:37:17 -03:00
Eugen Rochko
1c9b7b8cbe New translations strings.xml (Lithuanian) 2024-04-15 15:37:17 -03:00
Eugen Rochko
f9844a7337 New translations strings.xml (Korean) 2024-04-15 15:36:54 -03:00
Eugen Rochko
4cb0eed126 New translations strings.xml (Georgian) 2024-04-15 15:36:53 -03:00
Eugen Rochko
bfab791850 New translations strings.xml (Japanese) 2024-04-15 15:36:49 -03:00
Eugen Rochko
a2767d6002 New translations strings.xml (Italian) 2024-04-15 15:36:48 -03:00
Eugen Rochko
62a65ab4c6 New translations strings.xml (Armenian) 2024-04-15 15:36:48 -03:00
Eugen Rochko
04d6ca9485 New translations strings.xml (Finnish) 2024-04-15 15:36:48 -03:00
Eugen Rochko
36620d7415 New translations strings.xml (Basque) 2024-04-15 15:36:48 -03:00
Eugen Rochko
d5e9697420 New translations strings.xml (German) 2024-04-15 15:36:47 -03:00
Eugen Rochko
e5da0681ae New translations strings.xml (Danish) 2024-04-15 15:36:47 -03:00
Eugen Rochko
863ff48bd4 New translations strings.xml (Czech) 2024-04-15 15:36:47 -03:00
Eugen Rochko
a9948ee996 New translations strings.xml (Arabic) 2024-04-15 15:36:47 -03:00
Eugen Rochko
fba231c855 New translations strings.xml (Spanish) 2024-04-15 15:36:47 -03:00
Eugen Rochko
09911e4494 New translations strings.xml (French) 2024-04-15 15:36:46 -03:00
Eugen Rochko
ff2407239f New translations strings.xml (Dutch) 2024-04-15 15:36:46 -03:00
Eugen Rochko
4d990647f3 New translations strings.xml (Persian) 2024-04-15 15:36:46 -03:00
Eugen Rochko
5ac91605ed New translations strings.xml (Catalan) 2024-04-15 15:36:45 -03:00
Eugen Rochko
4a8a3530ab New translations strings.xml (Belarusian) 2024-04-15 15:36:45 -03:00
Eugen Rochko
e8a600ff29 New translations strings.xml (Hindi) 2024-04-15 15:36:45 -03:00
Eugen Rochko
6df1cffc55 New translations strings.xml (Greek) 2024-04-15 15:36:40 -03:00
Eugen Rochko
5826b17840 New translations strings.xml (Russian) 2024-04-15 15:36:39 -03:00
Eugen Rochko
fdc052f8f1 New translations strings.xml (Polish) 2024-04-15 15:36:39 -03:00
Eugen Rochko
a59774fe78 New translations strings.xml (Hungarian) 2024-04-15 15:36:39 -03:00
Eugen Rochko
19bf9c4ee6 New translations strings.xml (Dutch) 2024-04-15 15:36:31 -03:00
edxkl
2139dbd76b Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.5% (413 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2024-04-13 21:18:28 +00:00
edxkl
aab829ac4d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (100 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pt_BR/
2024-04-13 21:18:24 +00:00
LucasGGamerM
d047379785 fix(compose-autocomplete): add null checks to hashtags logic
Fuck java
2024-04-12 16:53:43 -03:00
LucasGGamerM
ff9587661e fix(emoji-selector): fixes crash when you typed an emoji's name and pressed the virtual keyboard's emoji suggestion
@sk22 might like this one
2024-04-12 16:29:49 -03:00
LucasGGamerM
e6a4f81b78 fix(client-filters): readd the necessary logic for them to work again 2024-04-12 14:39:33 -03:00
jonta
ad92a08271 Translated using Weblate (Portuguese (Brazil))
Currently translated at 91.1% (382 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2024-04-11 15:18:21 +00:00
LucasGGamerM
038923bf8f Merge pull request #374 from FineFindus/fix/show-filter-warning
fix: replace correct filter item
2024-04-10 12:39:54 -03:00
LucasGGamerM
b0518b807e Merge pull request #373 from FineFindus/fix/open-url
fix(search): open lemmy URLs
2024-04-10 12:39:19 -03:00
LucasGGamerM
2c743b11e6 fix(unmuting): add request body when unmuting to prevent failed POST request 2024-04-10 12:37:55 -03:00
LucasGGamerM
6c519b3cb9 feat(mute-sheet): use secondary button for mute duration instead of a new row 2024-04-10 12:30:02 -03:00
FineFindus
5ed4fdbb36 fix: replace correct filter item 2024-04-10 17:22:11 +02:00
FineFindus
a844852669 fix(search): open lemmy URLs
Closes https://github.com/LucasGGamerM/moshidon/issues/349.
2024-04-10 16:45:02 +02:00
LucasGGamerM
4f3d711d2b Merge pull request #371
feat: add mute duration and notifications to mute confirmation sheet
2024-04-07 13:08:50 -03:00
LucasGGamerM
24be1c68ad fix(restriction-confirmation-sheet): make secondary button appear on top of primary button
This is the first part of the chore to readd the mute timer thing
2024-04-07 08:42:36 -03:00
FineFindus
fe96ee5f2a feat: propagate row clicks to child 2024-04-02 20:04:40 +02:00
FineFindus
e6501ad8a3 feat: add mute notifications toggle 2024-04-02 19:53:54 +02:00
FineFindus
18f8c1d29e refactor: only add body for muting requests 2024-04-02 19:34:26 +02:00
FineFindus
56540d7e7b feat: re-add mute duration option 2024-04-02 19:32:08 +02:00
FineFindus
37f63b1142 feat: add support for rows with custom views 2024-04-02 19:31:22 +02:00
LucasGGamerM
da3c19cdd0 Merge pull request #367 from Mattis142/master
Add Korean Store listing translation
2024-03-30 08:09:49 -03:00
LucasGGamerM
9a28d23be0 fix(photo-viewer-sheet): make alt text selectable again 2024-03-30 07:56:14 -03:00
LucasGGamerM
51a9c7c58e Merge branch 'fix/double_push' 2024-03-29 18:00:02 -03:00
lyyn
8e00b04f71 Fix missing case for STATUS notification title 2024-03-29 17:53:48 -03:00
LucasGGamerM
d08c64e749 Merge pull request #368 from FineFindus/fix/open-link-browser
fix: use correct openURL function
2024-03-28 17:32:44 -03:00
FineFindus
974774a913 fix: use correct openURL function 2024-03-28 20:49:51 +01:00
Mattis142
608ac60aec Delete metadata/ko/images/phoneScreenshots/test 2024-03-27 16:48:48 +01:00
Mattis142
a4586f2c0c Added Korean translation for Store listing 2024-03-27 16:48:13 +01:00
Mattis142
1da2df220b Create test 2024-03-27 16:46:53 +01:00
CDN18
13ed669423 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/zh_Hans/
2024-03-25 21:18:21 +00:00
Titanium
0046fa5d9e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/zh_Hans/
2024-03-25 21:18:21 +00:00
LucasGGamerM
0931702802 fix(custom-local-timelines): revert 629262c266 and fix the crash for good
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
2024-03-24 15:13:24 -03:00
LucasGGamerM
d7a229afeb Merge pull request #366 from FineFindus/feat/remove-home-longpress
feat: remove home tab longpress
2024-03-24 14:50:13 -03:00
LucasGGamerM
14f260d6a1 fix(#359): use upstream ripple overlay alpha value 2024-03-24 14:49:40 -03:00
FineFindus
2ab74de236 feat: remove home tab longpress 2024-03-23 12:03:40 +01:00
LucasGGamerM
7a4387a459 Merge pull request #364 from FineFindus/feat/replybar-longpress
feat: support longpressing replyBar
2024-03-22 18:18:50 -03:00
LucasGGamerM
629262c266 fix(custom-local-timelines): crash whenever a mention is shown
I don't know what caused this. Probably the HTML parser, but its now fixed forever, hopefully this thing does not happen again :)
2024-03-22 15:25:06 -03:00
FineFindus
a7c558afd8 feat: support longpressing replyBar 2024-03-22 13:21:31 +01:00
LucasGGamerM
8d6593e12c feat(search): add back bot indicator to account search results preview 2024-03-20 17:20:19 -03:00
LucasGGamerM
72cf5c3b59 fix(profile-fragment): add a margin to the domain button in the profile 2024-03-20 16:02:46 -03:00
LucasGGamerM
b018b817e8 Merge pull request #362
fix(profile): display username icons after server
2024-03-20 15:46:11 -03:00
LucasGGamerM
bf0c80d6b9 Merge pull request #363
fix: increase boost button witdth
2024-03-20 15:45:30 -03:00
FineFindus
daa391a877 fix: increase boost button witdth 2024-03-20 13:42:15 +01:00
FineFindus
1fedfce00d fix(profile): display username icons after server 2024-03-20 13:23:00 +01:00
gallegonovato
78805116f8 Translated using Weblate (Spanish)
Currently translated at 100.0% (38 of 38 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-03-20 11:53:34 +00:00
butterflyoffire
98a9966461 Translated using Weblate (French)
Currently translated at 92.0% (92 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/fr/
2024-03-20 11:53:34 +00:00
gallegonovato
2d127daf67 Translated using Weblate (Spanish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-03-20 11:53:34 +00:00
LucasGGamerM
2497ece9f1 fix(avatar-span): don´t check for handleClick to be null
This makes it so the avatar span is always shown, even when handleClick is null
2024-03-19 14:30:27 -03:00
LucasGGamerM
44b9f8c0ec Merge pull request #360 from FineFindus/fix/inline-reply-short
fix: display inline avatar for boosted replies
2024-03-19 14:18:11 -03:00
FineFindus
6ae234cf42 refactor(inline-avatar): find displayname using string search 2024-03-19 15:43:21 +01:00
FineFindus
39f3e72a47 fix: display inline avatar on short text 2024-03-19 09:14:09 +01:00
SomeTr
b1e2cab5fa Translated using Weblate (Ukrainian)
Currently translated at 100.0% (38 of 38 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/uk/
2024-03-18 17:53:34 +00:00
CDN18
75da570682 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (38 of 38 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/zh_Hans/
2024-03-18 17:53:34 +00:00
ghose
e08718fc08 Translated using Weblate (Galician)
Currently translated at 50.0% (19 of 38 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/gl/
2024-03-18 17:53:34 +00:00
CDN18
4ebe4434f2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (100 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/zh_Hans/
2024-03-18 17:53:33 +00:00
SomeTr
5cc9ecc8e0 Translated using Weblate (Ukrainian)
Currently translated at 84.0% (84 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/uk/
2024-03-18 17:53:33 +00:00
ghose
e943ac1a05 Translated using Weblate (Galician)
Currently translated at 100.0% (100 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/gl/
2024-03-18 17:53:33 +00:00
locness3
c439171a08 Translated using Weblate (French)
Currently translated at 89.0% (89 of 100 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/fr/
2024-03-18 17:53:33 +00:00
LucasGGamerM
a9c6bf24fd fix(avatar-span): make the avatar appear more centered relative to the rest of the text 2024-03-17 20:16:14 -03:00
LucasGGamerM
ad4423526d fix(extended-footer): readd "last edit"'s button icon 2024-03-17 20:01:30 -03:00
LucasGGamerM
445bf28e94 fix(extended-footer): make bottom padding smaller 2024-03-17 19:57:00 -03:00
LucasGGamerM
f33edfa70f Merge branch 'feat/inline-reply-avatar' 2024-03-17 16:31:47 -03:00
LucasGGamerM
e694715dae Merge branch 'bugfix/965'
# Conflicts:
#	mastodon/src/main/AndroidManifest.xml
2024-03-17 16:25:49 -03:00
LucasGGamerM
8f69358d34 Merge pull request #357
feat: use PhotoViewerInfoSheet buttons
2024-03-17 13:39:25 -03:00
ptrwrbl
f425a73c74 Translated using Weblate (Polish)
Currently translated at 100.0% (37 of 37 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/pl/
2024-03-17 16:28:29 +00:00
ptrwrbl
1b1138242b Translated using Weblate (Polish)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pl/
2024-03-17 16:28:29 +00:00
dogasert01
5fae68f68f Translated using Weblate (Turkish)
Currently translated at 21.2% (21 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/tr/
2024-03-17 16:28:29 +00:00
mihn
0100ecff9b Translated using Weblate (Vietnamese)
Currently translated at 98.9% (98 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/vi/
2024-03-17 16:28:29 +00:00
SomeTr
8651db933a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (37 of 37 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/uk/
2024-03-17 16:28:29 +00:00
dontobi
5082930a80 Translated using Weblate (German)
Currently translated at 100.0% (37 of 37 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/de/
2024-03-17 16:28:29 +00:00
dabid
cc8a81aa31 Translated using Weblate (Basque)
Currently translated at 2.7% (1 of 37 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/eu/
2024-03-17 16:28:29 +00:00
gallegonovato
ccb22802da Translated using Weblate (Spanish)
Currently translated at 100.0% (37 of 37 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-03-17 16:28:29 +00:00
dabid
c55f2056c2 Translated using Weblate (Basque)
Currently translated at 70.7% (70 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/eu/
2024-03-17 16:28:29 +00:00
dabid
6f5af40f2a Translated using Weblate (Spanish)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-03-17 16:28:29 +00:00
dabid
2fb879bd0d Translated using Weblate (Basque)
Currently translated at 64.6% (64 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/eu/
2024-03-17 16:28:29 +00:00
LucasGGamerM
a95c0058d8 Merge pull request #358
feat: add instance info button to DecentralizationExplainerSheet
2024-03-17 13:28:11 -03:00
LucasGGamerM
b1b03a3ada Merge pull request #355
fix: use correct hashtag text to prevent crash
2024-03-17 13:26:19 -03:00
LucasGGamerM
6b5344bd11 Merge pull request #356
feat: display custom emoji in reply button
2024-03-17 13:23:17 -03:00
FineFindus
4d91ff3866 feat(DecentralizationExplainerSheet): add button to open about page 2024-03-17 15:55:14 +01:00
FineFindus
a488776daa feat(profile): display DecentralizationExplainerSheet by clicking on username 2024-03-17 15:53:49 +01:00
FineFindus
740ff45bb6 feat: use photo info sheet buttons 2024-03-17 15:33:16 +01:00
FineFindus
7773c08387 fix(sheet-photo-viewer-info): increase button width 2024-03-17 15:30:03 +01:00
LucasGGamerM
4a13398801 feat(profile): make the new decentralization sheet appear only when holding the instance button 2024-03-17 10:37:02 -03:00
FineFindus
685e6e947f feat: display custom emoji in reply button 2024-03-17 14:18:20 +01:00
LucasGGamerM
1dd46df540 feat(profile): add the upstream handle thingys 2024-03-17 10:11:40 -03:00
FineFindus
c73fa2dd6b fix: use correct hashtag text
Fixes a regression introduced in 0af8dbf09b, taht caused the app to crash when long pressing on a hastag to copy it
2024-03-17 14:11:32 +01:00
LucasGGamerM
ad1de60968 fix(extended-footer): make it appear on top of the standard post footer 2024-03-17 09:49:56 -03:00
LucasGGamerM
d8a88d1803 fix(extended-footer-icons): makes them look good again 2024-03-17 09:46:30 -03:00
LucasGGamerM
f29a9b6748 Revert "feat(extended-footer): use lighter background color"
This reverts commit 7c8d0cb459.
2024-03-17 09:38:29 -03:00
LucasGGamerM
60be7191aa Merge pull request #352
feat: improve extended footer
2024-03-17 09:37:22 -03:00
LucasGGamerM
307f886ea5 Merge pull request #351
feat: show reply sheet on reply button
2024-03-17 09:32:25 -03:00
LucasGGamerM
47473a6372 Merge pull request #353
fix(remote-info): remove null check
2024-03-17 09:21:38 -03:00
LucasGGamerM
25f9e60527 fix(mentions-tab): make mentions tab actually show only mentions 2024-03-17 09:16:35 -03:00
FineFindus
a7b51095fb fix(remote-info): remove null check
Fixes a regression introduced  in 76867a971b, that caused remote info to be permanently disabled
2024-03-17 00:29:33 +01:00
FineFindus
75f14371d5 feat(extended-footer): add icons to boost/fav btns 2024-03-17 00:07:57 +01:00
FineFindus
7c8d0cb459 feat(extended-footer): use lighter background color 2024-03-17 00:07:36 +01:00
FineFindus
14a36d2602 feat: show reply sheet on reply button 2024-03-16 23:34:00 +01:00
LucasGGamerM
3939fc9795 chore(make-app-compile-again): just what the actual fuck did I just do 2024-03-16 18:23:11 -03:00
LucasGGamerM
06a26d308f Merge branch 'merge-upstream'
# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java
#	mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java
#	mastodon/src/main/java/org/joinmastodon/android/MainActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java
#	mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/AddAccountsToList.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/CreateList.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/DeleteList.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/GetList.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/GetLists.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/RemoveAccountsFromList.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/UpdateList.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusEditHistory.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetPublicTimeline.java
#	mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java
#	mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java
#	mastodon/src/main/java/org/joinmastodon/android/events/ListDeletedEvent.java
#	mastodon/src/main/java/org/joinmastodon/android/events/ListUpdatedCreatedEvent.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/CustomLocalTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ListsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/Status.java
#	mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java
#	mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/AccountViewModel.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/sheets/AccountSwitcherSheet.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/ActionModeHelper.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeLanguageAlertViewController.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/views/ListEditor.java
#	mastodon/src/main/res/drawable/fg_link_card.xml
#	mastodon/src/main/res/drawable/ic_arrow_drop_down_24px.xml
#	mastodon/src/main/res/drawable/ic_arrow_right_24px.xml
#	mastodon/src/main/res/drawable/ic_arrow_upward_24px.xml
#	mastodon/src/main/res/drawable/ic_bookmark_fill1_24px.xml
#	mastodon/src/main/res/drawable/ic_boost_24px.xml
#	mastodon/src/main/res/drawable/ic_boost_private.xml
#	mastodon/src/main/res/drawable/ic_help_24px.xml
#	mastodon/src/main/res/drawable/ic_info_fill1_24px.xml
#	mastodon/src/main/res/drawable/ic_m3_cancel.xml
#	mastodon/src/main/res/layout/display_item_extended_footer.xml
#	mastodon/src/main/res/layout/display_item_link_card.xml
#	mastodon/src/main/res/layout/fragment_onboarding_follow_suggestions.xml
#	mastodon/src/main/res/layout/fragment_onboarding_profile_setup.xml
#	mastodon/src/main/res/layout/home_toolbar.xml
#	mastodon/src/main/res/layout/item_discover_account.xml
#	mastodon/src/main/res/layout/item_generic_list_content.xml
#	mastodon/src/main/res/menu/post.xml
#	mastodon/src/main/res/menu/profile.xml
#	mastodon/src/main/res/values-ar-rSA/strings.xml
#	mastodon/src/main/res/values-be-rBY/strings.xml
#	mastodon/src/main/res/values-ca-rES/strings.xml
#	mastodon/src/main/res/values-cs-rCZ/strings.xml
#	mastodon/src/main/res/values-da-rDK/strings.xml
#	mastodon/src/main/res/values-eu-rES/strings.xml
#	mastodon/src/main/res/values-fa-rIR/strings.xml
#	mastodon/src/main/res/values-fi-rFI/strings.xml
#	mastodon/src/main/res/values-fil-rPH/strings.xml
#	mastodon/src/main/res/values-fr-rFR/strings.xml
#	mastodon/src/main/res/values-gd-rGB/strings.xml
#	mastodon/src/main/res/values-gl-rES/strings.xml
#	mastodon/src/main/res/values-hu-rHU/strings.xml
#	mastodon/src/main/res/values-hy-rAM/strings.xml
#	mastodon/src/main/res/values-in-rID/strings.xml
#	mastodon/src/main/res/values-is-rIS/strings.xml
#	mastodon/src/main/res/values-it-rIT/strings.xml
#	mastodon/src/main/res/values-ja-rJP/strings.xml
#	mastodon/src/main/res/values-kab/strings.xml
#	mastodon/src/main/res/values-ko-rKR/strings.xml
#	mastodon/src/main/res/values-nl-rNL/strings.xml
#	mastodon/src/main/res/values-pl-rPL/strings.xml
#	mastodon/src/main/res/values-pt-rBR/strings.xml
#	mastodon/src/main/res/values-pt-rPT/strings.xml
#	mastodon/src/main/res/values-ro-rRO/strings.xml
#	mastodon/src/main/res/values-ru-rRU/strings.xml
#	mastodon/src/main/res/values-sl-rSI/strings.xml
#	mastodon/src/main/res/values-sv-rSE/strings.xml
#	mastodon/src/main/res/values-tr-rTR/strings.xml
#	mastodon/src/main/res/values-uk-rUA/strings.xml
#	mastodon/src/main/res/values-zh-rCN/strings.xml
#	mastodon/src/main/res/values/attrs.xml
#	mastodon/src/main/res/values/palettes.xml
#	mastodon/src/main/res/values/strings.xml
#	mastodon/src/main/res/values/styles.xml
2024-03-16 18:21:22 -03:00
LucasGGamerM
d1656a525e fix: make action button text on account card look decent again 2024-03-16 17:59:17 -03:00
ptrwrbl
b0dc521b90 Translated using Weblate (Polish)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2024-03-09 15:13:09 +00:00
ptrwrbl
732de52ebb Translated using Weblate (Polish)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2024-03-09 15:13:09 +00:00
LucasGGamerM
e9b6acb92d feat: readd prereply sheets from upstream back into the code 2024-03-04 21:00:30 -03:00
LucasGGamerM
75e8d738a4 fix: crash when opening other people's profiles 2024-03-04 20:42:34 -03:00
LucasGGamerM
ec02aed557 feat: add the new block confirmation sheet from upstream 2024-03-04 20:25:24 -03:00
LucasGGamerM
20d0acc88c chore: make the mute confirmation dialog work
TODO: Readd the time option
2024-03-04 16:40:10 -03:00
LucasGGamerM
71dd974d19 chore: readd all the new bottom sheet code 2024-03-04 16:29:57 -03:00
LucasGGamerM
0d7e10ca8a fix(status-header): use correct icon for manage user lists menu entry 2024-03-03 16:46:03 -03:00
LucasGGamerM
fa31b0f2d6 fix(status-header): use correct name for manage user lists and remove duplicate copy link menu entrys 2024-03-03 16:44:06 -03:00
LucasGGamerM
a91e08fe1a fix(onboarding-follow-suggestions): use the tabbar background drawable there
This makes the fragment look good
2024-03-03 16:43:22 -03:00
LucasGGamerM
3ea8b370bb fix(link-card): use placeholder image when there is no card image
And round off the corners too
2024-03-03 16:25:09 -03:00
LucasGGamerM
02eb178443 fix(onboarding-follow-suggestions): remove non-wanted code and fix a crash 2024-03-03 16:04:42 -03:00
LucasGGamerM
e8b77d0dfa fix(list-members fragment): fix compiler cries and readd some code 2024-03-03 15:42:17 -03:00
LucasGGamerM
f880d4b7b6 fix(snackbars): add the required colors to pallete to make them look good 2024-03-03 12:03:57 -03:00
LucasGGamerM
f5514e35e1 fix(extended-footer): use proper colors for extended footer 2024-03-03 11:50:44 -03:00
LucasGGamerM
193ef88814 fix(extended-footer): readd visibility indicator 2024-03-03 11:43:13 -03:00
LucasGGamerM
7de58a5e35 fix(extended-footer): make everything align 2024-03-03 11:35:38 -03:00
LucasGGamerM
633261c16b chore(merge-upstream): commenting out the lines that made the compilation fail 2024-03-03 11:02:12 -03:00
LucasGGamerM
e295c8c381 chore(merge-upstream): readd has subtitle method to BaseAccountListFragment 2024-03-03 10:33:49 -03:00
LucasGGamerM
e71232ad40 chore(merge-upstream): use extended footer from upstream for now 2024-03-03 10:23:38 -03:00
Steph Kraemer
123ed6c56d Fix for #965 - don't backup accounts.json 2024-03-01 20:22:33 -05:00
LucasGGamerM
cf31a1be57 chore(merge-upstream): a bunch of stuff is still left to be done :D 2024-02-27 20:53:32 -03:00
LucasGGamerM
64e82bdeed chore(merge-upstream): still 21 things to do 2024-02-18 14:19:05 -03:00
LucasGGamerM
76867a971b chore(merge-upstream): more conflict solving tomfoolery 2024-02-16 20:21:38 -03:00
LucasGGamerM
0af8dbf09b chore(merging-upstream): bunch of conflicts to solve 2024-02-14 21:08:16 -03:00
Grishka
8dffbff97c Domain badges & info sheet & my fanciest animation yet 2024-02-13 07:31:42 +03:00
Grishka
efb8cd565b Update locales 2024-02-12 21:40:28 +03:00
Grishka
1f5bdb975b Merge branch 'l10n_master' 2024-02-12 21:38:00 +03:00
Grishka
22dfc33974 Update strings 2024-02-12 21:37:44 +03:00
FineFindus
2071602607 feat: display boost timestamp 2024-02-10 14:54:58 +01:00
FineFindus
72940832a6 fix(ReblogOrReplyLineStatusDisplayItem): only display avatar for bossting 2024-02-10 13:55:31 +01:00
FineFindus
8e984a7cad feat: inline avatars for reblogline 2024-02-09 23:13:50 +01:00
EndermanCo
34b2a4e2a0 Translated using Weblate (Persian)
Currently translated at 65.0% (13 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2024-02-09 13:56:33 +00:00
butterflyoffire
2291c2bb28 Translated using Weblate (French)
Currently translated at 94.9% (398 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2024-02-09 13:56:33 +00:00
Grishka
6915d19fb4 fix 2024-02-09 03:38:00 +03:00
Grishka
ad2ef39ace AND-122 Mute, block, and domain block confirmation screens 2024-02-09 03:27:05 +03:00
Eugen Rochko
3cff655e6f New translations strings.xml (Persian) 2024-02-08 14:34:47 +01:00
Eugen Rochko
ed86a5a3e8 New translations strings.xml (Russian) 2024-02-07 19:48:30 +01:00
Eugen Rochko
f329435f51 New translations strings.xml (Catalan) 2024-02-06 18:19:58 +01:00
S1m
c8604ad68e Fix NullPointerException with double push 2024-02-06 09:14:31 +01:00
Eugen Rochko
6a6a80bcd7 New translations strings.xml (Belarusian) 2024-02-03 17:27:48 +01:00
Eugen Rochko
62e4983f02 New translations strings.xml (Hindi) 2024-02-03 09:56:54 +01:00
Eugen Rochko
6dfd991e87 New translations strings.xml (Hindi) 2024-02-03 08:52:38 +01:00
Eugen Rochko
e205462bf4 New translations short_description.txt (Hindi) 2024-02-03 07:47:35 +01:00
Eugen Rochko
03f341f6f8 New translations full_description.txt (Hindi) 2024-02-03 07:47:34 +01:00
Eugen Rochko
b9b08c5ea7 New translations strings.xml (Hindi) 2024-02-03 07:47:33 +01:00
Eugen Rochko
2b5498ff5d New translations full_description.txt (Hindi) 2024-02-03 06:46:31 +01:00
Eugen Rochko
84b058873d New translations strings.xml (Hindi) 2024-02-03 06:46:30 +01:00
Eugen Rochko
fcf5c0822e New translations strings.xml (Hindi) 2024-02-03 05:07:26 +01:00
Eugen Rochko
53c3da6a3d New translations strings.xml (Hindi) 2024-02-02 18:19:31 +01:00
Eugen Rochko
68371c9a0f New translations strings.xml (Hindi) 2024-02-02 16:58:56 +01:00
Grishka
e7295aac07 Fix #770 2024-02-02 15:34:06 +01:00
Eugen Rochko
ae7f65954a New translations strings.xml (Hindi) 2024-02-02 15:30:16 +01:00
Grishka
350a73c3eb Fix ripple color on Android 14
closes #767
2024-02-02 15:23:52 +01:00
EndermanCo
7581a6cf7e Translated using Weblate (Persian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2024-01-28 00:30:37 +00:00
LucasGGamerM
85b135fa34 docs: add 104 changelog 2024-01-27 10:28:56 -03:00
LucasGGamerM
a195aa56ca build: bump version number 2024-01-27 10:24:28 -03:00
butterflyoffire
b694329bda Translated using Weblate (Arabic (Algeria))
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ar_DZ/
2024-01-27 13:21:36 +00:00
CDN18
79362b59c4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (36 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/zh_Hans/
2024-01-27 13:21:36 +00:00
CDN18
fa51b6acb2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (36 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/zh_Hans/
2024-01-27 13:21:36 +00:00
CDN18
0aa277ee72 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/zh_Hans/
2024-01-27 13:21:36 +00:00
Linerly
d111be7293 Translated using Weblate (Indonesian)
Currently translated at 100.0% (36 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/id/
2024-01-27 13:21:36 +00:00
Linerly
dd06292b53 Translated using Weblate (Indonesian)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/id/
2024-01-27 13:21:36 +00:00
SomeTr
98953e8afa Translated using Weblate (Ukrainian)
Currently translated at 100.0% (36 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/uk/
2024-01-27 13:21:36 +00:00
legiz
8a8cefe68a Translated using Weblate (Russian)
Currently translated at 22.2% (8 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/ru/
2024-01-27 13:21:36 +00:00
legiz
bbd72e299b Translated using Weblate (Russian)
Currently translated at 96.9% (96 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ru/
2024-01-27 13:21:36 +00:00
gallegonovato
24b227f665 Translated using Weblate (Spanish)
Currently translated at 100.0% (36 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-01-27 13:21:36 +00:00
gallegonovato
71118b1f84 Translated using Weblate (Spanish)
Currently translated at 97.2% (35 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/es/
2024-01-27 13:21:36 +00:00
rffontenelle
095e1543fe Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/pt_BR/
2024-01-27 13:21:36 +00:00
alextecplayz
7bdd9811da Translated using Weblate (Romanian)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/ro/
2024-01-27 13:21:36 +00:00
dontobi
c086900ae6 Translated using Weblate (German)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/de/
2024-01-27 13:21:36 +00:00
dontobi
143df89855 Translated using Weblate (German)
Currently translated at 100.0% (36 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/de/
2024-01-27 13:21:36 +00:00
RTRedreovic
b0da6897a0 Translated using Weblate (Esperanto)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/eo/
2024-01-27 13:21:36 +00:00
SomeTr
b798bbf234 Translated using Weblate (Ukrainian)
Currently translated at 97.2% (35 of 36 strings)

Translation: Moshidon/metadata
Translate-URL: https://translate.codeberg.org/projects/moshidon/metadata/uk/
2024-01-27 13:21:36 +00:00
SomeTr
93ba515a29 Translated using Weblate (Ukrainian)
Currently translated at 83.8% (83 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/uk/
2024-01-27 13:21:36 +00:00
ghose
df5bfa77c6 Translated using Weblate (Galician)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/gl/
2024-01-27 13:21:36 +00:00
gallegonovato
797293fc4b Translated using Weblate (Spanish)
Currently translated at 100.0% (99 of 99 strings)

Translation: Moshidon/values
Translate-URL: https://translate.codeberg.org/projects/moshidon/values/es/
2024-01-27 13:21:36 +00:00
Eugen Rochko
66d8ba9b5d New translations strings.xml (Hungarian) 2024-01-26 21:49:50 +01:00
Eugen Rochko
f944b12f45 New translations strings.xml (Hungarian) 2024-01-26 20:20:12 +01:00
Eugen Rochko
61928a1cf0 New translations strings.xml (Greek) 2024-01-26 16:15:35 +01:00
Eugen Rochko
f06196802e New translations strings.xml (Greek) 2024-01-26 11:34:11 +01:00
Grishka
e162833ad7 Update fastlane screenshots (closes #633) 2024-01-26 13:28:43 +03:00
Eugen Rochko
936ffdc793 New translations strings.xml (Swedish) 2024-01-25 21:43:29 +01:00
Eugen Rochko
0bbf6abc0c New translations strings.xml (Russian) 2024-01-25 11:03:38 +01:00
Gregory K
5552dc2ac6 Merge pull request #769 from jixiaoyong/master
fix: NullPointerException crash while change post language twice
2024-01-25 11:14:52 +03:00
JI,XIAOYONG
a65d6fbeb3 fix: NullPointerException crash while change post language twice
Closes https://github.com/mastodon/mastodon-android/issues/766
2024-01-25 15:54:15 +08:00
Eugen Rochko
43612ffbc1 New translations strings.xml (Polish) 2024-01-24 20:55:45 +01:00
Eugen Rochko
971881bbd3 New translations strings.xml (Slovenian) 2024-01-21 13:00:18 +01:00
Eugen Rochko
390cc6b65d New translations strings.xml (Slovenian) 2024-01-21 12:01:14 +01:00
Eugen Rochko
ee31288769 New translations strings.xml (Thai) 2024-01-20 22:09:59 +01:00
Eugen Rochko
401986af29 New translations strings.xml (Galician) 2024-01-19 15:25:19 +01:00
Eugen Rochko
e41e89c5cd New translations strings.xml (Hungarian) 2024-01-19 15:25:18 +01:00
Eugen Rochko
53de0cfc63 New translations strings.xml (Polish) 2024-01-19 14:07:59 +01:00
Eugen Rochko
e68481395f New translations strings.xml (Polish) 2024-01-19 13:07:50 +01:00
butterflyoffire
2c86356389 Translated using Weblate (Arabic)
Currently translated at 78.0% (327 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2024-01-17 17:56:32 +00:00
Eugen Rochko
9a361e0688 New translations strings.xml (Czech) 2024-01-16 15:39:51 +01:00
Eugen Rochko
b8cce74824 New translations strings.xml (Portuguese) 2024-01-16 14:39:36 +01:00
Eugen Rochko
f1ad6fc511 New translations strings.xml (Hungarian) 2024-01-16 14:39:35 +01:00
Eugen Rochko
2aa4cc1a88 New translations strings.xml (Czech) 2024-01-16 14:39:34 +01:00
Eugen Rochko
fb17ba4777 New translations strings.xml (Portuguese, Brazilian) 2024-01-16 12:35:13 +01:00
Eugen Rochko
6e3c464c97 New translations strings.xml (Portuguese) 2024-01-16 12:35:12 +01:00
Eugen Rochko
640e5163a8 New translations strings.xml (Portuguese, Brazilian) 2024-01-16 10:41:19 +01:00
Eugen Rochko
fdd3f2f398 New translations strings.xml (Italian) 2024-01-16 00:28:25 +01:00
Eugen Rochko
dfc55a13b8 New translations strings.xml (Italian) 2024-01-15 23:26:28 +01:00
Eugen Rochko
5a83b79ac2 New translations strings.xml (Polish) 2024-01-15 01:45:23 +01:00
Eugen Rochko
7d954ab3c2 New translations strings.xml (Polish) 2024-01-15 00:43:45 +01:00
Eugen Rochko
ec0b830f4f New translations strings.xml (Polish) 2024-01-14 23:43:15 +01:00
Eugen Rochko
26256b67d3 New translations strings.xml (Polish) 2024-01-14 20:48:10 +01:00
Eugen Rochko
2f9d60b9c0 New translations strings.xml (Polish) 2024-01-14 19:51:03 +01:00
Eugen Rochko
499a325bc8 New translations strings.xml (Polish) 2024-01-14 18:07:35 +01:00
LucasGGamerM
a1a4c59b83 fix(polls): maybe fix on vote poll crash issues 2024-01-12 20:51:59 -03:00
Eugen Rochko
97ca2634a0 New translations strings.xml (Lithuanian) 2024-01-12 16:34:20 +01:00
Eugen Rochko
6630f0f8da New translations strings.xml (Lithuanian) 2024-01-12 14:51:16 +01:00
Eugen Rochko
129b253176 New translations strings.xml (Armenian) 2024-01-11 20:43:52 +01:00
Eugen Rochko
c2382d065e New translations strings.xml (Armenian) 2024-01-11 19:46:15 +01:00
LucasGGamerM
38f74c96bf feat(metadata): use @luis142's in store listing images! 2024-01-10 21:20:36 -03:00
Eugen Rochko
085264755a New translations strings.xml (Chinese Traditional) 2024-01-08 14:54:56 +01:00
Eugen Rochko
baac955e52 New translations strings.xml (Chinese Traditional) 2024-01-08 13:30:18 +01:00
Eugen Rochko
4a0501209a New translations strings.xml (Turkish) 2024-01-07 21:08:49 +01:00
Eugen Rochko
e471b36d39 New translations strings.xml (Japanese) 2024-01-07 09:53:25 +01:00
Eugen Rochko
5c2961cf7c New translations strings.xml (Japanese) 2024-01-07 08:56:52 +01:00
Eugen Rochko
6e980f17c6 New translations strings.xml (Vietnamese) 2024-01-07 06:43:58 +01:00
Eugen Rochko
2860ce8755 New translations strings.xml (Vietnamese) 2024-01-07 05:25:19 +01:00
Eugen Rochko
ca25a868a0 New translations strings.xml (Thai) 2024-01-04 21:01:23 +01:00
Eugen Rochko
74f3bd5905 New translations strings.xml (Thai) 2024-01-04 19:45:26 +01:00
Eugen Rochko
e0a53b4296 New translations strings.xml (Slovenian) 2024-01-04 14:57:53 +01:00
Eugen Rochko
c20f043f38 New translations strings.xml (Icelandic) 2024-01-04 12:50:19 +01:00
Eugen Rochko
daf3005178 New translations strings.xml (Dutch) 2024-01-04 11:32:03 +01:00
Eugen Rochko
d17e24faae New translations strings.xml (Scottish Gaelic) 2024-01-03 22:18:18 +01:00
Eugen Rochko
0cd17accf9 New translations strings.xml (Thai) 2024-01-03 22:18:13 +01:00
Eugen Rochko
65f7b97e60 New translations strings.xml (Persian) 2024-01-03 22:18:11 +01:00
Eugen Rochko
c7324285f3 New translations strings.xml (Indonesian) 2024-01-03 22:18:10 +01:00
Eugen Rochko
6bc795ebea New translations strings.xml (Portuguese, Brazilian) 2024-01-03 22:18:09 +01:00
Eugen Rochko
f2616cdd58 New translations strings.xml (Galician) 2024-01-03 22:18:08 +01:00
Eugen Rochko
d50f65ffd8 New translations strings.xml (Vietnamese) 2024-01-03 22:18:07 +01:00
Eugen Rochko
b39b2d0544 New translations strings.xml (Chinese Traditional) 2024-01-03 22:18:06 +01:00
Eugen Rochko
cdaaa91bcc New translations strings.xml (Ukrainian) 2024-01-03 22:18:05 +01:00
Eugen Rochko
109dca0b8a New translations strings.xml (Turkish) 2024-01-03 22:18:03 +01:00
Eugen Rochko
ee87da564b New translations strings.xml (Swedish) 2024-01-03 22:18:02 +01:00
Eugen Rochko
b143559a0f New translations strings.xml (Russian) 2024-01-03 22:18:01 +01:00
Eugen Rochko
9b89727c80 New translations strings.xml (Polish) 2024-01-03 22:17:59 +01:00
Eugen Rochko
68a252c85c New translations strings.xml (Norwegian) 2024-01-03 22:17:58 +01:00
Eugen Rochko
d99cb91e89 New translations strings.xml (Lithuanian) 2024-01-03 22:17:57 +01:00
Eugen Rochko
38879cd2fe New translations strings.xml (Korean) 2024-01-03 22:17:56 +01:00
Eugen Rochko
af4d98a48b New translations strings.xml (Japanese) 2024-01-03 22:17:54 +01:00
Eugen Rochko
39bb93d650 New translations strings.xml (Italian) 2024-01-03 22:17:53 +01:00
Eugen Rochko
0a3568f424 New translations strings.xml (Hungarian) 2024-01-03 22:17:52 +01:00
Eugen Rochko
e0b45720f0 New translations strings.xml (Finnish) 2024-01-03 22:17:50 +01:00
Eugen Rochko
f5b7024bb5 New translations strings.xml (Basque) 2024-01-03 22:17:49 +01:00
Eugen Rochko
f1bfa1f598 New translations strings.xml (Greek) 2024-01-03 22:17:48 +01:00
Eugen Rochko
653304f9a4 New translations strings.xml (German) 2024-01-03 22:17:47 +01:00
Eugen Rochko
3d416a038a New translations strings.xml (Danish) 2024-01-03 22:17:46 +01:00
Eugen Rochko
e0eeb87182 New translations strings.xml (Czech) 2024-01-03 22:17:45 +01:00
Eugen Rochko
2570a86da9 New translations strings.xml (Belarusian) 2024-01-03 22:17:43 +01:00
Eugen Rochko
7b110f16b3 New translations strings.xml (Arabic) 2024-01-03 22:17:42 +01:00
Eugen Rochko
d170e87325 New translations strings.xml (Spanish) 2024-01-03 22:17:41 +01:00
Eugen Rochko
4a60f0c576 New translations strings.xml (Icelandic) 2024-01-03 22:17:39 +01:00
Eugen Rochko
4b5e9d604c New translations strings.xml (French) 2024-01-03 22:17:38 +01:00
Eugen Rochko
f9562d5087 New translations strings.xml (Chinese Simplified) 2024-01-03 22:17:37 +01:00
Eugen Rochko
786091c0a4 New translations strings.xml (Armenian) 2024-01-03 22:17:36 +01:00
Eugen Rochko
436b8240ef New translations strings.xml (Slovenian) 2024-01-03 22:17:35 +01:00
Eugen Rochko
e7253dcf97 New translations strings.xml (Dutch) 2024-01-03 22:17:34 +01:00
mdwalters
6815cd77e4 Translated using Weblate (Esperanto)
Currently translated at 0.4% (2 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/eo/
2024-01-03 20:58:07 +00:00
ptrwrbl
4f9a1db26b Translated using Weblate (Polish)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pl/
2024-01-03 20:58:07 +00:00
ptrwrbl
d3bcf9d8ee Translated using Weblate (Polish)
Currently translated at 99.7% (418 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2024-01-03 20:58:07 +00:00
Grishka
48f9aabaf7 Support for invite links (AND-90) 2024-01-03 23:51:35 +03:00
Eugen Rochko
14d353ae27 New translations strings.xml (Icelandic) 2024-01-03 14:57:33 +01:00
Eugen Rochko
9a82846b84 New translations strings.xml (Icelandic) 2024-01-02 15:54:39 +01:00
Eugen Rochko
a4c9bbadc4 New translations strings.xml (Icelandic) 2024-01-02 14:01:52 +01:00
CDN18
35d39b63e2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2023-12-30 17:56:33 +00:00
CDN18
15c77e4220 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-12-30 17:56:33 +00:00
Eugen Rochko
fa70c55084 New translations strings.xml (French) 2023-12-30 16:27:55 +01:00
Jacocococo
962c094f7e Properly hide content warning in quoted post 2023-12-29 23:34:40 +01:00
Jacocococo
c6081fb4d4 Let quoted posts appear in notifications 2023-12-29 23:27:14 +01:00
Jacocococo
1832de3aab Fix issues with expandable quoted statuses 2023-12-29 23:25:06 +01:00
Eugen Rochko
8d0a89fb06 New translations strings.xml (Chinese Simplified) 2023-12-29 18:47:34 +01:00
Eugen Rochko
3caf6cb94c New translations strings.xml (Chinese Simplified) 2023-12-29 17:46:24 +01:00
Linerly
5c15914bab Translated using Weblate (Indonesian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-12-28 07:30:51 +00:00
LucasGGamerM
9f29d72212 chore: change order of toolbar icons in photoViewer 2023-12-27 21:09:29 -03:00
Eugen Rochko
f4854061ea New translations strings.xml (Armenian) 2023-12-27 21:54:36 +01:00
Eugen Rochko
bf7607674e New translations strings.xml (Armenian) 2023-12-27 20:06:13 +01:00
LucasGGamerM
9786e324b7 style: fix typo in PhotoViewer error logs 2023-12-27 10:34:59 -03:00
LucasGGamerM
a7fef67d48 fix: swap saveViaDownloadManager to shareAfterDownloading in failed sharing action
I forgot to change the functions when I wrote it
2023-12-27 10:12:32 -03:00
LucasGGamerM
30726cb364 feat: allow sharing attachments from PhotoViewer
Addresses #290
2023-12-27 09:59:13 -03:00
LucasGGamerM
e0ff1f6725 fix(#307): use longs instead of ints in Pleroma/Akkoma instance poll configs
Who the hell had the idea to use longs for this >:C
2023-12-26 18:05:48 -03:00
SomeTr
7e244d65bf Translated using Weblate (Ukrainian)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-12-26 12:56:33 +00:00
SomeTr
9c8e6647bc Translated using Weblate (Ukrainian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-12-26 12:56:32 +00:00
LucasGGamerM
d8cc578537 fix(profile): take away favorites from the topbar icons list
This fixes a thing where the menu entry would just be weird
2023-12-24 23:20:48 -03:00
LucasGGamerM
f9f863ea5e fix(profile-note): add noteEdit must not be empty before hiding the note button 2023-12-23 15:37:23 -03:00
LucasGGamerM
2efb79f6cb fix(profile-note): hide keyboard when edit note box goes away 2023-12-23 10:10:46 -03:00
LucasGGamerM
d534557915 fix(profile): throw share button back into 3-dot menu 2023-12-23 10:03:06 -03:00
LucasGGamerM
68ebc1aa93 feat: swap note button icon 2023-12-23 10:01:35 -03:00
LucasGGamerM
251d90e0ec Merge pull request #321 from FineFindus/fix/profile-appbar-menu
feat: move icons to profile appbar
2023-12-23 09:46:13 -03:00
FineFindus
ccd313533b feat(profile): add note delete icon 2023-12-23 11:54:06 +01:00
FineFindus
f70ea97d9e feat(profile): always show add note and share 2023-12-23 11:28:54 +01:00
FineFindus
3c9efdbbf2 feat(profile): save note onHidden 2023-12-23 11:27:07 +01:00
FineFindus
3bd67f9ab1 feat(profile): move favorites to overflow menu 2023-12-23 11:25:36 +01:00
FineFindus
42946eca44 Revert "feat: make profile note box be visible at all times"
This reverts commit d6c05f0850.
2023-12-23 11:23:51 +01:00
LucasGGamerM
b2d502ae79 feat: add open in browser button to instance about fragment 2023-12-22 10:09:28 -03:00
LucasGGamerM
4d9f625ff4 refactor(statusDisplayItem): move status hidden filter check to start of method to allow an early return
This optimises things by not computing the list of stuff when the status is simply not going to be rendered
2023-12-21 20:01:52 -03:00
LucasGGamerM
18e3fadb26 refactor(profile-note): remove save button, automatically save on focus change/exit 2023-12-21 19:42:29 -03:00
LucasGGamerM
0364d95300 feat: make profile note box be consistent with the other text input boxes in the app 2023-12-21 19:34:47 -03:00
LucasGGamerM
d6c05f0850 feat: make profile note box be visible at all times 2023-12-21 18:38:59 -03:00
Eugen Rochko
137a8ca27b New translations strings.xml (Slovenian) 2023-12-21 22:08:11 +01:00
Eugen Rochko
b9ed4e0ee2 New translations strings.xml (Dutch) 2023-12-21 15:36:39 +01:00
Eugen Rochko
bc04672d32 New translations strings.xml (Dutch) 2023-12-21 12:26:19 +01:00
Eugen Rochko
70c668ecf1 New translations strings.xml (Polish) 2023-12-17 22:13:43 +01:00
LucasGGamerM
5cc94fa2b0 fix: hopefully this addresses a crash when opening an unsent post with an audio file (again)
Crash fix hopefully
2023-12-17 17:20:36 -03:00
Eugen Rochko
64bbe2c438 New translations strings.xml (Polish) 2023-12-17 21:16:34 +01:00
LucasGGamerM
08d4c135ea fix: hopefully this addresses a crash when opening an unsent post with an audio file
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
2023-12-17 17:04:22 -03:00
LucasGGamerM
6220a20734 fix: crash when publishing a scheduled post with the relocated publish button and an error occurs
Goddamnit, at least its fixed
2023-12-17 16:48:30 -03:00
LucasGGamerM
f1c55aa5e8 fix: crash that sometimes happens when longclicking emoji in compose
I have no idea how it happens, nor why. But I am sure this will fix it
2023-12-17 13:23:09 -03:00
Eugen Rochko
32209e766e New translations strings.xml (Greek) 2023-12-17 11:29:11 +01:00
Eugen Rochko
99349cff0a New translations strings.xml (Persian) 2023-12-17 09:35:27 +01:00
Eugen Rochko
74ca1961e0 New translations strings.xml (Spanish) 2023-12-17 03:38:08 +01:00
Eugen Rochko
ef6ba7fe0c New translations strings.xml (Spanish) 2023-12-17 02:07:50 +01:00
Eugen Rochko
9ace2b71cc New translations strings.xml (Spanish) 2023-12-16 22:08:55 +01:00
Eugen Rochko
0c54654b8b New translations strings.xml (Basque) 2023-12-16 20:25:55 +01:00
Eugen Rochko
bf686309fb New translations strings.xml (Basque) 2023-12-16 18:48:35 +01:00
Eugen Rochko
ce4f46537b New translations strings.xml (Spanish) 2023-12-16 17:44:05 +01:00
Eugen Rochko
4c43207f17 New translations strings.xml (Spanish) 2023-12-16 16:46:39 +01:00
LucasGGamerM
b8fe1fd640 build: not compile appkit unnecessarily on nightly builds 2023-12-15 18:21:28 -03:00
LucasGGamerM
928506b360 build: comment out release signing config because nightly refuses to build with it
Wtf is wrong with this
2023-12-15 18:17:21 -03:00
LucasGGamerM
167a2e1e2f build: add release signing config for fdroidRelease 2023-12-15 13:04:06 -03:00
Eugen Rochko
afe5bcd1f3 New translations strings.xml (Portuguese, Brazilian) 2023-12-15 17:01:27 +01:00
Eugen Rochko
3bda81bd43 New translations strings.xml (Portuguese, Brazilian) 2023-12-15 16:02:20 +01:00
Eugen Rochko
7339b2325f New translations strings.xml (Armenian) 2023-12-15 12:30:11 +01:00
Eugen Rochko
ee84a9ee7e New translations strings.xml (Armenian) 2023-12-15 11:09:33 +01:00
Eugen Rochko
fef594150a New translations strings.xml (Turkish) 2023-12-13 17:09:47 +01:00
Eugen Rochko
10371f69cb New translations strings.xml (Turkish) 2023-12-13 15:52:37 +01:00
Eugen Rochko
75cf3d76fb New translations strings.xml (Dutch) 2023-12-12 16:01:19 +01:00
Eugen Rochko
51a7d00c47 New translations strings.xml (Lithuanian) 2023-12-11 15:13:33 +01:00
Eugen Rochko
9ac8261cc4 New translations strings.xml (Lithuanian) 2023-12-11 10:21:28 +01:00
Eugen Rochko
1f4ad80b7d New translations strings.xml (Lithuanian) 2023-12-11 07:10:38 +01:00
Eugen Rochko
4b090f0d68 New translations strings.xml (Lithuanian) 2023-12-10 20:30:16 +01:00
Eugen Rochko
4002bcde26 New translations strings.xml (Lithuanian) 2023-12-10 10:29:09 +01:00
Eugen Rochko
ded3777b40 New translations strings.xml (Lithuanian) 2023-12-10 09:10:25 +01:00
Eugen Rochko
7236066003 New translations strings.xml (Vietnamese) 2023-12-10 03:30:15 +01:00
Eugen Rochko
033f07ea09 New translations strings.xml (Lithuanian) 2023-12-09 22:16:10 +01:00
Eugen Rochko
283c0cba4b New translations strings.xml (Lithuanian) 2023-12-09 21:05:47 +01:00
Eugen Rochko
e3a1fc2fbb New translations strings.xml (Lithuanian) 2023-12-09 19:50:11 +01:00
Eugen Rochko
95de9e2917 New translations strings.xml (Lithuanian) 2023-12-09 16:56:00 +01:00
Eugen Rochko
a82ebeed11 New translations strings.xml (Basque) 2023-12-09 15:50:00 +01:00
Eugen Rochko
3a3aa0be1c New translations strings.xml (Lithuanian) 2023-12-09 07:31:00 +01:00
Eugen Rochko
e72491c2d1 New translations strings.xml (Lithuanian) 2023-12-08 19:29:39 +01:00
Eugen Rochko
36dede1f93 New translations strings.xml (Lithuanian) 2023-12-08 16:59:20 +01:00
Eugen Rochko
ed15daf9e9 New translations strings.xml (Lithuanian) 2023-12-08 15:26:26 +01:00
Eugen Rochko
c6052c841d New translations strings.xml (Lithuanian) 2023-12-08 14:07:22 +01:00
Eugen Rochko
ce39c7ca8f New translations strings.xml (Lithuanian) 2023-12-08 10:16:11 +01:00
Eugen Rochko
b7723dcb98 New translations strings.xml (Lithuanian) 2023-12-08 08:50:52 +01:00
Eugen Rochko
ad0774f8a5 New translations strings.xml (Armenian) 2023-12-07 22:59:36 +01:00
Eugen Rochko
9172feb72b New translations strings.xml (Italian) 2023-12-07 21:21:39 +01:00
Eugen Rochko
a297bd3281 New translations strings.xml (Lithuanian) 2023-12-07 16:52:37 +01:00
Eugen Rochko
e713a9cfc3 New translations strings.xml (Lithuanian) 2023-12-07 15:46:22 +01:00
Eugen Rochko
195395a22d New translations strings.xml (Lithuanian) 2023-12-07 14:14:31 +01:00
Eugen Rochko
7b6a62b047 New translations strings.xml (Lithuanian) 2023-12-07 13:14:37 +01:00
Eugen Rochko
ada1c9ff6d New translations strings.xml (Lithuanian) 2023-12-07 12:05:29 +01:00
Eugen Rochko
5a0a14ed56 New translations strings.xml (Lithuanian) 2023-12-07 09:11:41 +01:00
Eugen Rochko
cad3879646 New translations strings.xml (Galician) 2023-12-07 07:52:19 +01:00
Eugen Rochko
5d961991d4 New translations strings.xml (Lithuanian) 2023-12-06 22:10:19 +01:00
Eugen Rochko
e27536743f New translations strings.xml (Czech) 2023-12-06 22:10:18 +01:00
Eugen Rochko
9f8d4a0f34 New translations strings.xml (Lithuanian) 2023-12-06 20:56:46 +01:00
Eugen Rochko
67b6a89fd9 New translations strings.xml (Swedish) 2023-12-06 20:56:45 +01:00
Eugen Rochko
dabc4058ba New translations strings.xml (Lithuanian) 2023-12-06 18:59:35 +01:00
Eugen Rochko
6c468602c6 New translations full_description.txt (Lithuanian) 2023-12-06 17:20:30 +01:00
Eugen Rochko
9c5d29a860 New translations strings.xml (Lithuanian) 2023-12-06 17:20:29 +01:00
Eugen Rochko
da5e2a6b50 New translations short_description.txt (Lithuanian) 2023-12-06 16:23:00 +01:00
Eugen Rochko
a194569fd4 New translations full_description.txt (Lithuanian) 2023-12-06 16:22:58 +01:00
Eugen Rochko
78a4ace9b2 New translations strings.xml (Lithuanian) 2023-12-06 14:26:32 +01:00
Eugen Rochko
9a664088cd New translations strings.xml (Lithuanian) 2023-12-06 13:02:34 +01:00
Eugen Rochko
1d2e6f880b New translations strings.xml (Lithuanian) 2023-12-06 11:26:57 +01:00
Eugen Rochko
2cd2918d53 New translations strings.xml (Japanese) 2023-12-06 11:26:56 +01:00
Eugen Rochko
9b49db6677 New translations strings.xml (French) 2023-12-06 08:58:00 +01:00
Eugen Rochko
9f6c61e5c0 New translations title.txt (Lithuanian) 2023-12-05 21:30:20 +01:00
Eugen Rochko
b6a2bb7881 New translations short_description.txt (Lithuanian) 2023-12-05 21:30:19 +01:00
Eugen Rochko
62262010b9 New translations full_description.txt (Lithuanian) 2023-12-05 21:30:18 +01:00
Eugen Rochko
72fe9a04a6 New translations strings.xml (Lithuanian) 2023-12-05 21:30:17 +01:00
Eugen Rochko
d8cf55ae21 New translations strings.xml (Greek) 2023-12-05 17:58:41 +01:00
qbane
4d128b4408 Translated using Weblate (Chinese (Traditional))
Currently translated at 52.2% (219 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-12-05 14:38:29 +00:00
Eugen Rochko
dfb393b934 New translations strings.xml (Chinese Traditional) 2023-12-05 07:46:21 +01:00
Eugen Rochko
cd27716f6a New translations strings.xml (Russian) 2023-12-05 07:46:20 +01:00
Eugen Rochko
469553b34e New translations strings.xml (Thai) 2023-12-05 06:46:35 +01:00
Grishka
5d7c37262e Info sheet in media viewer (AND-109) 2023-12-04 21:33:25 +03:00
Eugen Rochko
3f3867473f New translations strings.xml (Czech) 2023-12-04 11:21:21 +01:00
Grishka
b08cd1eb4b Crash fixes 2023-12-04 06:22:21 +03:00
qbane
e0098efe32 Translated using Weblate (Chinese (Traditional))
Currently translated at 51.3% (215 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-12-03 18:38:29 +00:00
Grishka
1f9ff8d341 Increase timeout to 60 seconds 2023-12-03 21:38:00 +03:00
Eugen Rochko
528b362f64 New translations strings.xml (Thai) 2023-12-03 09:24:19 +01:00
Eugen Rochko
1db10c5047 New translations strings.xml (Thai) 2023-12-03 08:16:34 +01:00
Eugen Rochko
f295f5f4e7 New translations strings.xml (Japanese) 2023-12-02 17:44:51 +01:00
Eugen Rochko
08924bd9b0 New translations strings.xml (Russian) 2023-12-02 15:31:03 +01:00
Eugen Rochko
5d432435a1 New translations strings.xml (Russian) 2023-12-02 14:26:43 +01:00
Eugen Rochko
8bd76aa833 New translations strings.xml (Italian) 2023-12-02 00:37:04 +01:00
Eugen Rochko
2147cb87ac New translations strings.xml (Slovenian) 2023-12-01 18:50:27 +01:00
Eugen Rochko
00ed0f5402 New translations strings.xml (Galician) 2023-12-01 16:15:43 +01:00
Eugen Rochko
870f79f6cd New translations strings.xml (Galician) 2023-12-01 14:51:25 +01:00
Eugen Rochko
da879213fc New translations strings.xml (Greek) 2023-11-30 23:34:22 +01:00
Eugen Rochko
db66974bd6 New translations strings.xml (Chinese Traditional) 2023-11-30 02:07:51 +01:00
Eugen Rochko
e3d5ae1d65 New translations strings.xml (Thai) 2023-11-29 20:48:42 +01:00
SomeTr
42f5975f6b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-11-29 17:23:51 +00:00
alextecplayz
1045593cc9 Translated using Weblate (Romanian)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-11-29 17:23:51 +00:00
gallegonovato
3443b80ff7 Translated using Weblate (Spanish)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-11-29 17:23:51 +00:00
poesty
9fe6b3457a Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (418 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-11-29 17:23:50 +00:00
sk22
0a26380f23 Translated using Weblate (German)
Currently translated at 100.0% (419 of 419 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-11-29 17:23:50 +00:00
Eugen Rochko
b06bc5b3b7 New translations strings.xml (Russian) 2023-11-29 11:46:16 +01:00
Eugen Rochko
a4c988012d New translations strings.xml (Georgian) 2023-11-29 09:16:14 +01:00
Grishka
a200701e4c Link card improvements (AND-115)
closes #651
2023-11-29 07:51:11 +03:00
Eugen Rochko
e8f604792c New translations strings.xml (Chinese Traditional) 2023-11-29 03:02:35 +01:00
Eugen Rochko
c8b0666ef9 New translations strings.xml (Scottish Gaelic) 2023-11-29 00:35:44 +01:00
Eugen Rochko
13aa72b150 New translations strings.xml (Thai) 2023-11-29 00:35:39 +01:00
Eugen Rochko
6694074b18 New translations strings.xml (Persian) 2023-11-29 00:35:37 +01:00
Eugen Rochko
63aa32c636 New translations strings.xml (Indonesian) 2023-11-29 00:35:36 +01:00
Eugen Rochko
5fbab870c3 New translations strings.xml (Portuguese, Brazilian) 2023-11-29 00:35:35 +01:00
Eugen Rochko
4a34e248e0 New translations strings.xml (Icelandic) 2023-11-29 00:35:34 +01:00
Eugen Rochko
2c45165e53 New translations strings.xml (Galician) 2023-11-29 00:35:33 +01:00
Eugen Rochko
3f029ac45b New translations strings.xml (Chinese Traditional) 2023-11-29 00:35:32 +01:00
Eugen Rochko
a4cf76d5ba New translations strings.xml (Chinese Simplified) 2023-11-29 00:35:31 +01:00
Eugen Rochko
3044000cf8 New translations strings.xml (Ukrainian) 2023-11-29 00:35:30 +01:00
Eugen Rochko
ab1ef5cfd8 New translations strings.xml (Turkish) 2023-11-29 00:35:29 +01:00
Eugen Rochko
16b91a283a New translations strings.xml (Swedish) 2023-11-29 00:35:28 +01:00
Eugen Rochko
e9fbdc21fa New translations strings.xml (Slovenian) 2023-11-29 00:35:27 +01:00
Eugen Rochko
b429e662aa New translations strings.xml (Russian) 2023-11-29 00:35:26 +01:00
Eugen Rochko
834ad1736e New translations strings.xml (Polish) 2023-11-29 00:35:24 +01:00
Eugen Rochko
91021699d2 New translations strings.xml (Norwegian) 2023-11-29 00:35:23 +01:00
Eugen Rochko
0f86aa12ab New translations strings.xml (Japanese) 2023-11-29 00:35:21 +01:00
Eugen Rochko
fb7bf6f308 New translations strings.xml (Italian) 2023-11-29 00:35:20 +01:00
Eugen Rochko
5aa67aaa78 New translations strings.xml (Hungarian) 2023-11-29 00:35:19 +01:00
Eugen Rochko
2e892e7305 New translations strings.xml (Finnish) 2023-11-29 00:35:16 +01:00
Eugen Rochko
6486a1689f New translations strings.xml (Basque) 2023-11-29 00:35:15 +01:00
Eugen Rochko
5966535111 New translations strings.xml (German) 2023-11-29 00:35:14 +01:00
Eugen Rochko
a2cf4bda99 New translations strings.xml (Danish) 2023-11-29 00:35:13 +01:00
Eugen Rochko
7a93c8615d New translations strings.xml (Arabic) 2023-11-29 00:35:12 +01:00
Eugen Rochko
cf29f11cea New translations strings.xml (Spanish) 2023-11-29 00:35:10 +01:00
Eugen Rochko
23188a26d7 New translations strings.xml (Belarusian) 2023-11-29 00:35:08 +01:00
Eugen Rochko
0480dc0140 New translations strings.xml (French) 2023-11-29 00:35:07 +01:00
Eugen Rochko
cb14b29c78 New translations strings.xml (Armenian) 2023-11-29 00:35:06 +01:00
Eugen Rochko
bf68272de3 New translations strings.xml (Greek) 2023-11-29 00:35:05 +01:00
Eugen Rochko
730f5f8cc9 New translations strings.xml (Dutch) 2023-11-29 00:35:03 +01:00
Eugen Rochko
4b6d328e3d New translations strings.xml (Czech) 2023-11-29 00:35:02 +01:00
Eugen Rochko
cfde38be2d New translations strings.xml (Vietnamese) 2023-11-29 00:35:01 +01:00
Grishka
a2ea8e76fb Improve follow recommendations screen (AND-101) 2023-11-29 02:09:59 +03:00
Grishka
e797d8a1c2 Revert "Update icon"
This reverts commit b58c157c87.
2023-11-29 01:26:51 +03:00
Grishka
b58c157c87 Update icon 2023-11-29 01:22:27 +03:00
Eugen Rochko
58f746a285 New translations strings.xml (Georgian) 2023-11-28 10:52:56 +01:00
Eugen Rochko
a6bba42a49 New translations title.txt (Georgian) 2023-11-28 07:16:50 +01:00
Eugen Rochko
519d6868b2 New translations short_description.txt (Georgian) 2023-11-28 07:16:49 +01:00
Eugen Rochko
5322120097 New translations full_description.txt (Georgian) 2023-11-28 07:16:48 +01:00
Eugen Rochko
2c88c86480 New translations strings.xml (Georgian) 2023-11-28 07:16:47 +01:00
Eugen Rochko
55f32fd45b New translations strings.xml (Dutch) 2023-11-28 01:12:21 +01:00
Eugen Rochko
f39f0b03d1 New translations strings.xml (Dutch) 2023-11-27 23:54:03 +01:00
poesty
ef3605c8e3 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (416 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-11-27 22:03:36 +00:00
alextecplayz
3df20c4749 Translated using Weblate (Romanian)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ro/
2023-11-27 22:03:36 +00:00
gallegonovato
c63e87de45 Translated using Weblate (Spanish)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-11-27 22:03:36 +00:00
qbane
1151e41846 Translated using Weblate (Chinese (Traditional))
Currently translated at 51.3% (214 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-11-27 22:03:36 +00:00
alextecplayz
09668d2500 Translated using Weblate (Romanian)
Currently translated at 100.0% (417 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-11-27 22:03:36 +00:00
0que
773a24af2c Translated using Weblate (Russian)
Currently translated at 70.0% (14 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2023-11-27 22:03:36 +00:00
gallegonovato
b1f6409c8d Translated using Weblate (Spanish)
Currently translated at 100.0% (417 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-11-27 22:03:36 +00:00
SomeTr
ee8e535e58 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (20 of 20 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-11-27 22:03:36 +00:00
Arkxv
d128f29bbc Translated using Weblate (Japanese)
Currently translated at 94.2% (393 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ja/
2023-11-27 22:03:36 +00:00
Eugen Rochko
ff2f1a4955 New translations strings.xml (Dutch) 2023-11-27 21:48:52 +01:00
Grishka
b283e216a7 Increase default HTTP timeouts to 30 seconds
fixes #751
2023-11-27 20:30:33 +03:00
Eugen Rochko
4328d568b3 New translations strings.xml (Belarusian) 2023-11-27 15:23:31 +01:00
Eugen Rochko
8edc47703f New translations strings.xml (Dutch) 2023-11-27 14:08:47 +01:00
Eugen Rochko
92ce906163 New translations strings.xml (Dutch) 2023-11-27 05:46:48 +01:00
Eugen Rochko
6e141e360e New translations strings.xml (French) 2023-11-25 22:27:46 +01:00
Grishka
d1aba87e13 Fix #747 2023-11-25 03:38:57 +03:00
Grishka
723853079e Fix "go to account" in search 2023-11-25 03:35:21 +03:00
Eugen Rochko
cd0742c093 New translations strings.xml (Armenian) 2023-11-24 23:26:11 +01:00
Eugen Rochko
52d5de5aec New translations strings.xml (Dutch) 2023-11-24 23:26:10 +01:00
Eugen Rochko
4f8a5ae5db New translations strings.xml (Dutch) 2023-11-24 21:58:52 +01:00
Eugen Rochko
616f2463c7 New translations strings.xml (Dutch) 2023-11-24 20:51:33 +01:00
Eugen Rochko
d3b711a966 New translations strings.xml (Greek) 2023-11-24 19:55:43 +01:00
Eugen Rochko
827fe34709 New translations strings.xml (Dutch) 2023-11-24 19:55:41 +01:00
Eugen Rochko
4b6c0242d5 New translations strings.xml (Dutch) 2023-11-24 18:54:54 +01:00
Eugen Rochko
c3cbc16084 New translations strings.xml (Czech) 2023-11-23 18:37:42 +01:00
Eugen Rochko
36493bfc88 New translations strings.xml (Vietnamese) 2023-11-23 18:37:41 +01:00
Eugen Rochko
66ce93a3ff New translations strings.xml (Vietnamese) 2023-11-23 16:34:34 +01:00
Grishka
957bc76dbb Merge branch 'l10n_master' 2023-11-23 00:27:58 +03:00
Grishka
1f5a28fb33 Add top margin to pre-reply shits 2023-11-23 00:27:24 +03:00
Grishka
045c58ce66 fix color 2023-11-22 18:26:56 +03:00
Grishka
e2dde7239f Render custom emojis in non-mutual pre-reply sheet 2023-11-22 18:06:14 +03:00
Eugen Rochko
512ad93eea New translations strings.xml (Turkish) 2023-11-22 00:00:30 +01:00
Grishka
19759023a4 Bump version 2023-11-21 22:34:51 +03:00
Grishka
714d3399ce Merge branch 'l10n_master' 2023-11-21 22:34:30 +03:00
Grishka
e1850e5282 Validate timezone and locale against what server supports
closes #654, AND-118
2023-11-21 22:31:26 +03:00
Grishka
a05c917b2c Assorted crash fixes 2023-11-21 21:46:41 +03:00
Grishka
8f3a9c265c Fix a rare crash when opening a notification 2023-11-21 21:27:43 +03:00
Grishka
6f1a33b76e Fix #741 2023-11-21 21:17:30 +03:00
Eugen Rochko
8f0451175f New translations strings.xml (Japanese) 2023-11-21 13:38:37 +01:00
Eugen Rochko
37a3a4f1c0 New translations strings.xml (Chinese Simplified) 2023-11-21 05:53:58 +01:00
Eugen Rochko
bd85746726 New translations strings.xml (Chinese Simplified) 2023-11-21 04:58:34 +01:00
Grishka
96265010bf Merge branch 'l10n_master' 2023-11-20 17:40:05 +03:00
Eugen Rochko
4209951ce3 New translations strings.xml (Armenian) 2023-11-19 11:46:47 +01:00
Eugen Rochko
f1cbd95439 New translations strings.xml (Thai) 2023-11-18 10:24:17 +01:00
Eugen Rochko
d63382c6d9 New translations strings.xml (Thai) 2023-11-18 09:18:16 +01:00
Grishka
20697fb334 Show bio instead of fields in non-mutual pre-reply sheet 2023-11-18 10:58:09 +03:00
Eugen Rochko
1090d1ca42 New translations strings.xml (Thai) 2023-11-17 21:37:58 +01:00
Eugen Rochko
bec4acdf51 New translations strings.xml (Japanese) 2023-11-17 17:27:10 +01:00
Eugen Rochko
800b78bfd8 New translations strings.xml (Vietnamese) 2023-11-17 16:15:13 +01:00
Eugen Rochko
52b01b7bbe New translations strings.xml (Slovenian) 2023-11-17 16:15:12 +01:00
Eugen Rochko
8b71764207 New translations strings.xml (Japanese) 2023-11-17 14:56:57 +01:00
Eugen Rochko
a5d7a75f32 New translations strings.xml (Slovenian) 2023-11-17 14:00:00 +01:00
Eugen Rochko
8839bcb7aa New translations strings.xml (Slovenian) 2023-11-17 12:03:35 +01:00
Eugen Rochko
bcaf71760d New translations strings.xml (Chinese Traditional) 2023-11-17 08:51:19 +01:00
Eugen Rochko
1d95204648 New translations strings.xml (Russian) 2023-11-17 07:26:31 +01:00
Eugen Rochko
83bf2a808f New translations strings.xml (Russian) 2023-11-17 06:19:30 +01:00
Eugen Rochko
7ffb0a01c6 New translations strings.xml (Italian) 2023-11-16 22:09:36 +01:00
Eugen Rochko
0710113148 New translations strings.xml (Thai) 2023-11-16 20:31:10 +01:00
Eugen Rochko
7c43e9a1af New translations strings.xml (Norwegian) 2023-11-16 20:31:08 +01:00
Eugen Rochko
f9e768c378 New translations strings.xml (Thai) 2023-11-16 19:20:56 +01:00
Grishka
2063dbd0b0 Copy app version when tapped 2023-11-16 18:15:59 +03:00
Eugen Rochko
057683c72f New translations strings.xml (Kabyle) 2023-11-16 12:47:38 +01:00
Eugen Rochko
4963a0e722 New translations strings.xml (Occitan) 2023-11-16 12:47:36 +01:00
Eugen Rochko
1d66d288bd New translations strings.xml (Scottish Gaelic) 2023-11-16 12:47:35 +01:00
Eugen Rochko
b8f49157c3 New translations strings.xml (Bosnian) 2023-11-16 12:47:33 +01:00
Eugen Rochko
d77a62ef6f New translations strings.xml (Filipino) 2023-11-16 12:47:32 +01:00
Eugen Rochko
c531150483 New translations strings.xml (Burmese) 2023-11-16 12:47:31 +01:00
Eugen Rochko
991f41c531 New translations strings.xml (Croatian) 2023-11-16 12:47:30 +01:00
Eugen Rochko
b5fb7dd2ec New translations strings.xml (Thai) 2023-11-16 12:47:29 +01:00
Eugen Rochko
4fe8532971 New translations strings.xml (Bengali) 2023-11-16 12:47:27 +01:00
Eugen Rochko
4ae1e7d33e New translations strings.xml (Indonesian) 2023-11-16 12:47:26 +01:00
Eugen Rochko
43fa4526a4 New translations strings.xml (Portuguese, Brazilian) 2023-11-16 12:47:25 +01:00
Eugen Rochko
fc4b1da323 New translations strings.xml (Galician) 2023-11-16 12:47:24 +01:00
Eugen Rochko
843755f4e4 New translations strings.xml (Chinese Traditional) 2023-11-16 12:47:23 +01:00
Eugen Rochko
80e02f7520 New translations strings.xml (Chinese Simplified) 2023-11-16 12:47:22 +01:00
Eugen Rochko
af8f52e589 New translations strings.xml (Swedish) 2023-11-16 12:47:21 +01:00
Eugen Rochko
bc3f48dec9 New translations strings.xml (Russian) 2023-11-16 12:47:20 +01:00
Eugen Rochko
74ee832507 New translations strings.xml (Portuguese) 2023-11-16 12:47:18 +01:00
Eugen Rochko
da1b2d09b1 New translations strings.xml (Polish) 2023-11-16 12:47:17 +01:00
Eugen Rochko
99f8607211 New translations strings.xml (Norwegian) 2023-11-16 12:47:16 +01:00
Eugen Rochko
ef293088e1 New translations strings.xml (Dutch) 2023-11-16 12:47:15 +01:00
Eugen Rochko
e08e72ccb0 New translations strings.xml (Korean) 2023-11-16 12:47:14 +01:00
Eugen Rochko
b692440bab New translations strings.xml (Japanese) 2023-11-16 12:47:13 +01:00
Eugen Rochko
7061abc64b New translations strings.xml (Italian) 2023-11-16 12:47:12 +01:00
Eugen Rochko
0dd5064abb New translations strings.xml (Hungarian) 2023-11-16 12:47:11 +01:00
Eugen Rochko
a1aafff6ce New translations strings.xml (Hebrew) 2023-11-16 12:47:09 +01:00
Eugen Rochko
1f88f154af New translations strings.xml (German) 2023-11-16 12:47:08 +01:00
Eugen Rochko
3d1e0364c6 New translations strings.xml (Danish) 2023-11-16 12:47:06 +01:00
Eugen Rochko
0f1b5431bb New translations strings.xml (Czech) 2023-11-16 12:47:05 +01:00
Eugen Rochko
0369d3fa62 New translations strings.xml (Catalan) 2023-11-16 12:47:04 +01:00
Eugen Rochko
154e3a732a New translations strings.xml (Belarusian) 2023-11-16 12:47:03 +01:00
Eugen Rochko
9c979db043 New translations strings.xml (Arabic) 2023-11-16 12:47:02 +01:00
Eugen Rochko
0af089db89 New translations strings.xml (Spanish) 2023-11-16 12:47:00 +01:00
Eugen Rochko
1335613860 New translations strings.xml (Vietnamese) 2023-11-16 12:46:58 +01:00
Eugen Rochko
cb86bfd8dc New translations strings.xml (Persian) 2023-11-16 12:46:57 +01:00
Eugen Rochko
a0d32ae493 New translations strings.xml (Turkish) 2023-11-16 12:46:56 +01:00
Eugen Rochko
f7e56a6c40 New translations strings.xml (Slovenian) 2023-11-16 12:46:55 +01:00
Eugen Rochko
56613c75f7 New translations strings.xml (Armenian) 2023-11-16 12:46:54 +01:00
Eugen Rochko
fb3c35c0a0 New translations strings.xml (Basque) 2023-11-16 12:46:53 +01:00
Eugen Rochko
4b3dc0a59f New translations strings.xml (French) 2023-11-16 12:46:52 +01:00
Eugen Rochko
7855615a7b New translations strings.xml (Icelandic) 2023-11-16 12:46:50 +01:00
Eugen Rochko
ff6576f4da New translations strings.xml (Ukrainian) 2023-11-16 12:46:49 +01:00
Eugen Rochko
931fa9a9b0 New translations strings.xml (Finnish) 2023-11-16 12:46:48 +01:00
Eugen Rochko
77a70967f2 New translations strings.xml (Greek) 2023-11-16 12:46:47 +01:00
Grishka
e5506d952c Onboarding: replace fields with discoverability toggle (AND-100) 2023-11-16 12:15:17 +03:00
Eugen Rochko
2c2dbd0761 New translations strings.xml (Kabyle) 2023-11-16 09:56:39 +01:00
Eugen Rochko
e6f5ecd496 New translations strings.xml (Scottish Gaelic) 2023-11-16 09:56:37 +01:00
Eugen Rochko
73cea2d83c New translations strings.xml (Filipino) 2023-11-16 09:56:34 +01:00
Eugen Rochko
835a576f44 New translations strings.xml (Thai) 2023-11-16 09:56:31 +01:00
Eugen Rochko
0a090341cc New translations strings.xml (Indonesian) 2023-11-16 09:56:29 +01:00
Eugen Rochko
453671abfb New translations strings.xml (Portuguese, Brazilian) 2023-11-16 09:56:27 +01:00
Eugen Rochko
cfa7daa984 New translations strings.xml (Galician) 2023-11-16 09:56:26 +01:00
Eugen Rochko
88f913f586 New translations strings.xml (Chinese Traditional) 2023-11-16 09:56:25 +01:00
Eugen Rochko
5b4aeb4923 New translations strings.xml (Chinese Simplified) 2023-11-16 09:56:24 +01:00
Eugen Rochko
19133a2913 New translations strings.xml (Swedish) 2023-11-16 09:56:23 +01:00
Eugen Rochko
293035b7c8 New translations strings.xml (Russian) 2023-11-16 09:56:22 +01:00
Eugen Rochko
d06723de5c New translations strings.xml (Portuguese) 2023-11-16 09:56:20 +01:00
Eugen Rochko
bc45d0c499 New translations strings.xml (Polish) 2023-11-16 09:56:19 +01:00
Eugen Rochko
c320cccf6f New translations strings.xml (Norwegian) 2023-11-16 09:56:18 +01:00
Eugen Rochko
e3aebbd145 New translations strings.xml (Dutch) 2023-11-16 09:56:17 +01:00
Eugen Rochko
e15d378e46 New translations strings.xml (Korean) 2023-11-16 09:56:16 +01:00
Eugen Rochko
b6720d10fb New translations strings.xml (Japanese) 2023-11-16 09:56:15 +01:00
Eugen Rochko
83a2dbe8a1 New translations strings.xml (Italian) 2023-11-16 09:56:13 +01:00
Eugen Rochko
5b8592a99d New translations strings.xml (Hungarian) 2023-11-16 09:56:12 +01:00
Eugen Rochko
18a094c06c New translations strings.xml (German) 2023-11-16 09:56:10 +01:00
Eugen Rochko
a319ff3dc0 New translations strings.xml (Danish) 2023-11-16 09:56:08 +01:00
Eugen Rochko
0cb46eca1a New translations strings.xml (Czech) 2023-11-16 09:56:07 +01:00
Eugen Rochko
d85c814cba New translations strings.xml (Catalan) 2023-11-16 09:56:06 +01:00
Eugen Rochko
f49e660f29 New translations strings.xml (Belarusian) 2023-11-16 09:56:05 +01:00
Eugen Rochko
afa407e7d1 New translations strings.xml (Arabic) 2023-11-16 09:56:04 +01:00
Eugen Rochko
37e0f5ecea New translations strings.xml (Spanish) 2023-11-16 09:56:02 +01:00
Eugen Rochko
5000fdcfea New translations strings.xml (Vietnamese) 2023-11-16 09:56:00 +01:00
Eugen Rochko
2ec7489dbf New translations strings.xml (Persian) 2023-11-16 09:55:59 +01:00
Eugen Rochko
05965cea6e New translations strings.xml (Turkish) 2023-11-16 09:55:58 +01:00
Eugen Rochko
279e22ccb3 New translations strings.xml (Slovenian) 2023-11-16 09:55:57 +01:00
Eugen Rochko
6a6fc1ca8b New translations strings.xml (Armenian) 2023-11-16 09:55:56 +01:00
Eugen Rochko
b6b5426297 New translations strings.xml (Basque) 2023-11-16 09:55:55 +01:00
Eugen Rochko
e332ddda74 New translations strings.xml (French) 2023-11-16 09:55:54 +01:00
Eugen Rochko
2cd4cfb883 New translations strings.xml (Icelandic) 2023-11-16 09:55:52 +01:00
Eugen Rochko
ef12d09d35 New translations strings.xml (Ukrainian) 2023-11-16 09:55:51 +01:00
Eugen Rochko
1e365a8a7c New translations strings.xml (Finnish) 2023-11-16 09:55:50 +01:00
Eugen Rochko
e9363b41fd New translations strings.xml (Greek) 2023-11-16 09:55:49 +01:00
Grishka
5e99df137a Remove unused resources 2023-11-16 11:44:27 +03:00
Grishka
c0d0b45e24 Replace boost icons 2023-11-16 11:38:18 +03:00
Eugen Rochko
3340b4cdfa New translations strings.xml (Norwegian) 2023-11-15 22:47:52 +01:00
Eugen Rochko
d4afcc3383 New translations strings.xml (Norwegian) 2023-11-15 21:19:47 +01:00
Eugen Rochko
dad423eb04 New translations strings.xml (German) 2023-11-15 20:06:02 +01:00
Eugen Rochko
7b275d7e3d New translations strings.xml (Chinese Traditional) 2023-11-15 19:10:30 +01:00
Eugen Rochko
5274ecb721 New translations strings.xml (Chinese Traditional) 2023-11-15 18:12:43 +01:00
Eugen Rochko
e45367a482 New translations strings.xml (Japanese) 2023-11-15 18:12:42 +01:00
Eugen Rochko
83532edaab New translations strings.xml (Urdu (India)) 2023-11-15 16:43:17 +01:00
Eugen Rochko
793d28da6a New translations strings.xml (Kabyle) 2023-11-15 16:43:16 +01:00
Eugen Rochko
a8e575f680 New translations strings.xml (Igbo) 2023-11-15 16:43:15 +01:00
Eugen Rochko
98b0b3f9dd New translations strings.xml (Occitan) 2023-11-15 16:43:14 +01:00
Eugen Rochko
2e6d9c296a New translations strings.xml (Scottish Gaelic) 2023-11-15 16:43:13 +01:00
Eugen Rochko
a07dc96ef9 New translations strings.xml (Sinhala) 2023-11-15 16:43:12 +01:00
Eugen Rochko
8ba097a68a New translations strings.xml (Bosnian) 2023-11-15 16:43:11 +01:00
Eugen Rochko
b1dd990fea New translations strings.xml (Filipino) 2023-11-15 16:43:09 +01:00
Eugen Rochko
ba7864b910 New translations strings.xml (Burmese) 2023-11-15 16:43:08 +01:00
Eugen Rochko
5d8fa343cd New translations strings.xml (Hindi) 2023-11-15 16:43:07 +01:00
Eugen Rochko
3fc49c431b New translations strings.xml (Croatian) 2023-11-15 16:43:06 +01:00
Eugen Rochko
79b6e65ce3 New translations strings.xml (Thai) 2023-11-15 16:43:05 +01:00
Eugen Rochko
9f457d0d76 New translations strings.xml (Bengali) 2023-11-15 16:43:03 +01:00
Eugen Rochko
aa2ff62db4 New translations strings.xml (Indonesian) 2023-11-15 16:43:02 +01:00
Eugen Rochko
73fffca569 New translations strings.xml (Portuguese, Brazilian) 2023-11-15 16:43:01 +01:00
Eugen Rochko
45589fc033 New translations strings.xml (Galician) 2023-11-15 16:42:59 +01:00
Eugen Rochko
79b81ed932 New translations strings.xml (Chinese Traditional) 2023-11-15 16:42:58 +01:00
Eugen Rochko
d1242870df New translations strings.xml (Chinese Simplified) 2023-11-15 16:42:57 +01:00
Eugen Rochko
e0dbbc4bc0 New translations strings.xml (Swedish) 2023-11-15 16:42:56 +01:00
Eugen Rochko
bf89791817 New translations strings.xml (Russian) 2023-11-15 16:42:55 +01:00
Eugen Rochko
e3197f6dc1 New translations strings.xml (Portuguese) 2023-11-15 16:42:54 +01:00
Eugen Rochko
e6317aa898 New translations strings.xml (Polish) 2023-11-15 16:42:52 +01:00
Eugen Rochko
c73dc326fd New translations strings.xml (Norwegian) 2023-11-15 16:42:51 +01:00
Eugen Rochko
287e250357 New translations strings.xml (Dutch) 2023-11-15 16:42:50 +01:00
Eugen Rochko
9673a14420 New translations strings.xml (Korean) 2023-11-15 16:42:49 +01:00
Eugen Rochko
3333fdc8d7 New translations strings.xml (Japanese) 2023-11-15 16:42:48 +01:00
Eugen Rochko
9fb4b8bb6e New translations strings.xml (Italian) 2023-11-15 16:42:47 +01:00
Eugen Rochko
7b10ed13f4 New translations strings.xml (Hungarian) 2023-11-15 16:42:45 +01:00
Eugen Rochko
c528bd797d New translations strings.xml (Hebrew) 2023-11-15 16:42:44 +01:00
Eugen Rochko
264529705c New translations strings.xml (Irish) 2023-11-15 16:42:43 +01:00
Eugen Rochko
4669e3dfc7 New translations strings.xml (German) 2023-11-15 16:42:42 +01:00
Eugen Rochko
eff3798964 New translations strings.xml (Danish) 2023-11-15 16:42:41 +01:00
Eugen Rochko
78c526c25b New translations strings.xml (Czech) 2023-11-15 16:42:40 +01:00
Eugen Rochko
ec13415d1f New translations strings.xml (Catalan) 2023-11-15 16:42:39 +01:00
Eugen Rochko
96622184ae New translations strings.xml (Belarusian) 2023-11-15 16:42:38 +01:00
Eugen Rochko
3742c1c862 New translations strings.xml (Arabic) 2023-11-15 16:42:37 +01:00
Eugen Rochko
a0c7757428 New translations strings.xml (Spanish) 2023-11-15 16:42:36 +01:00
Eugen Rochko
15f9f4906a New translations strings.xml (Romanian) 2023-11-15 16:42:34 +01:00
Eugen Rochko
d577cd9b21 New translations strings.xml (Vietnamese) 2023-11-15 16:42:33 +01:00
Eugen Rochko
cf610cbb87 New translations strings.xml (Persian) 2023-11-15 16:42:32 +01:00
Eugen Rochko
1e1095204d New translations strings.xml (Turkish) 2023-11-15 16:42:31 +01:00
Eugen Rochko
3fb6a13a3a New translations strings.xml (Slovenian) 2023-11-15 16:42:30 +01:00
Eugen Rochko
2826655fe2 New translations strings.xml (Armenian) 2023-11-15 16:42:29 +01:00
Eugen Rochko
0ccf450b28 New translations strings.xml (Basque) 2023-11-15 16:42:28 +01:00
Eugen Rochko
d28b9460af New translations strings.xml (French) 2023-11-15 16:42:26 +01:00
Eugen Rochko
3e1bdf98c2 New translations strings.xml (Icelandic) 2023-11-15 16:42:25 +01:00
Eugen Rochko
3f87764230 New translations strings.xml (Ukrainian) 2023-11-15 16:42:24 +01:00
Eugen Rochko
25e8e2e9e1 New translations strings.xml (Finnish) 2023-11-15 16:42:23 +01:00
Eugen Rochko
3faf2ce9b9 New translations strings.xml (Greek) 2023-11-15 16:42:22 +01:00
Grishka
cbe243fc9e Open link in browser when a post/account links to itself
closes #739
2023-11-15 18:17:08 +03:00
Grishka
a438f633be Pre-reply sheets 2023-11-15 18:05:38 +03:00
Eugen Rochko
37ef67d7ac New translations strings.xml (Italian) 2023-11-15 13:33:31 +01:00
Eugen Rochko
67d631b0f0 New translations strings.xml (Russian) 2023-11-15 07:59:18 +01:00
Eugen Rochko
fc302ffa5f New translations strings.xml (Belarusian) 2023-11-15 03:27:35 +01:00
Eugen Rochko
8c28556a94 New translations strings.xml (Chinese Traditional) 2023-11-14 20:53:35 +01:00
Grishka
45cc531eec Thread fragment tweaks part 2 2023-11-14 21:27:15 +03:00
Grishka
5c9ad9286d Thread fragment tweaks part 1 2023-11-14 19:23:42 +03:00
Eugen Rochko
ad1c9486d7 New translations short_description.txt (Hungarian) 2023-11-14 15:33:01 +01:00
Eugen Rochko
ad6a03b712 New translations strings.xml (Hungarian) 2023-11-14 15:33:00 +01:00
Eugen Rochko
36bb8010bc New translations strings.xml (Swedish) 2023-11-14 13:43:32 +01:00
Eugen Rochko
2200da7a16 New translations strings.xml (Hungarian) 2023-11-14 13:43:31 +01:00
Grishka
688c0e2e85 Use sp units in more places
#723
2023-11-14 09:54:55 +03:00
Eugen Rochko
714345a65d New translations strings.xml (French) 2023-11-13 19:16:10 +01:00
Gregory K
34a1c7e408 Merge pull request #736 from alex-vit/alexv/fix/delete_account_shared_pref
Delete `id.xml` shared pref from the correct dir
2023-11-13 18:07:24 +03:00
Aleksandrs Vitjukovs
6255221d6a Delete id.xml shared pref from the correct dir 2023-11-13 16:54:10 +02:00
Eugen Rochko
58364de72a New translations strings.xml (Armenian) 2023-11-13 05:28:24 +01:00
Eugen Rochko
6d64df4ee4 New translations strings.xml (Turkish) 2023-11-11 15:16:48 +01:00
Gregory K
7bac2f206b Merge pull request #734 from FineFindus/feat/translate-media-upstream
Allow translation of attachments, spoilers and polls
2023-11-11 08:01:13 +03:00
FineFindus
75e1a17a2c fix: add args in correct order 2023-11-10 22:05:21 +01:00
FineFindus
47b13384a8 feat(status/translation): support translating spoiler 2023-11-10 22:04:07 +01:00
FineFindus
77b9efa7d1 feat(status/translation): support translating spoiler 2023-11-10 22:01:24 +01:00
FineFindus
be5f3b18af feat(status): translate poll options 2023-11-10 22:00:47 +01:00
FineFindus
d5d12a7ce5 fix(status/translation): do not require all fields 2023-11-10 22:00:47 +01:00
Eugen Rochko
d7726d7755 New translations strings.xml (Norwegian) 2023-11-10 21:26:30 +01:00
FineFindus
0cd0d37eff feat(status): translate media attachments 2023-11-10 21:07:24 +01:00
Eugen Rochko
4521def103 New translations strings.xml (Norwegian) 2023-11-10 20:08:28 +01:00
Eugen Rochko
5c70f0a758 New translations strings.xml (Arabic) 2023-11-09 23:12:18 +01:00
Eugen Rochko
c12c2c0416 New translations strings.xml (Arabic) 2023-11-09 22:09:22 +01:00
Eugen Rochko
db45c422e7 New translations strings.xml (Arabic) 2023-11-09 21:07:57 +01:00
Eugen Rochko
affd9a95c5 New translations strings.xml (Hungarian) 2023-11-09 15:25:20 +01:00
Gregory K
7baf25869a Merge pull request #732 from FineFindus/fix/invisible-menu
fix: disable group divider on EMUI
2023-11-08 22:35:22 +03:00
FineFindus
12096fb427 fix: disable group divider on EMUI 2023-11-08 20:25:25 +01:00
Gregory K
ef7136cb81 Merge pull request #731 from FineFindus/fix/edit-history-crash
fix: edit history crash
2023-11-08 22:00:48 +03:00
FineFindus
3c4baf0126 fix(status/edit-history): set fake poll fields 2023-11-08 19:45:49 +01:00
FineFindus
f0b87c62a5 fix(status/edit-history): check for negative array index 2023-11-08 19:36:24 +01:00
Eugen Rochko
a319435e91 New translations strings.xml (Belarusian) 2023-11-08 12:18:27 +00:00
Eugen Rochko
5bd0e988e3 New translations strings.xml (Belarusian) 2023-11-08 11:22:42 +00:00
Eugen Rochko
b2be669b9e New translations strings.xml (Belarusian) 2023-11-08 09:22:28 +00:00
Eugen Rochko
51952b0485 New translations strings.xml (Portuguese, Brazilian) 2023-11-05 10:47:44 +00:00
Eugen Rochko
2b0c5e7fac New translations strings.xml (Basque) 2023-11-04 19:16:42 +00:00
Eugen Rochko
3e6cea1a6a New translations strings.xml (Basque) 2023-11-04 17:29:35 +00:00
Eugen Rochko
1aec7c0999 New translations strings.xml (Russian) 2023-11-03 08:28:42 +00:00
Eugen Rochko
5da98809a5 New translations strings.xml (Russian) 2023-11-03 06:34:07 +00:00
Eugen Rochko
49695614b7 New translations strings.xml (Russian) 2023-11-03 05:25:21 +00:00
Eugen Rochko
3fbbc104b7 New translations strings.xml (Galician) 2023-11-02 08:42:01 +00:00
Eugen Rochko
2fe7c0b85e New translations strings.xml (Slovenian) 2023-11-02 00:28:09 +00:00
Grishka
09d0e82216 Fix video player state after app resumption 2023-11-02 02:21:11 +03:00
Eugen Rochko
d208fcea7d New translations strings.xml (Persian) 2023-11-01 21:16:18 +00:00
Eugen Rochko
cc0674db34 New translations strings.xml (Persian) 2023-11-01 20:07:40 +00:00
Grishka
1d5b84943d Merge branch 'l10n_master' 2023-11-01 17:38:39 +03:00
Eugen Rochko
14fe992ca5 New translations strings.xml (Turkish) 2023-11-01 14:37:37 +00:00
Grishka
15232bddaf Merge branch 'l10n_master' 2023-11-01 17:35:51 +03:00
Grishka
160ef25621 Also try resolving URLs from link cards 2023-11-01 17:32:50 +03:00
Eugen Rochko
2afb8688a3 New translations strings.xml (Vietnamese) 2023-10-31 11:03:23 +00:00
Eugen Rochko
9d1af035ea New translations strings.xml (Slovenian) 2023-10-31 08:55:58 +00:00
Eugen Rochko
fb7574d814 New translations strings.xml (Persian) 2023-10-30 16:47:36 +01:00
Eugen Rochko
201a3cb9e3 New translations strings.xml (Slovenian) 2023-10-30 15:17:03 +01:00
Eugen Rochko
cc735ee6a1 New translations strings.xml (Turkish) 2023-10-30 13:09:02 +01:00
Eugen Rochko
0165e14ea0 New translations strings.xml (Turkish) 2023-10-30 11:23:35 +01:00
Grishka
97d19605d5 Fix link cards 2023-10-29 14:43:10 +03:00
Grishka
bc490218f9 Open profile and post links in-app (AND-114) 2023-10-29 14:18:29 +03:00
Grishka
6dac05a21d Fix pagination in lists 2023-10-29 09:59:47 +03:00
Grishka
fd3fff6322 fix 2023-10-28 21:24:04 +03:00
Grishka
edb64fff2e Add info banner to the new local timeline 2023-10-28 18:56:15 +03:00
Grishka
fe0e854e72 Remove local timeline from search tab 2023-10-28 18:43:54 +03:00
Eugen Rochko
06c85fb203 New translations strings.xml (Slovenian) 2023-10-27 23:25:59 +02:00
Eugen Rochko
69926c4ae1 New translations strings.xml (Slovenian) 2023-10-27 22:19:05 +02:00
Eugen Rochko
ef44b0a412 New translations strings.xml (Slovenian) 2023-10-27 20:59:33 +02:00
Eugen Rochko
8577ac1027 New translations strings.xml (Armenian) 2023-10-27 17:46:27 +02:00
Eugen Rochko
32da050106 New translations strings.xml (Armenian) 2023-10-27 15:59:36 +02:00
Eugen Rochko
526b74b3ef New translations strings.xml (Basque) 2023-10-27 12:15:13 +02:00
Eugen Rochko
97ab328a9c New translations strings.xml (Basque) 2023-10-27 09:50:11 +02:00
Eugen Rochko
21603eedcf New translations strings.xml (Basque) 2023-10-26 16:45:54 +02:00
Eugen Rochko
7fbef273a1 New translations strings.xml (French) 2023-10-26 12:03:07 +02:00
Eugen Rochko
9e19716504 New translations strings.xml (Icelandic) 2023-10-26 10:47:01 +02:00
Eugen Rochko
b473642ab4 New translations strings.xml (Ukrainian) 2023-10-26 00:20:48 +02:00
Gregory K
fba55f01a0 Merge pull request #722 from untitaker/fix-gboard-garbage-alt-text
Remove garbage alt text from images attached via Gboard
2023-10-25 22:56:13 +03:00
Markus Unterwaditzer
015e63ba66 apply review feedback 2023-10-25 21:50:10 +02:00
Markus Unterwaditzer
d92e2407f3 Remove garbage alt text from images attached via Gboard
See also: https://github.com/tuskyapp/Tusky/pull/4068

----

Steps to reproduce:

1. install Gboard
   (https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin)

2. open a direct link to any image in Firefox
3. long-press the image to get a "Copy Image" dialogue (and copy the
   image)
4. compose a new post in Tusky
5. Gboard will suggest to paste the image from clipboard
6. paste image, see that when opening alt text editor, it is filled out
   with this garbage string

Why is this bad? It's not when I just fix the alt text. But it breaks
every mechanism that is supposed to remind me of adding alt text.

It's hard to argue that this is within scope of this app but I
also don't see it getting fixed in Gboard, so here we go.
2023-10-25 14:44:20 +02:00
Eugen Rochko
a4f84fb8cd New translations strings.xml (Finnish) 2023-10-25 12:13:33 +02:00
Eugen Rochko
bfe88745ca New translations strings.xml (Greek) 2023-10-25 12:13:31 +02:00
Grishka
0d334237ba Merge branch 'l10n_master' 2023-10-25 08:15:22 +03:00
Eugen Rochko
fd5cff3fea New translations strings.xml (Italian) 2023-10-24 23:34:04 +02:00
Eugen Rochko
af5b82e9fd New translations strings.xml (Italian) 2023-10-24 22:37:17 +02:00
Eugen Rochko
d3561748c8 New translations strings.xml (Scottish Gaelic) 2023-10-24 14:45:08 +02:00
Eugen Rochko
791a1d804b New translations strings.xml (Japanese) 2023-10-24 03:33:27 +02:00
Eugen Rochko
2442424e3b New translations strings.xml (Chinese Traditional) 2023-10-24 01:10:21 +02:00
Eugen Rochko
0ecedd2820 New translations strings.xml (Thai) 2023-10-23 18:31:33 +02:00
Eugen Rochko
958d62ec0c New translations strings.xml (Persian) 2023-10-23 17:11:47 +02:00
Eugen Rochko
400cfb2141 New translations strings.xml (Galician) 2023-10-23 17:11:44 +02:00
Eugen Rochko
52b860dd8f New translations strings.xml (Vietnamese) 2023-10-23 17:11:43 +02:00
Eugen Rochko
4d57d8d576 New translations strings.xml (Greek) 2023-10-23 17:11:31 +02:00
Eugen Rochko
9a098accd8 New translations strings.xml (German) 2023-10-23 17:11:29 +02:00
Eugen Rochko
62f3b2522c New translations strings.xml (Spanish) 2023-10-23 17:11:24 +02:00
Eugen Rochko
9b48cd2037 New translations strings.xml (Chinese Traditional) 2023-10-23 17:11:21 +02:00
Eugen Rochko
69776d45d1 New translations strings.xml (Swedish) 2023-10-23 17:11:18 +02:00
Eugen Rochko
b8fb2660a4 New translations strings.xml (Japanese) 2023-10-23 17:11:14 +02:00
Eugen Rochko
941281298d New translations strings.xml (Icelandic) 2023-10-23 17:11:13 +02:00
Eugen Rochko
8afc4511a6 New translations strings.xml (Thai) 2023-10-23 17:11:12 +02:00
Grishka
f43ef325ae Following -> Home 2023-10-23 17:12:22 +03:00
Eugen Rochko
bbdc323204 New translations strings.xml (Icelandic) 2023-10-23 15:21:14 +02:00
Grishka
27cbb70352 Overdraw fix 2023-10-22 05:18:10 +03:00
Grishka
f5b10b516c Show all followers when creating a list 2023-10-22 05:14:21 +03:00
Grishka
5580308968 Improve lists caching and consistency 2023-10-22 05:07:28 +03:00
Grishka
572901ec9d minor fixes 2023-10-21 11:52:15 +03:00
Grishka
965239d215 Apply filters on more screens 2023-10-21 11:44:39 +03:00
Grishka
ac1e5e991e Don't auto-refresh notifications if the user scrolled too far 2023-10-21 11:34:55 +03:00
Grishka
66b7b127f9 List creation (AND-98) 2023-10-21 09:13:01 +03:00
Grishka
0ed858b99c Update string 2023-10-18 02:56:13 +03:00
Grishka
9b3e153a4d Add "manage accounts" item to settings 2023-10-18 01:41:10 +03:00
Grishka
e525aef3d9 Improve new posts button animation 2023-10-18 01:37:02 +03:00
Grishka
201b72c9c8 Add "copy link" to post context menu 2023-10-17 04:41:18 +03:00
Grishka
26b99f5f68 New and improved™ "new posts" button (AND-102) 2023-10-17 04:31:07 +03:00
Grishka
d3dc774492 Empty states for home timeline popup submenus (AND-99) 2023-10-17 03:22:23 +03:00
Grishka
1f7155a932 Reorder notification tabs and remember selection (AND-82) 2023-10-17 03:00:05 +03:00
Grishka
02729fe02b Always allow marking notifications as read 2023-10-17 02:47:50 +03:00
Gregory K
f5b98009dd Merge pull request #712 from mastodon/remove-funding
Delete .github/FUNDING.yml
2023-10-13 14:08:47 +03:00
Renaud Chaput
cf0b66d852 Delete .github/FUNDING.yml
We now have an organisation-wide funding configuration, this one is no longer needed
2023-10-13 10:45:53 +02:00
Grishka
84026afb92 A bunch of minor RTL fixes 2023-10-11 00:49:40 +03:00
Grishka
4dea7d2a52 Retain CWs but expand them when showCWs setting is off
#323, closes #87
2023-10-11 00:27:26 +03:00
Grishka
2df1b7dd61 Remove repeating logging code from MastodonAPIController 2023-10-10 23:47:36 +03:00
Grishka
89042113a5 Fix default server loading timeout 2023-10-10 23:43:44 +03:00
Grishka
48665ebcce Scroll search to top on query change 2023-10-10 23:28:09 +03:00
Grishka
103aaafff1 Hide the FAB while editing profile
closes #709
2023-10-08 22:31:27 +03:00
Grishka
dff2217e80 Lists
closes #89, closes #279
2023-10-08 22:03:16 +03:00
672 changed files with 19787 additions and 4130 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,12 +1,11 @@
# These are supported funding model platforms
github: LucasGGamerM
custom: ["https://liberapay.com/LucasGGamerM/donate", liberapay.com]
patreon: # mastodon
open_collective: # Replace with a single Open Collective username e.g., user1
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username e.g., user1
liberapay: LucasGGamerM # Replace with a single Liberapay username e.g., user1
issuehunt: # Replace with a single IssueHunt username e.g., user1
otechie: # Replace with a single Otechie username e.g., user1
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -25,7 +25,7 @@ Does this issue also occur with the respective upstream release?
> No / Yes
> In case it does, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead.
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Moshidon, feel free to still create this issue!
**Screenshots and screen recordings**

View File

@@ -11,27 +11,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Appkit Repo
uses: actions/checkout@v3
with:
repository: grishka/appkit
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
cache: gradle
- name: Comment out signing config in appkits gradle file
run: |
sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
- name: Grant execute permission for gradlew for Appkit
run: chmod +x gradlew
- name: Compile appkit
run: ./gradlew publishToMavenLocal
# - name: Checkout Appkit Repo
# uses: actions/checkout@v3
# with:
# repository: grishka/appkit
#
# - name: set up JDK 17
# uses: actions/setup-java@v3
# with:
# java-version: '17'
# distribution: 'corretto'
# cache: gradle
#
# - name: Comment out signing config in appkits gradle file
# run: |
# sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
#
# - name: Grant execute permission for gradlew for Appkit
# run: chmod +x gradlew
#
# - name: Compile appkit
# run: ./gradlew publishToMavenLocal
- uses: actions/checkout@v3
- name: set up JDK 17

View File

@@ -21,7 +21,7 @@
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate (Currently broken)
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate
### You can also donate some Monero through this wallet address as well:
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j

View File

@@ -15,9 +15,9 @@ android {
archivesBaseName = "moshidon"
applicationId "org.joinmastodon.android.moshinda"
minSdk 23
targetSdk 33
versionCode 103
versionName "2.1.4+fork.103.moshinda"
targetSdk 34
versionCode 107
versionName "2.3.0+fork.107.moshinda"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
}
@@ -44,6 +44,28 @@ android {
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
}
}
// release{
// storeFile = file("keystore/release_keystore.jks")
// storePassword System.getenv("RELEASE_SIGNING_STORE_PASSWORD")
// if (storePassword == null) {
// Properties properties = new Properties()
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
// storePassword = properties.getProperty('RELEASE_SIGNING_STORE_PASSWORD')
// }
// keyAlias System.getenv("RELEASE_SIGNING_KEY_ALIAS")
// if (keyAlias == null) {
// Properties properties = new Properties()
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
// keyAlias = properties.getProperty('RELEASE_SIGNING_KEY_ALIAS')
// }
// keyPassword System.getenv("RELEASE_SIGNING_KEY_PASSWORD")
// if (keyPassword == null) {
// Properties properties = new Properties()
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
// keyPassword = properties.getProperty('RELEASE_SIGNING_KEY_PASSWORD')
// }
// }
}
buildTypes {
@@ -80,8 +102,16 @@ android {
shrinkResources true
versionNameSuffix '-play'
}
githubRelease { initWith release }
fdroidRelease { initWith release }
githubRelease {
initWith release
versionNameSuffix '-github'
}
fdroidRelease {
initWith release
// The F-droid build system doesn't like this at all for some reason.
// versionNameSuffix '-fdroid'
// signingConfig signingConfigs.release
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -117,7 +147,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.14'
implementation 'me.grishka.appkit:appkit:1.2.16'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -1,101 +0,0 @@
package org.joinmastodon.android.utils;
import static org.joinmastodon.android.model.FilterAction.*;
import static org.joinmastodon.android.model.FilterContext.*;
import static org.junit.Assert.*;
import org.joinmastodon.android.model.LegacyFilter;
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 LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter();
private static final List<LegacyFilter> 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()),
noAltText = Status.ofFake(null, "display me with a warning", Instant.now()),
withAltText = 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);
noAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
withAltText.mediaAttachments = Attachment.createFakeAttachments("fakeurl", new ColorDrawable());
for (Attachment mediaAttachment : withAltText.mediaAttachments) {
mediaAttachment.description = "Alt Text";
}
}
@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));
}
@Test
public void testAltTextFilterNoPass() {
assertFalse("should not pass because of no alt text",
new StatusFilterPredicate(allFilters, HOME).test(noAltText));
}
@Test
public void testAltTextFilterPass() {
assertTrue("should pass because of alt text",
new StatusFilterPredicate(allFilters, HOME).test(withAltText));
}
}

View File

@@ -211,7 +211,13 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
if(state==UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED);
}else{
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
downloadID=dm.enqueue(
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))

View File

@@ -5,7 +5,8 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
@@ -31,6 +32,7 @@
android:name=".MastodonApp"
android:allowBackup="true"
android:label="@string/mo_app_name"
android:dataExtractionRules="@xml/backup_rules"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher"
@@ -80,6 +82,15 @@
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".ChooseAccountForComposeActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
android:theme="@style/TransparentDialog">
<intent-filter>
<action android:name="android.intent.action.CHOOSER"/>
<category android:name="android.intent.category.LAUNCHER"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
@@ -105,13 +116,11 @@
</receiver>
<provider
android:name="org.joinmastodon.android.utils.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
android:name=".TweakedFileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths"/>
</provider>
</application>

View File

@@ -20,13 +20,16 @@ cachapa.xyz
canary.fedinuke.example.com
catgirl.life
cawfee.club
childlove.space
childlove.su
clew.lol
clubcyberia.co
contrapointsfan.club
cottoncandy.cafe
crlf.ninja
crucible.world
cum.camp
cum.salon
cunnyborea.space
decayable.ink
dembased.xyz
detroitriotcity.com
@@ -34,10 +37,12 @@ djsumdog.com
eientei.org
eveningzoo.club
fluf.club
foxgirl.lol
freak.university
freeatlantis.com
freespeechextremist.com
froth.zone
fsebugoutzone.org
gameliberty.club
gearlandia.haus
genderheretics.xyz
@@ -49,6 +54,7 @@ goyim.app
h5q.net
haeder.net
handholding.io
harpy.faith
hitchhiker.social
iddqd.social
kitsunemimi.club
@@ -56,15 +62,14 @@ kiwifarms.cc
kurosawa.moe
kyaruc.moe
leafposter.club
lewdieheaven.com
liberdon.com
ligma.pro
loli.church
lolicon.rocks
lolison.network
lolison.top
lovingexpressions.net
makemysarcophagus.com
marsey.moe
mastinator.com
merovingian.club
midwaytrades.com
@@ -74,17 +79,21 @@ mouse.services
mugicha.club
narrativerry.xyz
natehiggers.online
nationalist.social
needs.vodka
neenster.org
nicecrew.digital
nightshift.social
nnia.space
noagendasocial.com
noagendasocial.nl
noagendatube.com
noauthority.social
nobodyhasthe.biz
norwoodzero.net
nyanide.com
onionfarms.org
parcero.bond
pawlicker.com
pawoo.net
pedo.school
@@ -129,9 +138,11 @@ sonichu.com
spinster.xyz
springbo.cc
strelizia.net
taihou.website
tastingtraffic.net
teci.world
theapex.social
theblab.org
thechimp.zone
thenobody.club
thepostearthdestination.com
@@ -139,9 +150,11 @@ tkammer.de
trumpislovetrumpis.life
truthsocial.co.in
usualsuspects.lol
vampiremaid.cafe
varishangout.net
vtuberfan.social
wolfgirl.bar
xn--p1abe3d.xn--80asehdb
yggdrasil.social
youjo.love
zhub.link

View File

@@ -88,8 +88,13 @@ public class AudioPlayerService extends Service{
nm=getSystemService(NotificationManager.class);
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE), RECEIVER_EXPORTED);
registerReceiver(receiver, new IntentFilter(ACTION_STOP), RECEIVER_EXPORTED);
}else{
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
}
instance=this;
}

View File

@@ -0,0 +1,52 @@
package org.joinmastodon.android;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
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.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
import java.util.Objects;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
public class ChooseAccountForComposeActivity extends FragmentStackActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState);
if (savedInstanceState == null && Objects.equals(getIntent().getAction(), Intent.ACTION_CHOOSER)) {
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if (sessions.isEmpty()){
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
finish();
} else if (sessions.size() > 1) {
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, R.drawable.ic_fluent_compose_28_regular,
R.string.choose_account, null, false);
sheet.setOnClick((accountId, open) -> {
openComposeFragment(accountId);
});
sheet.show();
} else if (sessions.size() == 1) {
openComposeFragment(sessions.get(0).getID());
}
}
}
private void openComposeFragment(String accountID){
getWindow().setBackgroundDrawable(null);
Bundle args=new Bundle();
args.putString("account", accountID);
Fragment fragment=new ComposeFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
}
}

View File

@@ -13,7 +13,7 @@ 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.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil;
@@ -42,7 +42,11 @@ public class ExternalShareActivity extends FragmentStackActivity{
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
finish();
} else if (isOpenable || sessions.size() > 1) {
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, R.drawable.ic_fluent_share_28_regular,
isOpenable
? R.string.sk_external_share_or_open_title
: R.string.sk_external_share_title,
null, isOpenable);
sheet.setOnClick((accountId, open) -> {
if (open && text.isPresent()) {
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {

View File

@@ -0,0 +1,841 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.content.ClipData;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing
* of files associated with an app by creating a <code>content://</code> {@link Uri} for a file
* instead of a <code>file:///</code> {@link Uri}.
* <p>
* A content URI allows you to grant read and write access using
* temporary access permissions. When you create an {@link Intent} containing
* a content URI, in order to send the content URI
* to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add
* permissions. These permissions are available to the client app for as long as the stack for
* a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a
* {@link android.app.Service}, the permissions are available as long as the
* {@link android.app.Service} is running.
* <p>
* In comparison, to control access to a <code>file:///</code> {@link Uri} you have to modify the
* file system permissions of the underlying file. The permissions you provide become available to
* <em>any</em> app, and remain in effect until you change them. This level of access is
* fundamentally insecure.
* <p>
* The increased level of file access security offered by a content URI
* makes FileProvider a key part of Android's security infrastructure.
* <p>
* This overview of FileProvider includes the following topics:
* </p>
* <ol>
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
* <li><a href="#GetUri">Retrieving the Content URI for a File</li>
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
* </ol>
* <h3 id="ProviderDefinition">Defining a FileProvider</h3>
* <p>
* Since the default functionality of FileProvider includes content URI generation for files, you
* don't need to define a subclass in code. Instead, you can include a FileProvider in your app
* by specifying it entirely in XML. To specify the FileProvider component itself, add a
* <code><a href="{@docRoot}guide/topics/manifest/provider-element.html">&lt;provider&gt;</a></code>
* element to your app manifest. Set the <code>android:name</code> attribute to
* <code>androidx.core.content.FileProvider</code>. Set the <code>android:authorities</code>
* attribute to a URI authority based on a domain you control; for example, if you control the
* domain <code>mydomain.com</code> you should use the authority
* <code>com.mydomain.fileprovider</code>. Set the <code>android:exported</code> attribute to
* <code>false</code>; the FileProvider does not need to be public. Set the
* <a href="{@docRoot}guide/topics/manifest/provider-element.html#gprmsn"
* >android:grantUriPermissions</a> attribute to <code>true</code>, to allow you
* to grant temporary access to files. For example:
* <pre class="prettyprint">
*&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider
* android:name="androidx.core.content.FileProvider"
* android:authorities="com.mydomain.fileprovider"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* ...
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
* <p>
* If you want to override any of the default behavior of FileProvider methods, extend
* the FileProvider class and use the fully-qualified class name in the <code>android:name</code>
* attribute of the <code>&lt;provider&gt;</code> element.
* <h3 id="SpecifyFiles">Specifying Available Files</h3>
* A FileProvider can only generate a content URI for files in directories that you specify
* beforehand. To specify a directory, specify the its storage area and path in XML, using child
* elements of the <code>&lt;paths&gt;</code> element.
* For example, the following <code>paths</code> element tells FileProvider that you intend to
* request content URIs for the <code>images/</code> subdirectory of your private file area.
* <pre class="prettyprint">
*&lt;paths xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;files-path name="my_images" path="images/"/&gt;
* ...
*&lt;/paths&gt;
*</pre>
* <p>
* The <code>&lt;paths&gt;</code> element must contain one or more of the following child elements:
* </p>
* <dl>
* <dt>
* <pre class="prettyprint">
*&lt;files-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the <code>files/</code> subdirectory of your app's internal storage
* area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
* Context.getFilesDir()}.
* </dd>
* <dt>
* <pre>
*&lt;cache-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* <dt>
* <dd>
* Represents files in the cache subdirectory of your app's internal storage area. The root path
* of this subdirectory is the same as the value returned by {@link Context#getCacheDir()
* getCacheDir()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of the external storage area. The root path of this subdirectory
* is the same as the value returned by
* {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-files-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external storage area. The root path of this
* subdirectory is the same as the value returned by
* {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-cache-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external cache area. The root path of this
* subdirectory is the same as the value returned by
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-media-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external media area. The root path of this
* subdirectory is the same as the value returned by the first result of
* {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
* <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
* </dd>
* </dl>
* <p>
* These child elements all use the same attributes:
* </p>
* <dl>
* <dt>
* <code>name="<i>name</i>"</code>
* </dt>
* <dd>
* A URI path segment. To enforce security, this value hides the name of the subdirectory
* you're sharing. The subdirectory name for this value is contained in the
* <code>path</code> attribute.
* </dd>
* <dt>
* <code>path="<i>path</i>"</code>
* </dt>
* <dd>
* The subdirectory you're sharing. While the <code>name</code> attribute is a URI path
* segment, the <code>path</code> value is an actual subdirectory name. Notice that the
* value refers to a <b>subdirectory</b>, not an individual file or files. You can't
* share a single file by its file name, nor can you specify a subset of files using
* wildcards.
* </dd>
* </dl>
* <p>
* You must specify a child element of <code>&lt;paths&gt;</code> for each directory that contains
* files for which you want content URIs. For example, these XML elements specify two directories:
* <pre class="prettyprint">
*&lt;paths xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;files-path name="my_images" path="images/"/&gt;
* &lt;files-path name="my_docs" path="docs/"/&gt;
*&lt;/paths&gt;
*</pre>
* <p>
* Put the <code>&lt;paths&gt;</code> element and its children in an XML file in your project.
* For example, you can add them to a new file called <code>res/xml/file_paths.xml</code>.
* To link this file to the FileProvider, add a
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">&lt;meta-data&gt;</a> element
* as a child of the <code>&lt;provider&gt;</code> element that defines the FileProvider. Set the
* <code>&lt;meta-data&gt;</code> element's "android:name" attribute to
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
* to <code>&#64;xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
* extension). For example:
* <pre class="prettyprint">
*&lt;provider
* android:name="androidx.core.content.FileProvider"
* android:authorities="com.mydomain.fileprovider"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* &lt;meta-data
* android:name="android.support.FILE_PROVIDER_PATHS"
* android:resource="&#64;xml/file_paths" /&gt;
*&lt;/provider&gt;
*</pre>
* <h3 id="GetUri">Generating the Content URI for a File</h3>
* <p>
* To share a file with another app using a content URI, your app has to generate the content URI.
* To generate the content URI, create a new {@link File} for the file, then pass the {@link File}
* to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an
* {@link Intent}. The client app that receives the content URI can open the file
* and access its contents by calling
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}.
* <p>
* For example, suppose your app is offering files to other apps with a FileProvider that has the
* authority <code>com.mydomain.fileprovider</code>. To get a content URI for the file
* <code>default_image.jpg</code> in the <code>images/</code> subdirectory of your internal storage
* add the following code:
* <pre class="prettyprint">
*File imagePath = new File(Context.getFilesDir(), "images");
*File newFile = new File(imagePath, "default_image.jpg");
*Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
*</pre>
* As a result of the previous snippet,
* {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI
* <code>content://com.mydomain.fileprovider/my_images/default_image.jpg</code>.
* <h3 id="Permissions">Granting Temporary Permissions to a URI</h3>
* To grant an access permission to a content URI returned from
* {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following:
* <ul>
* <li>
* Call the method
* {@link Context#grantUriPermission(String, Uri, int)
* Context.grantUriPermission(package, Uri, mode_flags)} for the <code>content://</code>
* {@link Uri}, using the desired mode flags. This grants temporary access permission for the
* content URI to the specified package, according to the value of the
* the <code>mode_flags</code> parameter, which you can set to
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
* or both. The permission remains in effect until you revoke it by calling
* {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
* reboots.
* </li>
* <li>
* Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}.
* </li>
* <li>
* Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both.
* </li>
* <li>
* Finally, send the {@link Intent} to
* another app. Most often, you do this by calling
* {@link android.app.Activity#setResult(int, Intent) setResult()}.
* <p>
* Permissions granted in an {@link Intent} remain in effect while the stack of the receiving
* {@link android.app.Activity} is active. When the stack finishes, the permissions are
* automatically removed. Permissions granted to one {@link android.app.Activity} in a client
* app are automatically extended to other components of that app.
* </p>
* </li>
* </ul>
* <h3 id="ServeUri">Serving a Content URI to Another App</h3>
* <p>
* There are a variety of ways to serve the content URI for a file to a client app. One common way
* is for the client app to start your app by calling
* {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()},
* which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app.
* In response, your app can immediately return a content URI to the client app or present a user
* interface that allows the user to pick a file. In the latter case, once the user picks the file
* your app can return its content URI. In both cases, your app returns the content URI in an
* {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}.
* </p>
* <p>
* You can also put the content URI in a {@link android.content.ClipData} object and then add the
* object to an {@link Intent} you send to a client app. To do this, call
* {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can
* add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own
* content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent}
* to set temporary access permissions, the same permissions are applied to all of the content
* URIs.
* </p>
* <p class="note">
* <strong>Note:</strong> The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is
* only available in platform version 16 (Android 4.1) and later. If you want to maintain
* compatibility with previous versions, you should send one content URI at a time in the
* {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling
* {@link Intent#setData setData()}.
* </p>
* <h3 id="">More Information</h3>
* <p>
* To learn more about FileProvider, see the Android training class
* <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files Securely with URIs</a>.
* </p>
*/
public class FileProvider extends ContentProvider {
private static final String[] COLUMNS = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
private static final String
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
private static final File DEVICE_ROOT = new File("/");
@GuardedBy("sCache")
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
private PathStrategy mStrategy;
/**
* The default FileProvider implementation does not need to be initialized. If you want to
* override this method, you must provide your own subclass of FileProvider.
*/
@Override
public boolean onCreate() {
return true;
}
/**
* After the FileProvider is instantiated, this method is called to provide the system with
* information about the provider.
*
* @param context A {@link Context} for the current component.
* @param info A {@link ProviderInfo} for the new provider.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Sanity check our security
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority);
}
/**
* Return a content URI for a given {@link File}. Specific temporary
* permissions for the content URI can be set with
* {@link Context#grantUriPermission(String, Uri, int)}, or added
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
* <code>content</code> {@link Uri} for file paths defined in their <code>&lt;paths&gt;</code>
* meta-data element. See the Class Overview for more information.
*
* @param context A {@link Context} for the current component.
* @param authority The authority of a {@link FileProvider} defined in a
* {@code <provider>} element in your app's manifest.
* @param file A {@link File} pointing to the filename for which you want a
* <code>content</code> {@link Uri}.
* @return A content URI for the file.
* @throws IllegalArgumentException When the given {@link File} is outside
* the paths supported by the provider.
*/
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
/**
* Use a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
* managed by the FileProvider.
* FileProvider reports the column names defined in {@link OpenableColumns}:
* <ul>
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
* <li>{@link OpenableColumns#SIZE}</li>
* </ul>
* For more information, see
* {@link ContentProvider#query(Uri, String[], String, String[], String)
* ContentProvider.query()}.
*
* @param uri A content URI returned by {@link #getUriForFile}.
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
* included.
* @param selection Selection criteria to apply. If null then all data that matches the content
* URI is returned.
* @param selectionArgs An array of {@link String}, containing arguments to bind to
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
* values are bound to <i>selection</i> as {@link String} values.
* @param sortOrder A {@link String} containing the column name(s) on which to sort
* the resulting {@link Cursor}.
* @return A {@link Cursor} containing the results of the query.
*
*/
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
/**
* Returns the MIME type of a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
*
* @param uri A content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @return If the associated file has an extension, the MIME type associated with that
* extension; otherwise <code>application/octet-stream</code>.
*/
@Override
public String getType(@NonNull Uri uri) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
/**
* Deletes the file associated with the specified content URI, as
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
*
* @param uri A content URI for a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param selection Ignored. Set to {@code null}.
* @param selectionArgs Ignored. Set to {@code null}.
* @return 1 if the delete succeeds; otherwise, 0.
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
return file.delete() ? 1 : 0;
}
/**
* By default, FileProvider automatically returns the
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor}.
*
* To override this method, you must provide your own subclass of FileProvider.
*
* @param uri A content URI associated with a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
* write access, or "rwt" for read and write access that truncates any existing file.
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
*/
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
/**
* Return {@link PathStrategy} for given authority, either by parsing or
* returning from cache.
*/
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
/**
* Parse and return {@link PathStrategy} for given authority as defined in
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
*
* @see #getPathStrategy(Context, String)
*/
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
if (info == null) {
throw new IllegalArgumentException(
"Couldn't find meta-data for provider with authority " + authority);
}
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = context.getExternalFilesDirs(null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = context.getExternalCacheDirs();
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
/**
* Strategy for mapping between {@link File} and {@link Uri}.
* <p>
* Strategies must be symmetric so that mapping a {@link File} to a
* {@link Uri} and then back to a {@link File} points at the original
* target.
* <p>
* Strategies must remain consistent across app launches, and not rely on
* dynamic state. This ensures that any generated {@link Uri} can still be
* resolved if your process is killed and later restarted.
*
* @see SimplePathStrategy
*/
interface PathStrategy {
/**
* Return a {@link Uri} that represents the given {@link File}.
*/
Uri getUriForFile(File file);
/**
* Return a {@link File} that represents the given {@link Uri}.
*/
File getFileForUri(Uri uri);
}
/**
* Strategy that provides access to files living under a narrow whitelist of
* filesystem roots. It will throw {@link SecurityException} if callers try
* accessing files outside the configured roots.
* <p>
* For example, if configured with
* {@code addRoot("myfiles", context.getFilesDir())}, then
* {@code context.getFileStreamPath("foo.txt")} would map to
* {@code content://myauthority/myfiles/foo.txt}.
*/
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<String, File>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
/**
* Copied from ContentResolver.java
*/
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
} else if ("w".equals(mode) || "wt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else if ("wa".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_APPEND;
} else if ("rw".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE;
} else if ("rwt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
return modeBits;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
}

View File

@@ -28,6 +28,9 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
public class GlobalUserPreferences{
private static final String TAG="GlobalUserPreferences";
@@ -54,7 +57,6 @@ public class GlobalUserPreferences{
public static boolean spectatorMode;
public static boolean autoHideFab;
public static boolean allowRemoteLoading;
public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers;
public static boolean disableM3PillActiveIndicator;
public static boolean showNavigationLabels;
@@ -75,15 +77,20 @@ public class GlobalUserPreferences{
public static boolean hapticFeedback;
public static boolean replyLineAboveHeader;
public static boolean swapBookmarkWithBoostAction;
public static boolean loadRemoteAccountFollowers;
public static boolean mentionRebloggerAutomatically;
public static boolean showPostsWithoutAlt;
public static boolean showMediaPreview;
public static boolean removeTrackingParams;
public static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
}
private static SharedPreferences getPreReplyPrefs(){
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
}
public static <T> T fromJson(String json, Type type, T orElse){
if(json==null) return orElse;
try{
@@ -129,7 +136,6 @@ public class GlobalUserPreferences{
autoHideFab=prefs.getBoolean("autoHideFab", true);
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
@@ -152,10 +158,10 @@ public class GlobalUserPreferences{
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
hapticFeedback=prefs.getBoolean("hapticFeedback", true);
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
loadRemoteAccountFollowers=prefs.getBoolean("loadRemoteAccountFollowers", true);
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
showPostsWithoutAlt=prefs.getBoolean("showPostsWithoutAlt", true);
showMediaPreview=prefs.getBoolean("showMediaPreview", true);
removeTrackingParams=prefs.getBoolean("removeTrackingParams", true);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
@@ -205,7 +211,6 @@ public class GlobalUserPreferences{
.putBoolean("autoHideFab", autoHideFab)
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault)
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
.putBoolean("showNavigationLabels", showNavigationLabels)
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
@@ -224,7 +229,6 @@ public class GlobalUserPreferences{
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
.putBoolean("hapticFeedback", hapticFeedback)
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
.putBoolean("showDividers", showDividers)
@@ -232,16 +236,46 @@ public class GlobalUserPreferences{
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
.putBoolean("showMediaPreview", showMediaPreview)
.putBoolean("removeTrackingParams", removeTrackingParams)
.apply();
}
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
return true;
if(account==null)
return false;
String accountKey=account.acct;
if(!accountKey.contains("@"))
accountKey+="@"+AccountSessionManager.get(accountID).domain;
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
}
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
String key;
if(account==null){
key="opt_out_"+type;
}else{
String accountKey=account.acct;
if(!accountKey.contains("@"))
accountKey+="@"+AccountSessionManager.get(accountID).domain;
key="opt_out_"+type+"_"+accountKey.toLowerCase();
}
getPreReplyPrefs().edit().putBoolean(key, true).apply();
}
public enum ThemePreference{
AUTO,
LIGHT,
DARK
}
public enum PreReplySheetType{
OLD_POST,
NON_MUTUAL
}
public enum AutoRevealMode {
NEVER,
THREADS,
@@ -306,5 +340,4 @@ public class GlobalUserPreferences{
}
//endregion
}

View File

@@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.Uri;
import android.os.BadParcelableException;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
@@ -110,8 +111,6 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
@@ -131,11 +130,11 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
session=AccountSessionManager.get(accountID);
if(session==null || !session.activated)
return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
}
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true, null, 0, 0)
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
new GetSearchResults(q, type, true, null, 0, 0)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
@@ -186,17 +185,6 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
showFragment(fragment);
}
private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
return;
ComposeFragment compose=new ComposeFragment();
Bundle composeArgs=new Bundle();
composeArgs.putString("account", session.getID());
compose.setArguments(composeArgs);
showFragment(compose);
}
private void maybeRequestNotificationsPermission(){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
@@ -334,10 +322,14 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
if(fromNotification && hasNotification){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
} else if (intent.getBooleanExtra("compose", false)){
showCompose();
// Parcelables might not be compatible across app versions so this protects against possible crashes
// when a notification was received, then the app was updated, and then the user opened the notification
try{
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}catch(BadParcelableException x){
Log.w(TAG, x);
}
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
} else {

View File

@@ -25,7 +25,7 @@ public class MastodonApp extends Application{
params.diskCacheSize=100*1024*1024;
params.maxMemoryCacheSize=Integer.MAX_VALUE;
ImageCache.setParams(params);
NetworkUtils.setUserAgent("MastodonAndroid/"+BuildConfig.VERSION_NAME);
NetworkUtils.setUserAgent("MoshidonAndroid/"+BuildConfig.VERSION_NAME);
PushSubscriptionManager.tryRegisterFCM();
GlobalUserPreferences.load();

View File

@@ -101,7 +101,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
new GetNotificationByID(pn.notificationId+"")
new GetNotificationByID(pn.notificationId)
.setCallback(new Callback<>(){
@Override
public void onSuccess(org.joinmastodon.android.model.Notification result){
@@ -133,7 +133,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
if(intent.hasExtra("notification")){
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
String statusID=notification.status.id;
String statusID = null;
if(notification != null && notification.status != null)
statusID=notification.status.id;
if (statusID != null) {
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
@@ -154,9 +158,9 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
}
public void notifyUnifiedPush(Context context, String accountID, org.joinmastodon.android.model.Notification notification) {
public void notifyUnifiedPush(Context context, AccountSession account, org.joinmastodon.android.model.Notification notification) {
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, account, notification), account.getID(), notification);
}
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){

View File

@@ -0,0 +1,38 @@
package org.joinmastodon.android;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.FileNotFoundException;
import java.util.Arrays;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class TweakedFileProvider extends FileProvider{
private static final String TAG="TweakedFileProvider";
@Override
public String getType(@NonNull Uri uri){
Log.d(TAG, "getType() called with: uri = ["+uri+"]");
if(uri.getPathSegments().get(0).equals("image_cache")){
Log.i(TAG, "getType: HERE!");
return "image/jpeg"; // might as well be a png but image decoding APIs don't care, needs to be image/* though
}
return super.getType(uri);
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
Log.d(TAG, "query() called with: uri = ["+uri+"], projection = ["+Arrays.toString(projection)+"], selection = ["+selection+"], selectionArgs = ["+Arrays.toString(selectionArgs)+"], sortOrder = ["+sortOrder+"]");
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
Log.d(TAG, "openFile() called with: uri = ["+uri+"], mode = ["+mode+"]");
return super.openFile(uri, mode);
}
}

View File

@@ -72,7 +72,7 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
result.items
.stream()
.findFirst()
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, account, value)));
}
@Override

View File

@@ -9,21 +9,32 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
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.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
@@ -44,6 +55,7 @@ public class CacheController{
private final Runnable databaseCloseRunnable=this::closeDatabase;
private boolean loadingNotifications;
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
private List<FollowList> lists;
private static final int POST_FLAG_GAP_AFTER=1;
@@ -348,6 +360,99 @@ public class CacheController{
}, 0);
}
public void reloadLists(Callback<List<FollowList>> callback){
new GetLists()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<FollowList> result){
result.sort(Comparator.comparing(l->l.title));
lists=result;
if(callback!=null)
callback.onSuccess(result);
writeListsToFile();
}
@Override
public void onError(ErrorResponse error){
if(callback!=null)
callback.onError(error);
}
})
.exec(accountID);
}
private List<FollowList> loadListsFromFile(){
File file=getListsFile();
if(!file.exists())
return null;
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
}catch(Exception x){
Log.w(TAG, "failed to read lists from cache file", x);
return null;
}
}
private void writeListsToFile(){
databaseThread.postRunnable(()->{
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
MastodonAPIController.gson.toJson(lists, out);
}catch(IOException x){
Log.w(TAG, "failed to write lists to cache file", x);
}
}, 0);
}
public void getLists(Callback<List<FollowList>> callback){
if(lists!=null){
if(callback!=null)
callback.onSuccess(lists);
return;
}
databaseThread.postRunnable(()->{
List<FollowList> lists=loadListsFromFile();
if(lists!=null){
this.lists=lists;
if(callback!=null)
uiHandler.post(()->callback.onSuccess(lists));
return;
}
reloadLists(callback);
}, 0);
}
public File getListsFile(){
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
}
public void addList(FollowList list){
if(lists==null)
return;
lists.add(list);
lists.sort(Comparator.comparing(l->l.title));
writeListsToFile();
}
public void deleteList(String id){
if(lists==null)
return;
lists.removeIf(l->l.id.equals(id));
writeListsToFile();
}
public void updateList(FollowList list){
if(lists==null)
return;
for(int i=0;i<lists.size();i++){
if(lists.get(i).id.equals(list.id)){
lists.set(i, list);
lists.sort(Comparator.comparing(l->l.title));
writeListsToFile();
break;
}
}
}
private class DatabaseHelper extends SQLiteOpenHelper{
public DatabaseHelper(){

View File

@@ -54,7 +54,9 @@ public class MastodonAPIController{
.create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
private AccountSession session;
@@ -89,7 +91,11 @@ public class MastodonAPIController{
final boolean isBad = host == null || badDomains.stream().anyMatch(h -> h.equalsIgnoreCase(host) || host.toLowerCase().endsWith("." + h));
thread.postRunnable(()->{
try{
// if (isBad) throw new IllegalArgumentException();
if(isBad){
Log.i(TAG, "submitRequest: refusing to connect to bad domain: " + host);
throw new IllegalArgumentException("Failed to connect to domain");
}
if(req.canceled)
return;
Request.Builder builder=new Request.Builder()
@@ -122,15 +128,15 @@ public class MastodonAPIController{
}
if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
call.enqueue(new Callback(){
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e){
if(call.isCanceled())
if(req.canceled)
return;
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
synchronized(req){
req.okhttpCall=null;
}
@@ -139,10 +145,10 @@ public class MastodonAPIController{
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
if(call.isCanceled())
if(req.canceled)
return;
if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
synchronized(req){
req.okhttpCall=null;
}
@@ -153,7 +159,7 @@ public class MastodonAPIController{
try{
if(BuildConfig.DEBUG){
JsonElement respJson=JsonParser.parseReader(reader);
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
Log.d(TAG, logTag(session)+"response body: "+respJson);
if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
else if(req.respClass!=null)
@@ -175,7 +181,7 @@ public class MastodonAPIController{
return;
}
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x);
return;
}
@@ -184,19 +190,19 @@ public class MastodonAPIController{
req.validateAndPostprocessResponse(respObj, response);
}catch(IOException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
req.onError(x.getLocalizedMessage(), response.code(), x);
return;
}
if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
req.onSuccess(respObj);
}else{
try{
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
Log.w(TAG, logTag(session)+response+" received error: "+error);
if(error.has("details")){
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
@@ -231,7 +237,7 @@ public class MastodonAPIController{
});
}catch(Exception x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
req.onError(x.getLocalizedMessage(), 0, x);
}
}, 0);
@@ -244,4 +250,8 @@ public class MastodonAPIController{
public static OkHttpClient getHttpClient(){
return httpClient;
}
private static String logTag(AccountSession session){
return "["+(session==null ? "no-auth" : session.getID())+"] ";
}
}

View File

@@ -180,6 +180,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
public RequestBody getRequestBody() throws IOException{
if(requestBody instanceof RequestBody rb)
return rb;
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
}

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
public CheckInviteLink(String path){
super(HttpMethod.GET, path, Response.class);
addHeader("Accept", "application/json");
}
@Override
protected String getPathPrefix(){
return "";
}
public static class Response extends BaseModel{
@RequiredField
public String inviteCode;
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.FollowList;
import java.util.List;
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
public GetAccountLists(String id){
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
}
}

View File

@@ -4,22 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
super(HttpMethod.POST, "/accounts", Token.class);
setRequestBody(new Body(username, email, password, locale, reason, timezone));
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
}
private static class Body{
public String username, email, password, locale, reason, timeZone;
public String username, email, password, locale, reason, timeZone, inviteCode;
public boolean agreement=true;
public Body(String username, String email, String password, String locale, String reason, String timeZone){
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
this.username=username;
this.email=email;
this.password=password;
this.locale=locale;
this.reason=reason;
this.timeZone=timeZone;
this.inviteCode=inviteCode;
}
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
import java.util.List;
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
addQueryParameter("q", q);
if(limit>0)
addQueryParameter("limit", limit+"");
if(offset>0)
addQueryParameter("offset", offset+"");
if(resolve)
addQueryParameter("resolve", "true");
if(following)
addQueryParameter("following", "true");
}
}

View File

@@ -4,15 +4,21 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
public SetAccountMuted(String id, boolean muted, long duration){
public SetAccountMuted(String id, boolean muted, long duration, boolean muteNotifications){
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
setRequestBody(new Request(duration));
if(muted)
setRequestBody(new Request(duration, muteNotifications));
else{
setRequestBody(new Object());
}
}
private static class Request{
public long duration;
public Request(long duration){
public boolean muteNotifications;
public Request(long duration, boolean muteNotifications){
this.duration=duration;
this.muteNotifications=muteNotifications;
}
}
}

View File

@@ -22,6 +22,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
private Uri avatar, cover;
private File avatarFile, coverFile;
private List<AccountField> fields;
private Boolean discoverable, indexable;
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
@@ -41,6 +42,12 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
this.fields=fields;
}
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
this.discoverable=discoverable;
this.indexable=indexable;
return this;
}
@Override
public RequestBody getRequestBody() throws IOException{
MultipartBody.Builder bldr=new MultipartBody.Builder()
@@ -58,15 +65,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
}else if(coverFile!=null){
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
}
if(fields.isEmpty()){
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
}else{
int i=0;
for(AccountField field:fields){
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
i++;
if(fields!=null){
if(fields.isEmpty()){
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
}else{
int i=0;
for(AccountField field:fields){
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
i++;
}
}
}
if(discoverable!=null)
bldr.addFormDataPart("discoverable", discoverable.toString());
if(indexable!=null)
bldr.addFormDataPart("indexable", indexable.toString());
return bldr.build();
}

View File

@@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
import com.google.gson.annotations.SerializedName;
import androidx.annotation.Keep;
@Keep
class KeywordAttribute{
public String id;
@SerializedName("_destroy")

View File

@@ -1,17 +1,19 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.util.List;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class AddAccountsToList extends MastodonAPIRequest<Object> {
public AddAccountsToList(String listId, List<String> accountIds){
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
Request req = new Request();
req.accountIds = accountIds;
setRequestBody(req);
}
import java.nio.charset.StandardCharsets;
import java.util.Collection;
public static class Request{
public List<String> accountIds;
}
import okhttp3.FormBody;
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
public AddAccountsToList(String listID, Collection<String> accountIDs){
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
for(String id:accountIDs){
builder.add("account_ids[]", id);
}
setRequestBody(builder.build());
}
}

View File

@@ -1,21 +1,23 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.FollowList;
public class CreateList extends MastodonAPIRequest<ListTimeline> {
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
super(HttpMethod.POST, "/lists", ListTimeline.class);
Request req = new Request();
req.title = title;
req.exclusive = exclusive;
req.repliesPolicy = repliesPolicy;
setRequestBody(req);
public class CreateList extends MastodonAPIRequest<FollowList>{
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
super(HttpMethod.POST, "/lists", FollowList.class);
setRequestBody(new Request(title, repliesPolicy, exclusive));
}
public static class Request {
private static class Request{
public String title;
public FollowList.RepliesPolicy repliesPolicy;
public boolean exclusive;
public ListTimeline.RepliesPolicy repliesPolicy;
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
this.title=title;
this.repliesPolicy=repliesPolicy;
this.exclusive=exclusive;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
public class GetListAccounts extends HeaderPaginationRequest<Account>{
public GetListAccounts(String listID, String maxID, int limit){
super(HttpMethod.GET, "/lists/"+listID+"/accounts", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
addQueryParameter("limit", String.valueOf(limit));
}
}

View File

@@ -3,11 +3,11 @@ package org.joinmastodon.android.api.requests.lists;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.FollowList;
import java.util.List;
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
public GetLists() {
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
}

View File

@@ -1,17 +1,19 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.util.List;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
public RemoveAccountsFromList(String listId, List<String> accountIds){
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
Request req = new Request();
req.accountIds = accountIds;
setRequestBody(req);
}
import java.nio.charset.StandardCharsets;
import java.util.Collection;
public static class Request{
public List<String> accountIds;
}
import okhttp3.FormBody;
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
for(String id:accountIDs){
builder.add("account_ids[]", id);
}
setRequestBody(builder.build());
}
}

View File

@@ -1,15 +1,23 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.FollowList;
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
CreateList.Request req = new CreateList.Request();
req.title = title;
req.exclusive = exclusive;
req.repliesPolicy = repliesPolicy;
setRequestBody(req);
public class UpdateList extends MastodonAPIRequest<FollowList>{
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
setRequestBody(new Request(title, repliesPolicy, exclusive));
}
private static class Request{
public String title;
public FollowList.RepliesPolicy repliesPolicy;
public boolean exclusive;
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
this.title=title;
this.repliesPolicy=repliesPolicy;
this.exclusive=exclusive;
}
}
}

View File

@@ -26,8 +26,11 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
s.visibility=StatusPrivacy.PUBLIC;
s.mentions=Collections.emptyList();
s.tags=Collections.emptyList();
if (s.poll != null)
if(s.poll!=null){
s.poll.id="fakeID"+i;
s.poll.emojis=Collections.emptyList();
s.poll.ownVotes=Collections.emptyList();
}
i++;
}
super.validateAndPostprocessResponse(respObj, httpResponse);

View File

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

View File

@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID, String replyVisibility){
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
if(local)
addQueryParameter("local", "true");
@@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("remote", "true");
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
if(!TextUtils.isEmpty(minID))
addQueryParameter("min_id", minID);
if(!TextUtils.isEmpty(sinceID))
addQueryParameter("since_id", sinceID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(replyVisibility != null)

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.timelines;
import androidx.annotation.NonNull;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetTrendingLinksTimeline extends MastodonAPIRequest<List<Status>>{
public GetTrendingLinksTimeline(@NonNull String url, String maxID, String minID, int limit){
super(HttpMethod.GET, "/timelines/link/", new TypeToken<>(){});
addQueryParameter("url", url);
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
}

View File

@@ -50,6 +50,7 @@ public class AccountLocalPreferences{
public ShowEmojiReactions showEmojiReactions;
public ColorPreference color;
public ArrayList<Emoji> recentCustomEmoji;
public boolean preReplySheet;
private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
private final static Type timelinesType=new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
@@ -68,6 +69,7 @@ public class AccountLocalPreferences{
revealCWs=prefs.getBoolean("revealCWs", false);
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
// preReplySheet=prefs.getBoolean("preReplySheet", false);
// MEGALODON
showReplies=prefs.getBoolean("showReplies", true);
@@ -112,6 +114,9 @@ public class AccountLocalPreferences{
.putBoolean("hideSensitive", hideSensitiveMedia)
.putBoolean("serverSideFilters", serverSideFiltersSupported)
//TODO figure this stuff out
// .putBoolean("preReplySheet", preReplySheet)
// MEGALODON
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)

View File

@@ -21,11 +21,13 @@ import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AltTextFilter;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
@@ -34,7 +36,9 @@ import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator;
import java.io.File;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -70,6 +74,7 @@ public class AccountSession{
private transient SharedPreferences prefs;
private transient boolean preferencesNeedSaving;
private transient AccountLocalPreferences localPreferences;
private transient List<FollowList> lists;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
@@ -310,8 +315,11 @@ public class AccountSession{
return true;
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
if(getLocalPreferences().serverSideFiltersSupported){
// Moshidon: this code path in CustomLocalTimelines makes the app crash, so this check is here
if (s.filtered == null)
return false;
for(FilterResult filter : s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE && filter.filter.context.contains(context))
return true;
}
}else if(wordFilters!=null){
@@ -323,6 +331,21 @@ public class AccountSession{
return false;
}
public List<FilterResult> getClientSideFilters(Status status) {
List<FilterResult> filters = new ArrayList<>();
// filter post that have no alt text
// it only applies when activated in the settings
AltTextFilter altTextFilter=new AltTextFilter(FilterAction.WARN, EnumSet.allOf(FilterContext.class));
if(altTextFilter.matches(status)){
FilterResult filterResult=new FilterResult();
filterResult.filter=altTextFilter;
filterResult.keywordMatches=List.of();
filters.add(filterResult);
}
return filters;
}
public void updateAccountInfo(){
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
}
@@ -343,4 +366,12 @@ public class AccountSession{
.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
.orElse("");
}
public boolean isNotificationsMentionsOnly(){
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
}
public void setNotificationsMentionsOnly(boolean mentionsOnly){
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
}
}

View File

@@ -1,7 +1,5 @@
package org.joinmastodon.android.api.session;
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
@@ -17,7 +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.ChooseAccountForComposeActivity;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -96,6 +94,7 @@ public class AccountSessionManager{
private AccountSessionManager(){
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
// This file should not be backed up, otherwise the app may start with accounts already logged in. See res/xml/backup_rules.xml
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
if(!file.exists())
return;
@@ -216,12 +215,17 @@ public class AccountSessionManager{
public void removeAccount(String id){
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
session.getCacheController().getListsFile().delete();
MastodonApp.context.deleteDatabase(id+".db");
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
MastodonApp.context.deleteSharedPreferences(id);
}else{
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
String dataDir=MastodonApp.context.getApplicationInfo().dataDir;
if(dataDir!=null){
File prefsDir=new File(dataDir, "shared_prefs");
new File(prefsDir, id+".xml").delete();
}
}
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
@@ -485,15 +489,19 @@ public class AccountSessionManager{
if(Build.VERSION.SDK_INT<26)
return;
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
Intent intent = new Intent(MastodonApp.context, ChooseAccountForComposeActivity.class)
.setAction(Intent.ACTION_CHOOSER)
.putExtra("compose", true);
// This was done so that the old shortcuts get updated to the new implementation.
if((sm.getDynamicShortcuts().isEmpty() || sm.getDynamicShortcuts().get(0).getIntent() != intent || BuildConfig.DEBUG ) && !sessions.isEmpty()){
// There are no shortcuts, but there are accounts. Add a compose shortcut.
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.setIntent(intent)
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
}else if(sessions.isEmpty()){

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Account;
public class AccountAddedToListEvent{
public final String accountID;
public final String listID;
public final Account account;
public AccountAddedToListEvent(String accountID, String listID, Account account){
this.accountID=accountID;
this.listID=listID;
this.account=account;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
public class AccountRemovedFromListEvent{
public final String accountID;
public final String listID;
public final String targetAccountID;
public AccountRemovedFromListEvent(String accountID, String listID, String targetAccountID){
this.accountID=accountID;
this.listID=listID;
this.targetAccountID=targetAccountID;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class FinishListCreationFragmentEvent{
public final String accountID;
public final String listID;
public FinishListCreationFragmentEvent(String accountID, String listID){
this.accountID=accountID;
this.listID=listID;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.FollowList;
public class ListCreatedEvent{
public final String accountID;
public final FollowList list;
public ListCreatedEvent(String accountID, FollowList list){
this.accountID=accountID;
this.list=list;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.FollowList;
public class ListUpdatedEvent{
public final String accountID;
public final FollowList list;
public ListUpdatedEvent(String accountID, FollowList list){
this.accountID=accountID;
this.list=list;
}
}

View File

@@ -0,0 +1,114 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AccountAddedToListEvent;
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
private Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.add_user_to_list_title);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
loadData();
}
@Override
protected void doLoadData(int offset, int count){
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowList> allLists){
if(getActivity()==null)
return;
loadAccountLists(allLists);
}
});
}
private void loadAccountLists(final List<FollowList> allLists){
currentRequest=new GetAccountLists(account.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowList> result){
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
onDataLoaded(allLists.stream()
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
.collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
protected int indexOfItemsAdapter(){
return 1;
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
TextView topText=new TextView(getActivity());
topText.setTextAppearance(R.style.m3_body_medium);
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onItemClick(CheckableListItem<FollowList> item){
boolean add=!item.checked;
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
req.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
item.checked=add;
rebindItem(item);
if(add){
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
}else{
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
}

View File

@@ -0,0 +1,176 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
protected FollowList followList;
protected AvatarPileListItem<Void> membersItem;
protected CheckableListItem<Void> exclusiveItem;
protected FloatingHintEditTextLayout titleEditLayout;
protected EditText titleEdit;
protected Spinner showRepliesSpinner;
private APIRequest<?> getMembersRequest;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
List<ListItem<Void>> items=new ArrayList<>();
if(followList!=null){
items.add(membersItem);
}
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
items.add(exclusiveItem);
onDataLoaded(items);
}
@Override
public void onDestroy(){
super.onDestroy();
if(getMembersRequest!=null)
getMembersRequest.cancel();
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
LinearLayout topView=new LinearLayout(getActivity());
topView.setOrientation(LinearLayout.VERTICAL);
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
titleEdit=titleEditLayout.findViewById(R.id.edit);
titleEdit.setHint(R.string.list_name);
titleEditLayout.updateHint();
if(followList!=null)
titleEdit.setText(followList.title);
topView.addView(titleEditLayout);
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
showRepliesLayout.setHint(R.string.list_show_replies_to);
topView.addView(showRepliesLayout);
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
getString(R.string.list_replies_no_one),
getString(R.string.list_replies_members),
getString(R.string.list_replies_anyone)
));
showRepliesSpinner.setAdapter(spinnerAdapter);
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
case FOLLOWED -> 2;
case LIST -> 1;
case NONE -> 0;
});
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected int indexOfItemsAdapter(){
return 1;
}
protected void doDeleteList(){
new DeleteList(followList.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
AccountSessionManager.get(accountID).getCacheController().deleteList(followList.id);
E.post(new ListDeletedEvent(accountID, followList.id));
Nav.finish(BaseEditListFragment.this);
}
@Override
public void onError(ErrorResponse error){
Activity activity=getActivity();
if(activity==null)
return;
error.showToast(activity);
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
private void onMembersClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
Nav.go(getActivity(), ListMembersFragment.class, args);
}
protected void loadMembers(){
getMembersRequest=new GetListAccounts(followList.id, null, 3)
.setCallback(new Callback<>(){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
getMembersRequest=null;
membersItem.avatars=new ArrayList<>();
for(int i=0;i<Math.min(3, result.size());i++){
Account acc=result.get(i);
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
}
rebindItem(membersItem);
imgLoader.updateImages();
}
@Override
public void onError(ErrorResponse error){
getMembersRequest=null;
}
})
.exec(accountID);
}
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
return switch(showRepliesSpinner.getSelectedItemPosition()){
case 0 -> FollowList.RepliesPolicy.NONE;
case 1 -> FollowList.RepliesPolicy.LIST;
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
};
}
}

View File

@@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -23,23 +24,26 @@ import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AkkomaTranslation;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.OldPostPreReplySheet;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
@@ -63,8 +67,9 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -73,14 +78,9 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -145,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(T s:items){
displayItems.addAll(buildDisplayItems(s));
}
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
}
@Override
@@ -166,6 +167,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
if(notify)
adapter.notifyItemRangeInserted(0, offset);
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
return offset;
}
@@ -222,7 +224,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder;
@Override
@@ -288,6 +290,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void photoViewerDismissed(){
currentPhotoViewer=null;
gridHolder.itemView.setHasTransientState(false);
}
@Override
@@ -299,12 +302,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return gridHolder.getViewController(index);
}
});
gridHolder.itemView.setHasTransientState(true);
}
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
private PreviewlessMediaAttachmentViewController transitioningHolder;
@Override
@@ -574,6 +578,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
i++;
}
// This is a temporary measure to deal with the app crashing when the poll isn't updated.
// This is needed because of a possible id mismatch that screws with things
if(firstOptionIndex==-1 || footerIndex==-1){
for(StatusDisplayItem item:displayItems){
if(status.id.equals(itemID)){
if(item instanceof SpoilerStatusDisplayItem){
spoilerItem=(SpoilerStatusDisplayItem) item;
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
firstOptionIndex=i;
}else if(item instanceof PollFooterStatusDisplayItem){
footerIndex=i;
break;
}
}
i++;
}
}
if(firstOptionIndex==-1 || footerIndex==-1)
throw new IllegalStateException("Can't find all poll items in displayItems");
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
@@ -635,11 +658,30 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
public void onPollViewResultsButtonClick(PollFooterStatusDisplayItem.Holder holder, boolean shown){
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item && item.getItemID().equals(holder.getItemID())){
item.showResults(shown);
int firstOptionIndex=-1, footerIndex=-1;
int i=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(holder.getItemID())){
if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
firstOptionIndex=i;
}else if(item instanceof PollFooterStatusDisplayItem){
footerIndex=i;
break;
}
}
i++;
}
if(firstOptionIndex==-1 || footerIndex==-1)
throw new IllegalStateException("Can't find all poll items in displayItems");
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
for(StatusDisplayItem item:pollItems){
if (item instanceof PollOptionStatusDisplayItem) {
((PollOptionStatusDisplayItem) item).isAnimating=true;
((PollOptionStatusDisplayItem) item).showResults=shown;
adapter.notifyItemRangeChanged(firstOptionIndex, pollItems.size());
}
}
}
protected void submitPollVote(String parentID, String pollID, List<Integer> choices){
@@ -667,6 +709,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
toggleSpoiler(status, isForQuote, holder.getItemID());
}
public void updateStatusWithQuote(DisplayItemsParent parent) {
Pair<Integer, Integer> items=findAllItemsOfParent(parent);
if (items==null)
return;
// Only StatusListFragments/NotificationsListFragments can display status with quotes
assert (this instanceof StatusListFragment) || (this instanceof NotificationsListFragment);
List<StatusDisplayItem> oldItems = displayItems.subList(items.first, items.second+1);
List<StatusDisplayItem> newItems=this.buildDisplayItems((T) parent);
int prevSize=oldItems.size();
oldItems.clear();
displayItems.addAll(items.first, newItems);
// Update the cache
final CacheController cache=AccountSessionManager.get(accountID).getCacheController();
if (parent instanceof Status) {
cache.updateStatus((Status) parent);
} else if (parent instanceof Notification) {
cache.updateNotification((Notification) parent);
}
adapter.notifyItemRangeRemoved(items.first, prevSize);
adapter.notifyItemRangeInserted(items.first, newItems.size());
}
public void removeStatus(DisplayItemsParent parent) {
Pair<Integer, Integer> items=findAllItemsOfParent(parent);
if (items==null)
return;
List<StatusDisplayItem> statusDisplayItems = displayItems.subList(items.first, items.second+1);
int prevSize=statusDisplayItems.size();
statusDisplayItems.clear();
adapter.notifyItemRangeRemoved(items.first, prevSize);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
Status status = holder.getItem().status;
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
@@ -702,6 +780,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.addAll(index+1, spoilerItem.contentItems);
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
}else{
if(spoilers.size()>1 && !isForQuote && status.quote.spoilerRevealed)
toggleSpoiler(status.quote, true, itemID);
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
}
@@ -714,19 +794,33 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
list.invalidateItemDecorations();
}
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable, boolean isForQuote) {
Status s=holder.getItem().status;
if(s.textExpandable!=expandable && list!=null) {
s.textExpandable=expandable;
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(header!=null) header.bindCollapseButton();
List<HeaderStatusDisplayItem.Holder> headers=findAllHoldersOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(headers!=null && !headers.isEmpty()){
HeaderStatusDisplayItem.Holder header=headers.size() > 1 && isForQuote ? headers.get(1) : headers.get(0);
if(header!=null) header.bindCollapseButton();
}
}
}
public void onToggleExpanded(Status status, String itemID) {
public void onToggleExpanded(Status status, boolean isForQuote, String itemID) {
status.textExpanded = !status.textExpanded;
notifyItemChanged(itemID, TextStatusDisplayItem.class);
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
// TODO: simplify this to a single case
if(!isForQuote)
// using the adapter directly to update the item does not work for non-quoted texts
notifyItemChanged(itemID, TextStatusDisplayItem.class);
else{
List<TextStatusDisplayItem.Holder> textItems=findAllHoldersOfType(itemID, TextStatusDisplayItem.Holder.class);
TextStatusDisplayItem.Holder text=textItems.size()>1 ? textItems.get(1) : textItems.get(0);
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
List<HeaderStatusDisplayItem.Holder> headers=findAllHoldersOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if (headers.isEmpty())
return;
HeaderStatusDisplayItem.Holder header=headers.size() > 1 && isForQuote ? headers.get(1) : headers.get(0);
if(header!=null) header.animateExpandToggle();
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
}
@@ -734,12 +828,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
int startPos = warning.getAbsoluteAdapterPosition();
WarningFilteredStatusDisplayItem filterItem=findItemOfType(warning.getItemID(), WarningFilteredStatusDisplayItem.class);
int startPos=displayItems.indexOf(filterItem);
displayItems.remove(startPos);
displayItems.addAll(startPos, warning.filteredItems);
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
if (startPos == 0) scrollToTop();
warning.getItem().status.filterRevealed = true;
list.invalidateItemDecorations();
}
@Override
@@ -756,6 +852,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
protected void loadRelationships(Set<String> ids){
if(ids.isEmpty())
return;
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
if(ids.isEmpty())
return;
// TODO somehow manage these and cancel outstanding requests on refresh
@@ -849,6 +948,23 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return null;
}
@Nullable
protected Pair<Integer, Integer> findAllItemsOfParent(DisplayItemsParent parent){
int startIndex=-1;
int endIndex=-1;
for(int i=0; i<displayItems.size(); i++){
StatusDisplayItem item = displayItems.get(i);
if(item.parentID.equals(parent.getID())) {
startIndex= startIndex==-1 ? i : startIndex;
endIndex=i;
}
}
if(startIndex==-1 || endIndex==-1)
return null;
return Pair.create(startIndex, endIndex);
}
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> List<H> findAllHoldersOfType(String id, Class<H> type){
ArrayList<H> holders=new ArrayList<>();
for(int i=0;i<list.getChildCount();i++){
@@ -1047,6 +1163,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
adapter.notifyDataSetChanged();
}
public void maybeShowPreReplySheet(Status status, Runnable proceed){
Relationship rel=getRelationship(status.account.id);
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
new NonMutualPreReplySheet(getActivity(), notAgain->{
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
proceed.run();
}, status.account, accountID).show();
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
new OldPostPreReplySheet(getActivity(), notAgain->{
if(notAgain)
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
proceed.run();
}, status).show();
}else{
proceed.run();
}
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
@Override

View File

@@ -7,6 +7,9 @@ import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDra
import android.Manifest;
import android.animation.ObjectAnimator;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.DatePickerDialog;
@@ -62,6 +65,7 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.TweakedFileProvider;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
@@ -75,7 +79,7 @@ import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Emoji;
@@ -95,7 +99,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.utils.FileProvider;
import org.joinmastodon.android.utils.Tracking;
import org.joinmastodon.android.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController;
@@ -135,12 +139,14 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID, CustomTransitionsFragment {
private static final int MEDIA_RESULT=717;
public static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -507,7 +513,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
int typeIndex=contentType.ordinal();
contentTypePopup.getMenu().findItem(typeIndex).setChecked(true);
if (contentTypePopup.getMenu().findItem(typeIndex) != null)
contentTypePopup.getMenu().findItem(typeIndex).setChecked(true);
contentTypeBtn.setSelected(typeIndex != ContentType.UNSPECIFIED.ordinal() && typeIndex != ContentType.PLAIN.ordinal());
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
@@ -527,7 +534,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onLaunchAccountSearch(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
}
});
View autocompleteView=autocompleteViewController.getView();
@@ -1169,6 +1176,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void actuallyPublish(boolean preview){
String text=mainEditText.getText().toString();
if(GlobalUserPreferences.removeTrackingParams)
text=Tracking.cleanUrlsInText(text);
CreateStatus.Request req=new CreateStatus.Request();
if("bottom".equals(postLang.encoding)){
text=new StatusTextEncoder(Bottom::encode).encode(text);
@@ -1293,7 +1302,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
.setPositiveButton(R.string.ok, (a, b)->{})
.show();
handlePublishError(null);
publishButton.setEnabled(false);
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
}
if (replyTo == null) updateRecentLanguages();
@@ -1320,7 +1329,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
result.preview=true;
wm.removeView(sendingOverlay);
sendingOverlay=null;
publishButton.setEnabled(true);
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
V.setVisibilityAnimated(sendProgress, View.GONE);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
@@ -1499,7 +1508,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void openCamera() throws IOException {
if (getContext().checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
File photoFile = File.createTempFile("img", ".jpg");
photoUri = FileProvider.getUriForFile(getContext(), getContext().getPackageName() + ".fileprovider", photoFile);
photoUri = UiUtils.getFileProviderUri(getContext(), photoFile);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
@@ -1657,7 +1666,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
}
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P) m.setGroupDividerEnabled(true);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()) m.setGroupDividerEnabled(true);
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
@Override
public boolean onMenuItemClick(MenuItem item){
@@ -1806,8 +1815,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
}
private String sanitizeMediaDescription(String description){
if(description == null){
return null;
}
// The Gboard android keyboard attaches this text whenever the user
// pastes something from the keyboard's suggestion bar.
// Due to different end user locales, the exact text may vary, but at
// least in version 13.4.08, all of the translations contained the
// string "Gboard".
if (description.contains("Gboard")){
return null;
}
return description;
}
@Override
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
description = sanitizeMediaDescription(description);
return mediaViewController.addMediaAttachment(uri, description);
}
@@ -1850,6 +1877,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Editable e=mainEditText.getText();
int start=e.getSpanStart(currentAutocompleteSpan);
int end=e.getSpanEnd(currentAutocompleteSpan);
if(start==-1 || end==-1)
return;
e.replace(start, end, text+" ");
finishAutocomplete();
InputConnection conn=mainEditText.getCurrentInputConnection();
@@ -1918,4 +1947,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
languageButton.setText(opt.language.getLanguageName());
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
}
@Override
public Animator onCreateEnterTransition(View prev, View container){
AnimatorSet anim=new AnimatorSet();
if(getArguments().getBoolean("fromThreadFragment")){
anim.playTogether(
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
);
}else{
anim.playTogether(
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
);
}
anim.setDuration(300);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
return anim;
}
@Override
public Animator onCreateExitTransition(View prev, View container){
AnimatorSet anim=new AnimatorSet();
anim.playTogether(
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
);
anim.setDuration(200);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
return anim;
}
}

View File

@@ -7,10 +7,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -135,20 +132,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text)
.setMessage(msg)
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
@@ -185,7 +171,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);

View File

@@ -0,0 +1,330 @@
package org.joinmastodon.android.fragments;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.views.CurlyArrowEmptyView;
import org.parceler.Parcels;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
private FollowList followList;
private Button nextButton;
private View buttonBar;
private FragmentRootLinearLayout rootView;
private FrameLayout searchFragmentContainer;
private FrameLayout fragmentContentWrap;
private AddNewListMembersFragment searchFragment;
private WindowInsets lastInsets;
private boolean dismissingSearchFragment;
private HashSet<String> accountIDsInList=new HashSet<>();
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.manage_list_members);
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
setLayout(R.layout.fragment_login);
setEmptyText(R.string.list_no_members);
setHasOptionsMenu(true);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
loadData();
}else{
onDataLoaded(List.of());
}
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetListAccounts(followList.id, null, 0)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
for(Account acc:result)
accountIDsInList.add(acc.id);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
}
})
.exec(accountID);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=super.onCreateView(inflater, container, savedInstanceState);
FrameLayout wrapper=new FrameLayout(getActivity());
wrapper.addView(view);
rootView=(FragmentRootLinearLayout) view;
fragmentContentWrap=wrapper;
return wrapper;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setText(R.string.done);
buttonBar=view.findViewById(R.id.button_bar);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
lastInsets=insets;
if(searchFragment!=null)
searchFragment.onApplyWindowInsets(insets);
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
rootView.dispatchApplyWindowInsets(insets);
}
@Override
protected List<View> getViewsForElevationEffect(){
return List.of(getToolbar(), buttonBar);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
MenuItem item=menu.add(R.string.add_list_member);
item.setIcon(R.drawable.ic_fluent_add_24_regular);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(searchFragmentContainer!=null)
return true;
searchFragmentContainer=new FrameLayout(getActivity());
searchFragmentContainer.setId(R.id.search_fragment);
fragmentContentWrap.addView(searchFragmentContainer);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
args.putBoolean("_can_go_back", true);
searchFragment=new AddNewListMembersFragment(this);
searchFragment.setArguments(args);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
if(lastInsets!=null)
searchFragment.onApplyWindowInsets(lastInsets);
searchFragmentContainer.setTranslationX(V.dp(100));
searchFragmentContainer.setAlpha(0f);
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
return true;
}
@Override
protected void initializeEmptyView(View contentView){
ViewStub emptyStub=contentView.findViewById(R.id.empty);
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
super.initializeEmptyView(contentView);
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
emptySecondary.setText(R.string.list_find_users);
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
}
@Override
protected void setStatusBarColor(int color){
rootView.setStatusBarColor(color);
}
@Override
protected void setNavigationBarColor(int color){
rootView.setNavigationBarColor(color);
}
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
dismissingSearchFragment=true;
rootView.setVisibility(View.VISIBLE);
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
fragmentContentWrap.removeView(searchFragmentContainer);
searchFragmentContainer=null;
searchFragment=null;
dismissingSearchFragment=false;
}).start();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}
private void onNextClick(View v){
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
@Override
public boolean isAccountInList(AccountViewModel account){
return accountIDsInList.contains(account.account.id);
}
@Override
public void addAccountToList(AccountViewModel account, Runnable onDone){
new AddAccountsToList(followList.id, Set.of(account.account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
accountIDsInList.add(account.account.id);
if(onDone!=null)
onDone.run();
int i=0;
for(AccountViewModel acc:data){
if(acc.account.id.equals(account.account.id)){
list.getAdapter().notifyItemChanged(i);
return;
}
i++;
}
int pos=data.size();
data.add(account);
list.getAdapter().notifyItemInserted(pos);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
}
@Override
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
accountIDsInList.remove(account.account.id);
if(onDone!=null)
onDone.run();
int i=0;
for(AccountViewModel acc:data){
if(acc.account.id.equals(account.account.id)){
list.getAdapter().notifyItemChanged(i);
return;
}
i++;
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
holder.setOnLongClickListener(vh->false);
Button button=holder.getButton();
button.setPadding(V.dp(24), 0, V.dp(24), 0);
button.setMinimumWidth(0);
button.setMinWidth(0);
button.setOnClickListener(v->{
holder.setActionProgressVisible(true);
holder.itemView.setHasTransientState(true);
Runnable onDone=()->{
holder.setActionProgressVisible(false);
holder.itemView.setHasTransientState(false);
};
AccountViewModel account=holder.getItem();
if(isAccountInList(account)){
removeAccountAccountFromList(account, onDone);
}else{
addAccountToList(account, onDone);
}
});
}
@Override
protected void onBindViewHolder(AccountViewHolder holder){
Button button=holder.getButton();
int textRes, styleRes;
if(isAccountInList(holder.getItem())){
textRes=R.string.remove;
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
}else{
textRes=R.string.add;
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}
button.setText(textRes);
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
button.setTextColor(ta.getColorStateList(0));
ta.recycle();
}
@Override
protected void loadRelationships(List<AccountViewModel> accounts){
// no-op
}
@Override
public Uri getWebUri(Uri.Builder base){
// TODO this
return null;
}
}

View File

@@ -0,0 +1,149 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
import org.joinmastodon.android.events.ListCreatedEvent;
import org.joinmastodon.android.events.ListUpdatedEvent;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class CreateListFragment extends BaseEditListFragment{
private Button nextButton;
private View buttonBar;
private FollowList followList;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.create_list);
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
setLayout(R.layout.fragment_login);
if(savedInstanceState!=null)
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_arrow_drop_down_18;
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setText(R.string.create);
buttonBar=view.findViewById(R.id.button_bar);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected List<View> getViewsForElevationEffect(){
return List.of(getToolbar(), buttonBar);
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putParcelable("list", Parcels.wrap(followList));
}
private void onNextClick(View v){
String title=titleEdit.getText().toString().trim();
if(TextUtils.isEmpty(title)){
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
return;
}
if(followList==null){
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(FollowList result){
followList=result;
proceed(false);
E.post(new ListCreatedEvent(accountID, result));
AccountSessionManager.get(accountID).getCacheController().addList(result);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(FollowList result){
followList=result;
proceed(true);
E.post(new ListUpdatedEvent(accountID, result));
AccountSessionManager.get(accountID).getCacheController().updateList(result);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}else{
proceed(true);
}
}
private void proceed(boolean needLoadMembers){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
args.putBoolean("needLoadMembers", needLoadMembers);
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}
@Subscribe
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
Nav.finish(this);
}
}
}

View File

@@ -7,16 +7,14 @@ import android.view.MenuInflater;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
@@ -46,14 +44,14 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, null, count, null, getLocalPrefs().timelineReplyVisibility)
.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, FilterContext.PUBLIC)).collect(Collectors.toList());
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
result.stream().forEach(status -> {
status.account.acct += "@"+domain;
status.mentions.forEach(mention -> mention.id = null);
@@ -82,12 +80,15 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
@Override
protected FilterContext getFilterContext() {
return null;
return FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(domain);
return new Uri.Builder()
.scheme("https")
.authority(domain)
.build();
}
@Override

View File

@@ -0,0 +1,67 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListUpdatedEvent;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class EditListFragment extends BaseEditListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.edit_list);
loadMembers();
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
menu.add(R.string.delete_list);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.delete_list)
.setMessage(getString(R.string.delete_list_confirm, followList.title))
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
.setNegativeButton(R.string.cancel, null)
.show();
return true;
}
@Override
public void onDestroy(){
super.onDestroy();
String newTitle=titleEdit.getText().toString();
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
boolean newExclusive=exclusiveItem.checked;
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
.setCallback(new Callback<>(){
@Override
public void onSuccess(FollowList result){
AccountSessionManager.get(accountID).getCacheController().updateList(result);
E.post(new ListUpdatedEvent(accountID, result));
}
@Override
public void onError(ErrorResponse error){
// TODO handle errors somehow
}
})
.exec(accountID);
}
}
}

View File

@@ -41,17 +41,14 @@ import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CustomLocalTimeline;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
import java.util.ArrayList;
import java.util.Collections;
@@ -74,7 +71,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
private Menu optionsMenu;
private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
private final List<ListTimeline> listTimelines=new ArrayList<>();
private final List<FollowList> followLists =new ArrayList<>();
private final List<Hashtag> hashtags=new ArrayList<>();
private MenuItem addHashtagItem;
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
@@ -94,8 +91,8 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
new GetLists().setCallback(new Callback<>(){
@Override
public void onSuccess(List<ListTimeline> result){
listTimelines.addAll(result);
public void onSuccess(List<FollowList> result){
followLists.addAll(result);
updateOptionsMenu();
}
@@ -224,7 +221,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
makeBackItem(hashtagsMenu);
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
followLists.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
@@ -399,7 +396,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
tl.setTitle(name);
if(item == null || item.getType()==TimelineDefinition.TimelineType.HASHTAG){
tl.setTagOptions(
mainHashtag,
TextUtils.isEmpty(mainHashtag) ? name : mainHashtag,
tagsAny.getChipValues(),
tagsAll.getChipValues(),
tagsNone.getChipValues(),

View File

@@ -297,8 +297,8 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
cover.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-2, image);
name.invalidate();
bio.invalidate();
name.setText(name.getText());
bio.setText(bio.getText());
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
@@ -319,7 +319,18 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, (Boolean visible) -> {
if(v==acceptButton){
acceptButton.setTextVisible(!visible);
acceptProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
acceptButton.setClickable(!visible);
}else{
rejectButton.setTextVisible(!visible);
rejectProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
rejectButton.setClickable(!visible);
}
itemView.setHasTransientState(false);
}, rel -> {
if(getContext()==null) return;
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);

View File

@@ -32,6 +32,8 @@ import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
@@ -63,6 +65,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
private MenuItem followMenuItem, pinMenuItem, muteMenuItem;
private boolean followRequestRunning;
private boolean toolbarContentVisible;
private String maxID;
private List<String> any;
private List<String> all;
@@ -98,7 +101,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
}
private void updateMuteState(boolean newMute) {
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtagName));
muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
}
@@ -229,7 +232,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtag).build();
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtagName).build();
}
@Override

View File

@@ -5,8 +5,6 @@ import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.graphics.drawable.RippleDrawable;
import android.content.Intent;
import android.graphics.Outline;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
@@ -37,7 +35,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
@@ -163,6 +161,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
notificationsBadge.setVisibility(View.GONE);
tabBar.selectTab(currentTab);
if(savedInstanceState==null){
getChildFragmentManager().beginTransaction()
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
@@ -184,7 +184,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
});
}
}
tabBar.selectTab(currentTab);
return content;
}
@@ -312,11 +311,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
discoverFragment.openSearch();
return true;
}
if(tab==R.id.tab_home){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
}
return false;
}

View File

@@ -53,7 +53,7 @@ import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -95,7 +95,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private ImageView collapsedChevron;
private TextView timelineTitle;
private PopupMenu switcherPopup;
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
private final Map<Integer, FollowList> listItems = new HashMap<>();
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
private List<TimelineDefinition> timelinesList;
private int count;
@@ -270,7 +270,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
new GetLists().setCallback(new Callback<>() {
@Override
public void onSuccess(List<ListTimeline> lists) {
public void onSuccess(List<FollowList> lists) {
updateList(lists, listItems);
}
@@ -408,7 +408,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
addListsToOverflowMenu();
addHashtagsToOverflowMenu();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic())
m.setGroupDividerEnabled(true);
}
@@ -512,7 +512,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
Bundle args=new Bundle();
args.putString("account", accountID);
int id = item.getItemId();
ListTimeline list;
FollowList list;
Hashtag hashtag;
if (item.getItemId() == R.id.menu_back) {
@@ -701,13 +701,13 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
handleListEvent(listItems, l -> l.id.equals(event.id), false, null);
handleListEvent(listItems, l -> l.id.equals(event.listID), false, null);
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
ListTimeline list = new ListTimeline();
FollowList list = new FollowList();
list.id = event.id;
list.title = event.title;
list.repliesPolicy = event.repliesPolicy;

View File

@@ -0,0 +1,324 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.widget.ImageButton;
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.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.AccountAddedToListEvent;
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.ActionModeHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.V;
public class ListMembersFragment extends PaginatedAccountListFragment{
private static final int ADD_MEMBER_RESULT=600;
private ImageButton fab;
private FollowList followList;
private boolean inSelectionMode;
private Set<String> selectedAccounts=new HashSet<>();
private ActionMode actionMode;
private MenuItem deleteItem;
public ListMembersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
setTitle(R.string.list_members);
setHasOptionsMenu(true);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetListAccounts(followList.id, maxID, count);
}
@Override
protected MastodonAPIRequest loadRemoteInfo(){
return null;
}
@Override
public Object getCurrentInfo(){
return null;
}
@Override
public String getRemoteDomain(){
return null;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
holder.setOnClickListener(this::onItemClick);
holder.setOnLongClickListener(this::onItemLongClick);
holder.getContextMenu().getMenu().add(0, R.id.remove_from_list, 0, R.string.remove_from_list);
holder.setOnCustomMenuItemSelectedListener(item->onItemMenuItemSelected(holder, item));
}
@Override
protected void onBindViewHolder(AccountViewHolder holder){
super.onBindViewHolder(holder);
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
if(inSelectionMode){
holder.setChecked(selectedAccounts.contains(holder.getItem().account.id));
}
}
@Override
public boolean wantsLightStatusBar(){
if(actionMode!=null)
return UiUtils.isDarkTheme();
return super.wantsLightStatusBar();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.selectable_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.select){
enterSelectionMode();
}else if(id==R.id.select_all){
for(AccountViewModel a:(ArrayList<AccountViewModel>)data){
selectedAccounts.add(a.account.id);
}
enterSelectionMode();
}
return true;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
fab.setContentDescription(getString(R.string.add_list_member));
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
UiUtils.applyBottomInsetToFAB(fab, insets);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==ADD_MEMBER_RESULT && success){
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
addAccounts(List.of(acc));
}
}
@Subscribe
public void onAccountRemovedFromList(AccountRemovedFromListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
removeAccountRows(Set.of(ev.targetAccountID));
}
}
@Subscribe
public void onAccountAddedToList(AccountAddedToListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
data.add(new AccountViewModel(ev.account, accountID));
list.getAdapter().notifyItemInserted(data.size()-1);
}
}
private void onFabClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
}
private void onItemClick(AccountViewHolder holder){
if(inSelectionMode){
String id=holder.getItem().account.id;
if(selectedAccounts.contains(id)){
selectedAccounts.remove(id);
holder.setChecked(false);
}else{
selectedAccounts.add(id);
holder.setChecked(true);
}
updateActionModeTitle();
deleteItem.setEnabled(!selectedAccounts.isEmpty());
return;
}
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(holder.getItem().account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private boolean onItemLongClick(AccountViewHolder holder){
if(inSelectionMode)
return false;
selectedAccounts.add(holder.getItem().account.id);
enterSelectionMode();
return true;
}
private void onItemMenuItemSelected(AccountViewHolder holder, MenuItem item){
int id=item.getItemId();
if(id==R.id.remove_from_list){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_member)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
.setNegativeButton(R.string.cancel, null)
.show();
}
}
private void updateItemsForSelectionModeTransition(){
list.getAdapter().notifyItemRangeChanged(0, data.size());
}
private void enterSelectionMode(){
inSelectionMode=true;
updateItemsForSelectionModeTransition();
V.setVisibilityAnimated(fab, View.INVISIBLE);
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
deleteItem=menu.findItem(R.id.delete);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_members)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
.setNegativeButton(R.string.cancel, null)
.show();
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode){
actionMode=null;
inSelectionMode=false;
selectedAccounts.clear();
updateItemsForSelectionModeTransition();
V.setVisibilityAnimated(fab, View.VISIBLE);
}
});
updateActionModeTitle();
}
private void updateActionModeTitle(){
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
}
private void removeAccounts(Set<String> ids){
new RemoveAccountsFromList(followList.id, ids)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
if(inSelectionMode)
actionMode.finish();
removeAccountRows(ids);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
private void addAccounts(Collection<Account> accounts){
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
for(Account acc:accounts){
data.add(new AccountViewModel(acc, accountID));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
private void removeAccountRows(Set<String> ids){
for(int i=data.size()-1;i>=0;i--){
if(ids.contains(((ArrayList<AccountViewModel>)data).get(i).account.id)){
data.remove(i);
list.getAdapter().notifyItemRemoved(i);
}
}
}
@Override
public Uri getWebUri(Uri.Builder base){
return null;
}
}

View File

@@ -20,7 +20,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -39,7 +39,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
private String listID;
private String listTitle;
@Nullable
private ListTimeline.RepliesPolicy repliesPolicy;
private FollowList.RepliesPolicy repliesPolicy;
private boolean exclusive;
@Override
@@ -54,19 +54,19 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
listID = args.getString("listID");
listTitle = args.getString("listTitle");
exclusive = args.getBoolean("listIsExclusive");
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
repliesPolicy = FollowList.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
setTitle(listTitle);
setHasOptionsMenu(true);
new GetList(listID).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline listTimeline) {
public void onSuccess(FollowList followList) {
if(getActivity()==null) return;
// TODO: save updated info
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
repliesPolicy = listTimeline.repliesPolicy;
if (!followList.title.equals(listTitle)) setTitle(followList.title);
if (followList.repliesPolicy != null && !followList.repliesPolicy.equals(repliesPolicy)) {
repliesPolicy = followList.repliesPolicy;
}
}
@@ -97,9 +97,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
.setPositiveButton(R.string.save, (d, which) -> {
String newTitle = editor.getTitle().trim();
setTitle(newTitle);
new UpdateList(listID, newTitle, editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
new UpdateList(listID, newTitle, editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
public void onSuccess(FollowList list) {
if(getActivity()==null) return;
setTitle(list.title);
listTitle = list.title;
@@ -119,7 +119,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
.show();
} else if (item.getItemId() == R.id.delete) {
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
E.post(new ListDeletedEvent(listID));
E.post(new ListDeletedEvent(accountID, listID));
Nav.finish(this);
});
}

View File

@@ -18,13 +18,14 @@ 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.ResultlessMastodonAPIRequest;
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.model.FollowList;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.ListEditor;
@@ -42,7 +43,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
public class ListsFragment extends MastodonRecyclerFragment<FollowList> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
@@ -97,9 +98,9 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
new CreateList(editor.getTitle(), editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
public void onSuccess(FollowList list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
@@ -120,10 +121,10 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
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);
ResultlessMastodonAPIRequest req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {}
public void onSuccess(Void o) {}
@Override
public void onError(ErrorResponse error) {
@@ -139,19 +140,19 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
public void onSuccess(List<FollowList> lists) {
if(getActivity()==null) return;
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
for (FollowList 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) {
public void onSuccess(List<FollowList> allLists) {
if(getActivity()==null) return;
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
List<FollowList> newLists = new ArrayList<>();
for (FollowList 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);
@@ -169,8 +170,8 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
@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)) {
FollowList item = data.get(i);
if (item.id.equals(event.listID)) {
data.remove(i);
adapter.notifyItemRemoved(i);
break;
@@ -181,7 +182,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
FollowList item = data.get(i);
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
@@ -230,7 +231,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private class ListViewHolder extends BindableViewHolder<FollowList> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
@@ -241,7 +242,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
}
@Override
public void onBind(ListTimeline item) {
public void onBind(FollowList item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular

View File

@@ -0,0 +1,95 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class ManageFollowedHashtagsFragment extends BaseSettingsFragment<Hashtag> implements ListItemWithOptionsMenu.OptionsMenuListener<Hashtag>{
private String maxID;
public ManageFollowedHashtagsFragment(){
super(100);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.manage_hashtags);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetFollowedTags(offset>0 ? maxID : null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result){
maxID=null;
if(result.nextPageUri!=null)
maxID=result.nextPageUri.getQueryParameter("max_id");
onDataLoaded(result.stream().map(t->{
int posts=t.getWeekPosts();
return new ListItemWithOptionsMenu<>(t.name, getResources().getQuantityString(R.plurals.x_posts_recently, posts, posts), ManageFollowedHashtagsFragment.this,
R.drawable.ic_fluent_tag_24_regular, ManageFollowedHashtagsFragment.this::onItemClick, t, false);
}).collect(Collectors.toList()), maxID!=null);
}
})
.exec(accountID);
}
@Override
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<Hashtag> item, Menu menu){
menu.clear();
menu.add(getString(R.string.unfollow_user, "#"+item.parentObject.name));
}
@Override
public void onListItemOptionSelected(ListItemWithOptionsMenu<Hashtag> item, MenuItem menuItem){
new M3AlertDialogBuilder(getActivity())
.setTitle(getString(R.string.unfollow_confirmation, "#"+item.parentObject.name))
.setPositiveButton(R.string.unfollow, (dlg, which)->doUnfollow(item))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onItemClick(ListItemWithOptionsMenu<Hashtag> item){
UiUtils.openHashtagTimeline(getActivity(), accountID, item.parentObject);
}
private void doUnfollow(ListItemWithOptionsMenu<Hashtag> item){
new SetTagFollowed(item.parentObject.name, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag result){
int index=data.indexOf(item);
if(index==-1)
return;
data.remove(index);
list.getAdapter().notifyItemRemoved(index);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
}

View File

@@ -0,0 +1,199 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListCreatedEvent;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedEvent;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
private ImageButton fab;
public ManageListsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.manage_lists);
loadData();
setRefreshEnabled(true);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
protected void doLoadData(int offset, int count){
Callback<List<FollowList>> callback=new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowList> result){
onDataLoaded(result.stream().map(ManageListsFragment.this::makeItem).collect(Collectors.toList()), false);
}
};
if(refreshing){
AccountSessionManager.get(accountID)
.getCacheController()
.reloadLists(callback);
}else{
AccountSessionManager.get(accountID)
.getCacheController()
.getLists(callback);
}
}
private ListItem<FollowList> makeItem(FollowList l){
return new ListItemWithOptionsMenu<>(l.title, null, ManageListsFragment.this, R.drawable.ic_list_alt_24px, ManageListsFragment.this::onListClick, l, false);
}
private void onListClick(ListItemWithOptionsMenu<FollowList> item){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(item.parentObject));
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
@Override
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<FollowList> item, Menu menu){
menu.add(0, R.id.edit, 0, R.string.edit_list);
menu.add(0, R.id.delete, 1, R.string.delete_list);
}
@Override
public void onListItemOptionSelected(ListItemWithOptionsMenu<FollowList> item, MenuItem menuItem){
int id=menuItem.getItemId();
if(id==R.id.edit){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(item.parentObject));
Nav.go(getActivity(), EditListFragment.class, args);
}else if(id==R.id.delete){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.delete_list)
.setMessage(getString(R.string.delete_list_confirm, item.parentObject.title))
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList(item.parentObject))
.setNegativeButton(R.string.cancel, null)
.show();
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
fab.setContentDescription(getString(R.string.create_list));
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
UiUtils.applyBottomInsetToFAB(fab, insets);
}
private void doDeleteList(FollowList list){
new DeleteList(list.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
for(int i=0;i<data.size();i++){
if(data.get(i).parentObject==list){
data.remove(i);
itemsAdapter.notifyItemRemoved(i);
AccountSessionManager.get(accountID).getCacheController().deleteList(list.id);
break;
}
}
}
@Override
public void onError(ErrorResponse error){
Activity activity=getActivity();
if(activity==null)
return;
error.showToast(activity);
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
@Subscribe
public void onListUpdated(ListUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
for(ListItem<FollowList> item:data){
if(item.parentObject.id.equals(ev.list.id)){
item.parentObject=ev.list;
item.title=ev.list.title;
rebindItem(item);
break;
}
}
}
@Subscribe
public void onListDeleted(ListDeletedEvent ev){
if(!ev.accountID.equals(accountID))
return;
int i=0;
for(ListItem<FollowList> item:data){
if(item.parentObject.id.equals(ev.listID)){
data.remove(i);
itemsAdapter.notifyItemRemoved(i);
break;
}
i++;
}
}
@Subscribe
public void onListCreated(ListCreatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
ListItem<FollowList> item=makeItem(ev.list);
data.add(item);
((List<ListItem<FollowList>>)data).sort(Comparator.comparing(l->l.parentObject.title));
itemsAdapter.notifyItemInserted(data.indexOf(item));
}
private void onFabClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), CreateListFragment.class, args);
}
}

View File

@@ -7,6 +7,7 @@ import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;

View File

@@ -7,6 +7,10 @@ import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -14,6 +18,7 @@ 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.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
@@ -47,12 +52,13 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private boolean onlyMentions;
private boolean onlyPosts;
private String maxID;
private boolean reloadingFromCache;
private DiscoverInfoBannerHelper bannerHelper;
private String unreadMarker, realUnreadMarker;
private MenuItem markAllReadItem;
@Override
protected boolean wantsComposeButton() {
@@ -63,13 +69,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
if(savedInstanceState!=null){
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
}
if (onlyPosts) {
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
}
onlyMentions=getArguments().getBoolean("onlyMentions", false);
setHasOptionsMenu(true);
}
@Override
@@ -81,14 +82,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
onlyMentions=getArguments().getBoolean("onlyMentions", false);
onlyPosts=getArguments().getBoolean("onlyPosts", false);
setTitle(R.string.notifications);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
if(!onlyMentions && !onlyPosts){
if(!onlyMentions){
switch(n.type){
case MENTION -> {
if(!getLocalPrefs().notificationFilters.mention)
@@ -164,7 +163,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, false, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
if(getActivity()==null)
@@ -186,7 +185,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
});
}
@Override
protected void onRelationshipsLoaded(){
if(getActivity()==null)
@@ -201,13 +200,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
protected void onShown(){
super.onShown();
if(!dataLoading){
if(onlyMentions){
refresh();
}else{
reloadingFromCache=true;
refresh();
}
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
if(!dataLoading && canRefreshWithoutUpsettingUser()){
reloadingFromCache=true;
refresh();
}
}
@@ -268,13 +264,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putBoolean("onlyMentions", onlyMentions);
outState.putBoolean("onlyPosts", onlyPosts);
}
private Notification getNotificationByID(String id){
for(Notification n:data){
if(n.id.equals(id))
@@ -380,11 +369,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
markAllReadItem=menu.findItem(R.id.mark_all_read);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.mark_all_read){
markAsRead();
resetUnreadBackground();
}
return true;
}
private void markAsRead(){
if(data.isEmpty())
return;
String id=data.get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
}
}
void resetUnreadBackground(){
if (getParentFragment() instanceof NotificationsFragment nf) {
nf.unreadMarker=nf.realUnreadMarker;
@@ -396,13 +412,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onRefresh(){
super.onRefresh();
if (getParentFragment() instanceof NotificationsFragment nf) {
if (!onlyMentions && !onlyPosts) nf.markAsRead();
if (!onlyMentions) nf.markAsRead();
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
nf.unreadMarker=nf.realUnreadMarker=m;
nf.updateMarkAllReadButton();
});
}
resetUnreadBackground();
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
unreadMarker=realUnreadMarker=m;
});
}
private void updateMarkAllReadButton(){
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
}
@Override
@@ -420,4 +443,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
? "/users/" + getSession().self.username + "/interactions"
: "/notifications").build();
}
private boolean canRefreshWithoutUpsettingUser(){
// TODO maybe reload notifications the same way we reload the home timelines, i.e. with gaps and stuff
if(data.size()<=itemsPerPage)
return true;
for(int i=list.getChildCount()-1;i>=0;i--){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
String id=itemHolder.getItemID();
for(int j=0;j<data.size();j++){
if(data.get(j).id.equals(id))
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
}
}
}
return true;
}
}

View File

@@ -8,6 +8,8 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -21,12 +23,9 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.text.TextWatcher;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
@@ -43,8 +42,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;
import android.widget.ImageButton;
@@ -72,11 +69,9 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment;
import org.joinmastodon.android.fragments.account_list.BlocksListFragment;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment;
import org.joinmastodon.android.fragments.account_list.MutesListFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
import org.joinmastodon.android.model.Account;
@@ -90,6 +85,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
@@ -143,7 +139,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
private ImageView lockIcon, botIcon;
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
@@ -154,7 +150,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private SwipeRefreshLayout refreshLayout;
private View followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress, notifyProgress, noteSaveProgress;
private ProgressBar actionProgress, notifyProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
@@ -197,7 +193,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
// profile note
private FrameLayout noteWrap;
private ImageButton noteSaveBtn;
private EditText noteEdit;
@Override
@@ -220,7 +215,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(!isOwnProfile)
loadRelationship();
else if (isInstanceAkkoma()) {
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
maxFields = (int) getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
}
}else{
profileAccountID=getArguments().getString("profileAccountID");
@@ -250,6 +245,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
name=content.findViewById(R.id.name);
usernameWrap=content.findViewById(R.id.username_wrap);
username=content.findViewById(R.id.username);
usernameDomain=content.findViewById(R.id.username_domain);
lockIcon=content.findViewById(R.id.lock_icon);
botIcon=content.findViewById(R.id.bot_icon);
bio=content.findViewById(R.id.bio);
@@ -271,7 +267,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
actionProgress=content.findViewById(R.id.action_progress);
notifyProgress=content.findViewById(R.id.notify_progress);
noteSaveProgress=content.findViewById(R.id.note_save_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
countersLayout=content.findViewById(R.id.profile_counters);
@@ -288,46 +283,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
noteEdit=content.findViewById(R.id.note_edit);
noteWrap=content.findViewById(R.id.note_edit_wrap);
noteSaveBtn=content.findViewById(R.id.note_save_btn);
noteSaveBtn.setOnClickListener((v->{
savePrivateNote(noteEdit.getText().toString());
InputMethodManager imm=(InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
noteEdit.clearFocus();
noteSaveBtn.clearFocus();
}));
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus){
hideFab();
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
}else if(!noteSaveBtn.hasFocus()){
}else{
showFab();
hideNoteSaveBtnIfNotDirty();
}
});
noteEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
if(relationship!=null && noteSaveBtn.getVisibility()!=View.VISIBLE && !s.toString().equals(relationship.note))
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
}
@Override
public void afterTextChanged(Editable s){}
});
noteSaveBtn.setOnFocusChangeListener((v, hasFocus)->{
if(!hasFocus && !noteEdit.hasFocus()){
showFab();
hideNoteSaveBtnIfNotDirty();
savePrivateNote(noteEdit.getText().toString());
}
});
@@ -435,28 +398,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
content.findViewById(R.id.username_wrap).setOnClickListener(v->{
try {
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
Bundle args = new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
args.putString("account", accountID);
Nav.go(getActivity(), SettingsServerFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getContext());
}
})
.wrapProgress((Activity) getContext(), R.string.loading, true)
.execRemote(Uri.parse(account.url).getHost());
} catch (NullPointerException ignored) {
// maybe the url was malformed?
Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show();
}
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
});
content.findViewById(R.id.username_wrap).setOnLongClickListener(v->{
@@ -464,7 +406,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(!usernameString.contains("@")){
usernameString+="@"+domain;
}
UiUtils.copyText(username, '@'+usernameString);
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+usernameString));
UiUtils.maybeShowTextCopiedToast(getActivity());
return true;
});
@@ -492,13 +435,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
return sizeWrapper;
}
private void hideNoteSaveBtnIfNotDirty(){
if(noteEdit.getText().toString().equals(relationship.note)){
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
}
// qrCodeButton.setOnClickListener(v->{
// Bundle args=new Bundle();
// args.putString("account", accountID);
// args.putParcelable("targetAccount", Parcels.wrap(account));
// ProfileQrCodeFragment qf=new ProfileQrCodeFragment();
// qf.setArguments(args);
// qf.show(getChildFragmentManager(), "qrDialog");
// });
return sizeWrapper;
}
private void showPrivateNote(){
@@ -517,8 +464,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
return;
}
V.setVisibilityAnimated(noteSaveProgress, View.VISIBLE);
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship result) {
@@ -529,8 +474,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
}
}).exec(accountID);
}
@@ -693,6 +636,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
getChildFragmentManager().putFragment(outState, "pinnedPosts", pinnedPostsFragment);
}
@Override
public void onHidden(){
if (relationship != null && !noteEdit.getText().toString().equals(relationship.note)){
savePrivateNote(noteEdit.getText().toString());
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
@@ -755,13 +705,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
// boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
String acct = ((isSelf || account.isRemote)
? account.getFullyQualifiedName()
: account.acct);
// String acct = ((isSelf || account.isRemote)
// ? account.getFullyQualifiedName()
// : account.acct);
username.setText('@'+acct);
username.setText("@"+account.username);
String domain=account.getDomain();
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
lockIcon.setVisibility(account.locked ? View.VISIBLE : View.GONE);
lockIcon.setImageTintList(ColorStateList.valueOf(username.getCurrentTextColor()));
@@ -780,7 +735,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
if (account.followersCount < 0 || account.followingCount < 0)
@@ -812,7 +767,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
for(AccountField field:account.fields){
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
@@ -857,23 +812,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
if(isOwnProfile){
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.scheduled, R.id.bookmarks, R.id.favorites);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.scheduled, R.id.bookmarks);
}else{
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags, R.id.favorites, R.id.scheduled);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.edit_note);
}
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
openWithAccounts.setVisible(hasMultipleAccounts);
SubMenu accountsMenu=openWithAccounts.getSubMenu();
if(hasMultipleAccounts){
accountsMenu.clear();
UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL(
getActivity(), s.getID(), account.url, false
));
}
menu.findItem(R.id.open_with_account).setVisible(hasMultipleAccounts);
if(isOwnProfile) {
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
menu.findItem(R.id.favorites).setIcon(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular);
UiUtils.insetPopupMenuIcon(getContext(), menu.findItem(R.id.favorites));
return;
}
@@ -902,18 +851,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else{
blockDomain.setVisible(false);
}
menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty())
? R.string.sk_add_note : R.string.sk_delete_note);
boolean canAddNote = noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty());
menu.findItem(R.id.edit_note).setTitle(canAddNote ? R.string.sk_add_note : R.string.sk_delete_note);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
UiUtils.openSystemShareSheet(getActivity(), account);
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){
@@ -994,8 +940,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.showSoftInput(noteEdit, 0);
}, 100);
}else if(relationship.note.isEmpty()){
}else if(relationship.note.isEmpty() && noteEdit.getText().toString().isEmpty()){
hidePrivateNote();
noteEdit.clearFocus();
noteEdit.postDelayed(()->{
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(noteEdit.getWindowToken(), 0);
}, 100);
UiUtils.beginLayoutTransition(scrollableContent);
}else{
new M3AlertDialogBuilder(getActivity())
@@ -1005,6 +956,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
.show();
}
invalidateOptionsMenu();
}else if(id==R.id.open_with_account){
UiUtils.pickAccount(getActivity(), accountID, R.string.sk_open_with_account, R.drawable.ic_fluent_person_swap_24_regular, session ->UiUtils.openURL(
getActivity(), session.getID(), account.url, false
), null);
}
return true;
}
@@ -1038,13 +993,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
noteSaveProgress.setIndeterminateTintList(noteEdit.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying);
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
UiUtils.beginLayoutTransition(scrollableContent);
}
@@ -1399,7 +1351,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() : account.avatar, ava), 0,
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
@@ -1411,7 +1363,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(drawable==null || drawable instanceof ColorDrawable)
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}
@@ -1635,8 +1587,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public void setImage(int index, Drawable image){
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
span.setDrawable(image);
title.invalidate();
value.invalidate();
title.setText(title.getText());
value.setText(value.getText());
toolbarTitleView.setText(toolbarTitleView.getText());
}
@Override

View File

@@ -85,7 +85,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, null,
return StatusDisplayItem.buildItems(this, s.toFormattedStatus(accountID), accountID, s, knownAccounts, null,
StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS |
StatusDisplayItem.FLAG_NO_FOOTER |
StatusDisplayItem.FLAG_NO_TRANSLATE);

View File

@@ -1,7 +1,12 @@
package org.joinmastodon.android.fragments;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
@@ -20,6 +27,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
@@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer, loadedDefaultServer;
private Uri currentInviteLink;
private ProgressDialog instanceLoadingProgress;
private String inviteCode;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
Bundle extras=new Bundle();
boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup);
extras.putString("defaultServer", chosenDefaultServer);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
}
private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer)
return;
instanceLoadingProgress=new ProgressDialog(getActivity());
instanceLoadingProgress.setCancelable(false);
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
instanceLoadingProgress.show();
if(currentInviteLink!=null){
new CheckInviteLink(currentInviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCode=result.inviteCode;
proceedWithServerDomain(currentInviteLink.getHost());
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.execNoAuth(currentInviteLink.getHost());
return;
}
proceedWithServerDomain(chosenDefaultServer);
}
private void proceedWithServerDomain(String domain){
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
if(!result.registrations){
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
@@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
if(inviteCode!=null)
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
@@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(chosenDefaultServer);
.execNoAuth(domain);
}
private void onLearnMoreClick(View v){
@@ -198,7 +258,18 @@ public class SplashFragment extends AppKitFragment{
}
private void loadAndChooseDefaultServer(){
loadingDefaultServer=true;
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
currentInviteLink=Uri.parse(clipText.toString());
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
}
}else{
loadingDefaultServer=true;
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){
@Override
@@ -241,7 +312,7 @@ public class SplashFragment extends AppKitFragment{
chosenDefaultServer=domain;
loadingDefaultServer=false;
loadedDefaultServer=true;
if(defaultServerButton!=null && getActivity()!=null){
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));

View File

@@ -3,8 +3,8 @@ 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.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.FilterContext;
@@ -13,7 +13,6 @@ import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId;
@@ -26,6 +25,8 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import name.fraser.neil.plaintext.diff_match_patch;
@@ -54,12 +55,47 @@ public class StatusEditHistoryFragment extends StatusListFragment{
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
if(result.size()<=1&& GlobalUserPreferences.allowRemoteLoading) {
//server send only a single edit, which is always the original status
//try to get the complete history from the remote server
loadRemoteData(result);
return;
}
onDataLoaded(result, false);
}
})
.exec(accountID);
}
void loadRemoteData(List<Status> prevData){
String remoteURL = Uri.parse(url).getHost();
String[] parts=url.split("/");
if(parts.length==0||remoteURL==null) {
onDataLoaded(prevData, false);
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint));
return;
}
new GetStatusEditHistory(parts[parts.length-1])
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
onDataLoaded(result, false);
}
@Override
public void onError(ErrorResponse errorResponse){
//fallback to previously loaded data
onDataLoaded(prevData, false);
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint));
}
})
.execNoAuth(remoteURL);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=new ArrayList<>();

View File

@@ -61,7 +61,9 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
flags |= StatusDisplayItem.FLAG_NO_TRANSLATE;
if(!GlobalUserPreferences.showMediaPreview)
flags |= StatusDisplayItem.FLAG_NO_MEDIA_PREVIEW;
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags);
/* MOSHIDON: we make the filterContext null in the main status in the thread fragment, so that the main status is never filtered (because you just clicked on it).
This also restores old behavior that got lost to time and changes in the filter system */
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, isMainThreadStatus ? null : getFilterContext(), isMainThreadStatus ? 0 : flags);
}
protected abstract FilterContext getFilterContext();

View File

@@ -2,11 +2,19 @@ package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
@@ -23,6 +31,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
@@ -50,7 +59,13 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
@@ -58,10 +73,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
private StatusContext result;
protected boolean contextInitiallyRendered, transitionFinished, preview;
private FrameLayout replyContainer;
private LinearLayout replyButton;
private ImageView replyButtonAva;
private TextView replyButtonText;
private int lastBottomInset;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setLayout(R.layout.fragment_thread);
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
@@ -143,10 +164,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}
}
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
itemsToModify.add(itemsToModify.size()-1, new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
}
return items;
}
@@ -207,12 +228,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.sensitiveRevealed = oldStatus.sensitiveRevealed;
s.filterRevealed = oldStatus.filterRevealed;
s.textExpanded = oldStatus.textExpanded;
}
if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText)) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = mainStatus.spoilerRevealed;
s.spoilerText != null){
if (s.spoilerText.equals(mainStatus.spoilerText) ||
(s.spoilerText.toLowerCase().startsWith("re: ") &&
s.spoilerText.substring(4).equals(mainStatus.spoilerText))){
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = mainStatus.spoilerRevealed;
}
}
}
}
@@ -283,6 +308,13 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
updatedStatus.filterRevealed = mainStatus.filterRevealed;
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
updatedStatus.sensitiveRevealed = mainStatus.sensitiveRevealed;
updatedStatus.textExpanded = mainStatus.textExpanded;
if(updatedStatus.quote!=null && mainStatus.quote!=null){
updatedStatus.quote.filterRevealed = mainStatus.quote.filterRevealed;
updatedStatus.quote.spoilerRevealed = mainStatus.quote.spoilerRevealed;
updatedStatus.quote.sensitiveRevealed = mainStatus.quote.sensitiveRevealed;
updatedStatus.quote.textExpanded = mainStatus.quote.textExpanded;
}
// returning fired event object to facilitate testing
Object event;
@@ -387,6 +419,22 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
replyContainer=view.findViewById(R.id.reply_button_wrapper);
replyButton=replyContainer.findViewById(R.id.reply_button);
replyButtonText=replyButton.findViewById(R.id.reply_btn_text);
replyButtonAva=replyButton.findViewById(R.id.avatar);
replyButton.setOutlineProvider(OutlineProviders.roundedRect(20));
replyButton.setClipToOutline(true);
replyButtonText.setText(HtmlParser.parseCustomEmoji(getString(R.string.reply_to_user, mainStatus.account.displayName), mainStatus.account.emojis));
UiUtils.loadCustomEmojiInTextView(replyButtonText);
replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
replyButtonAva.setClipToOutline(true);
replyButton.setOnClickListener(v->openReply(mainStatus, accountID));
replyButton.setOnLongClickListener(this::onReplyLongClick);
Account self=AccountSessionManager.get(accountID).self;
if(!TextUtils.isEmpty(self.avatar)){
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
}
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
showContent();
if(!loaded)
@@ -526,4 +574,36 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}
super.onErrorRetryClick();
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
lastBottomInset=insets.getSystemWindowInsetBottom();
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
}
private void openReply(Status status, String accountID){
maybeShowPreReplySheet(status, ()->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("replyTo", Parcels.wrap(status));
args.putBoolean("fromThreadFragment", true);
Nav.go(getActivity(), ComposeFragment.class, args);
});
}
private boolean onReplyLongClick(View v) {
if(mainStatus.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickAccount(v.getContext(), accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
String pickedAccountID = session.getID();
UiUtils.lookupStatus(v.getContext(), mainStatus, pickedAccountID, accountID, status -> {
if (status == null) return;
openReply(status, pickedAccountID);
});
}, null);
return true;
}
public int getSnackbarOffset(){
return replyContainer.getHeight()-lastBottomInset;
}
}

View File

@@ -6,7 +6,9 @@ import android.text.TextUtils;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.SearchViewHelper;
@@ -14,13 +16,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private String currentQuery;
public class AccountSearchFragment extends BaseAccountListFragment{
protected String currentQuery;
private boolean resultDelivered;
private SearchViewHelper searchViewHelper;
@@ -29,12 +32,11 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
super.onCreate(savedInstanceState);
setRefreshEnabled(false);
setEmptyText("");
dataLoaded();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder());
searchViewHelper.setListeners(this::onQueryChanged, null);
searchViewHelper.addDivider(contentView);
super.onViewCreated(view, savedInstanceState);
@@ -52,13 +54,21 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
AccountSearchFragment.this.onSuccess(result.accounts);
}
})
.exec(accountID);
}
protected void onSuccess(List<Account> result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
}
protected String getSearchViewPlaceholder(){
return getString(R.string.search_hint);
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();

View File

@@ -0,0 +1,37 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
import org.joinmastodon.android.model.Account;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class AddListMembersFragment extends AccountSearchFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Account> result){
AddListMembersFragment.this.onSuccess(result);
}
})
.exec(accountID);
}
@Override
protected String getSearchViewPlaceholder(){
return getString(R.string.search_among_people_you_follow);
}
}

View File

@@ -0,0 +1,125 @@
package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Button;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
@SuppressLint("ValidFragment") // This shouldn't be part of any saved states anyway
public class AddNewListMembersFragment extends AccountSearchFragment{
private Listener listener;
private String maxID;
public AddNewListMembersFragment(Listener listener){
this.listener=listener;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
if(TextUtils.isEmpty(currentQuery)){
currentRequest=new GetAccountFollowing(AccountSessionManager.get(accountID).self.id, offset>0 ? maxID : null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
setEmptyText("");
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
maxID=result.getNextPageMaxID();
}
})
.exec(accountID);
}else{
refreshing=true;
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Account> result){
AddNewListMembersFragment.this.onSuccess(result);
}
})
.exec(accountID);
}
}
@Override
protected String getSearchViewPlaceholder(){
return getString(R.string.search_among_people_you_follow);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
holder.setOnLongClickListener(vh->false);
Button button=holder.getButton();
button.setPadding(V.dp(24), 0, V.dp(24), 0);
button.setMinimumWidth(0);
button.setMinWidth(0);
button.setOnClickListener(v->{
holder.setActionProgressVisible(true);
holder.itemView.setHasTransientState(true);
Runnable onDone=()->{
holder.setActionProgressVisible(false);
holder.itemView.setHasTransientState(false);
onBindViewHolder(holder);
};
AccountViewModel account=holder.getItem();
if(listener.isAccountInList(account)){
listener.removeAccountAccountFromList(account, onDone);
}else{
listener.addAccountToList(account, onDone);
}
});
}
@Override
protected void onBindViewHolder(AccountViewHolder holder){
Button button=holder.getButton();
int textRes, styleRes;
if(listener.isAccountInList(holder.getItem())){
textRes=R.string.remove;
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
}else{
textRes=R.string.add;
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}
button.setText(textRes);
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
button.setTextColor(ta.getColorStateList(0));
ta.recycle();
}
@Override
protected void loadRelationships(List<AccountViewModel> accounts){
// no-op
}
public interface Listener{
boolean isAccountInList(AccountViewModel account);
void addAccountToList(AccountViewModel account, Runnable onDone);
void removeAccountAccountFromList(AccountViewModel account, Runnable onDone);
}
}

View File

@@ -39,6 +39,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
protected int itemLayoutRes=R.layout.item_account_list;
public BaseAccountListFragment(){
super(40);
@@ -74,6 +75,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected void loadRelationships(List<AccountViewModel> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
if(ids.isEmpty())
return;
GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req);
req.setCallback(new Callback<>(){
@@ -124,13 +127,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
Toolbar toolbar=getToolbar();
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
toolbar.setNavigationContentDescription(R.string.back);
if(hasSubtitle()){
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
toolbar.setTitleTextColor(color);
toolbar.setSubtitleTextColor(color);
}
}
}
@@ -162,6 +158,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
}
protected void onConfigureViewHolder(AccountViewHolder holder){}
protected void onBindViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
@@ -171,7 +168,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
onConfigureViewHolder(holder);
return holder;
}
@@ -179,6 +176,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@Override
public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(data.get(position));
BaseAccountListFragment.this.onBindViewHolder(holder);
super.onBindViewHolder(holder, position);
}

View File

@@ -279,8 +279,8 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
cover.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-2, image);
name.invalidate();
bio.invalidate();
name.setText(name.getText());
bio.setText(bio.getText());
}
if(image instanceof Animatable a && !a.isRunning())
a.start();

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.TextUtils;
@@ -24,6 +25,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 org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,7 +37,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 static final int QUERY_RESULT=937;
private TabLayout tabLayout;
@@ -80,8 +82,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.discover_hashtags;
case 1 -> R.id.discover_posts;
case 0 -> R.id.discover_posts;
case 1 -> R.id.discover_hashtags;
case 2 -> R.id.discover_news;
case 3 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i);
@@ -125,8 +127,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment)
.commit();
@@ -136,8 +138,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
tab.setText(switch(position){
case 0 -> R.string.hashtags;
case 1 -> R.string.posts;
case 0 -> R.string.posts;
case 1 -> R.string.hashtags;
case 2 -> R.string.news;
case 3 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position);
@@ -258,8 +260,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> hashtagsFragment;
case 1 -> postsFragment;
case 0 -> postsFragment;
case 1 -> hashtagsFragment;
case 2 -> newsFragment;
case 3 -> accountsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
@@ -291,6 +293,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(searchActive
? searchFragment
: getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
@@ -19,6 +20,8 @@ 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 org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
@@ -27,6 +30,7 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
@@ -40,7 +44,7 @@ import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop, IsOnTop{
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri{
private String accountID;
private DiscoverInfoBannerHelper bannerHelper;
private MergeRecyclerAdapter mergeAdapter;
@@ -115,6 +119,16 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
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<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
private final List<CardViewModel> data;
@@ -203,7 +217,16 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
@Override
public void onClick(){
UiUtils.launchWebBrowser(getActivity(), item.url);
//TODO: enable timeline for all servers once 4.3.0 is released
if(getInstance().isEmpty() ||
!getInstance().get().checkVersion(4,3,0)){
UiUtils.launchWebBrowser(getActivity(), item.url);
return;
}
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("trendingLink", Parcels.wrap(item));
Nav.go(getActivity(), DiscoverTrendingLinkTimelineFragment.class, args);
}
}

View File

@@ -18,7 +18,7 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverPostsFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
private int offset;
private int realOffset=0;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -26,26 +26,23 @@ public class DiscoverPostsFragment extends StatusListFragment{
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
}
@Override
protected void doLoadData(int o, int count){
if(refreshing) offset=0;
currentRequest=new GetTrendingStatuses(offset, count)
protected void doLoadData(int offset, int count){
currentRequest=new GetTrendingStatuses(offset==0 ? 0 : realOffset, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
boolean empty=result.isEmpty();
offset+=result.size();
realOffset+=result.size();
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, !empty);
onDataLoaded(result, !result.isEmpty());
bannerHelper.onBannerBecameVisible();
}
}).exec(accountID);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());

View File

@@ -0,0 +1,209 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetTrendingLinksTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeTabFragment;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
//TODO: replace this implementation when upstream implements their own design
public class DiscoverTrendingLinkTimelineFragment extends StatusListFragment{
private Card trendingLink;
private TextView headerTitle, headerSubtitle;
private Button openLinkButton;
private boolean toolbarContentVisible;
private Menu optionsMenu;
private MenuInflater optionsMenuInflater;
@Override
protected boolean wantsComposeButton() {
return true;
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
trendingLink=Parcels.unwrap(getArguments().getParcelable("trendingLink"));
setTitle(trendingLink.title);
setHasOptionsMenu(true);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetTrendingLinksTimeline(trendingLink.url, getMaxID(), null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
boolean more=applyMaxID(result);
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
onDataLoaded(result, more);
}
})
.exec(accountID);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
if(getParentFragment() instanceof HomeTabFragment) return;
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
View topChild=recyclerView.getChildAt(0);
int firstChildPos=recyclerView.getChildAdapterPosition(topChild);
float newAlpha=firstChildPos>0 ? 1f : Math.min(1f, -topChild.getTop()/(float)headerTitle.getHeight());
toolbarTitleView.setAlpha(newAlpha);
boolean newToolbarVisibility=newAlpha>0.5f;
if(newToolbarVisibility!=toolbarContentVisible){
toolbarContentVisible=newToolbarVisibility;
createOptionsMenu();
}
}
});
}
@Override
public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID, trendingLink.url);
}
@Override
public void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", trendingLink.url);
Nav.go(getActivity(), ComposeFragment.class, args);
}
@Override
protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
}
@Override
protected FilterContext getFilterContext() {
return FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/links").appendPath(trendingLink.url).build();
}
@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.header_trending_link_timeline, list, false);
headerTitle=header.findViewById(R.id.title);
headerSubtitle=header.findViewById(R.id.subtitle);
openLinkButton=header.findViewById(R.id.profile_action_btn);
headerTitle.setText(trendingLink.title);
openLinkButton.setVisibility(View.GONE);
openLinkButton.setOnClickListener(v->{
if(trendingLink==null)
return;
openLink();
});
updateHeader();
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
if(!(getParentFragment() instanceof HomeTabFragment)){
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(header));
}
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
protected int getMainAdapterOffset(){
return 1;
}
private void createOptionsMenu(){
optionsMenu.clear();
optionsMenuInflater.inflate(R.menu.trending_links_timeline, optionsMenu);
MenuItem openLinkMenuItem=optionsMenu.findItem(R.id.open_link);
openLinkMenuItem.setVisible(toolbarContentVisible);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.trending_links_timeline, menu);
super.onCreateOptionsMenu(menu, inflater);
optionsMenu=menu;
optionsMenuInflater=inflater;
createOptionsMenu();
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if (super.onOptionsItemSelected(item)) return true;
if (item.getItemId() == R.id.open_link && trendingLink!=null) {
openLink();
}
return true;
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f);
createOptionsMenu();
}
private void updateHeader(){
if(trendingLink==null || getActivity()==null)
return;
//TODO: update to show mastodon account once fully implemented upstream
headerSubtitle.setText(getContext().getString(R.string.article_by_author, TextUtils.isEmpty(trendingLink.authorName)? trendingLink.providerName : trendingLink.authorName));
openLinkButton.setVisibility(View.VISIBLE);
}
private void openLink() {
UiUtils.launchWebBrowser(getActivity(), trendingLink.url);
}
}

View File

@@ -29,7 +29,7 @@ public class FederatedTimelineFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, getMaxID(), count, getLocalPrefs().timelineReplyVisibility)
currentRequest=new GetPublicTimeline(false, false, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){

View File

@@ -29,7 +29,7 @@ public class LocalTimelineFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, getMaxID(), count, getLocalPrefs().timelineReplyVisibility)
currentRequest=new GetPublicTimeline(true, false, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){

View File

@@ -143,7 +143,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}*/
int offset=_offset;
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
.setCallback(new SimpleCallback<>(this){
.setCallback(new SimpleCallback<SearchResults>(this){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
@@ -164,7 +164,10 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
boolean wasRefreshing=refreshing;
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
if(wasRefreshing)
list.scrollToPosition(0);
}
})
.setTimeout(180000) // 3 minutes (searches can take a long time)

View File

@@ -10,6 +10,7 @@ import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -26,6 +27,7 @@ import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -434,7 +436,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
}
private void onOpenURLClick(ListItem<?> item_){
UiUtils.openURL(getContext(), accountID, searchViewHelper.getQuery(), false);
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
}
private void onGoToHashtagClick(ListItem<?> item_){

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
@@ -13,6 +14,7 @@ import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List;
@@ -23,7 +25,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop{
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri{
private String accountID;
public TrendingHashtagsFragment(){
@@ -65,6 +67,16 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
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

View File

@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;

View File

@@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
}

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
@@ -38,6 +37,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -48,7 +48,6 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery;
protected String currentSearchQueryButWithCasePreserved;
protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(getCurrentSearchQuery()));
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
loadInstanceInfo(getCurrentSearchQuery(), false);
}
@@ -156,6 +158,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
loadInstanceInfo(_domain, isFromRedirect, null);
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain))
return;
String domain=normalizeInstanceDomain(_domain);
@@ -180,7 +186,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){
showInstanceInfoLoadError(domain, x);
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -200,10 +209,11 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, getCurrentSearchQuery()) || Objects.equals(getCurrentSearchQuery(), redirects.get(domain)) || Objects.equals(getCurrentSearchQuery(), redirectsInverse.get(domain))){
boolean found=false;
@@ -230,11 +240,14 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(onError!=null)
onError.accept(error);
else
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -283,7 +296,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
}
}
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){
String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder()
.url(url)
@@ -297,7 +310,12 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
Activity a=getActivity();
if(a==null)
return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(e);
else
showInstanceInfoLoadError(domain, e);
});
}
@Override
@@ -309,7 +327,13 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
return;
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
a.runOnUiThread(()->{
String err=response.code()+" "+response.message();
if(onError!=null)
onError.accept(err);
else
showInstanceInfoLoadError(domain, err);
});
return;
}
InputSource source=new InputSource(response.body().charStream());
@@ -328,9 +352,19 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
}
}
}
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(origError);
else
showInstanceInfoLoadError(domain, origError);
});
}catch(Exception x){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
});
}
}
});

View File

@@ -1,8 +1,13 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
@@ -12,6 +17,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@@ -19,9 +26,12 @@ import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance;
@@ -29,6 +39,8 @@ import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -40,7 +52,9 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@@ -317,7 +334,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus();
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick);
nextButton.setEnabled(chosenInstance!=null);
}
@@ -351,91 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
@Override
protected void proceedWithAuthOrSignup(Instance instance){
if(currentInviteLinkAlert!=null){
currentInviteLinkAlert.dismiss();
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCodeHost=inviteLink.getHost();
inviteCode=result.inviteCode;
proceedWithAuthOrSignup(instance);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(inviteLink.getHost());
return;
}
}
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
if(instance.invitesEnabled){
showInviteLinkAlert(instance.uri);
}else{
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
}
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
private void onPickRandomInstanceClick(View v){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
onNextClick(v);
private void onUseInviteClick(View v){
showInviteLinkAlert(null);
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
// case "academia" -> "📚";
// case "activism" -> "✊";
// case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
private void showInviteLinkAlert(String domain){
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setView(R.layout.alert_invite_link)
.setPositiveButton(R.string.next, null)
.setNegativeButton(R.string.cancel, null)
.create();
private int getEmojiForCategory(String category){
return switch(category){
case "all" -> R.drawable.ic_category_all;
case "academia" -> R.drawable.ic_category_academia;
case "activism" -> R.drawable.ic_category_activism;
case "food" -> R.drawable.ic_category_food;
case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
case "general" -> R.drawable.ic_category_general;
case "journalism" -> R.drawable.ic_category_journalism;
case "lgbt" -> R.drawable.ic_category_lgbt;
case "regional" -> R.drawable.ic_category_regional;
case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
case "tech" -> R.drawable.ic_category_tech;
default -> R.drawable.ic_category_unknown;
};
}
Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE);
EditText edit=alert.findViewById(R.id.edit);
TextView supportingText=alert.findViewById(R.id.supporting_text);
TextView label=alert.findViewById(R.id.label);
TextView subtitle=alert.findViewById(R.id.subtitle);
ImageButton clear=alert.findViewById(R.id.clear);
clear.setVisibility(View.GONE);
private int getTitleForCategory(String category){
return switch(category){
case "all" -> R.string.category_all;
case "academia" -> R.string.category_academia;
case "activism" -> R.string.category_activism;
case "food" -> R.string.category_food;
case "furry" -> R.string.category_furry;
case "games" -> R.string.category_games;
case "general" -> R.string.category_general;
case "journalism" -> R.string.category_journalism;
case "lgbt" -> R.string.category_lgbt;
case "regional" -> R.string.category_regional;
case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
case "tech" -> R.string.category_tech;
default -> 0;
if(TextUtils.isEmpty(domain)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setText(getString(R.string.need_invite_to_join_server, domain));
}
Consumer<String> errorSetter=err->{
supportingText.setText(err);
int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
supportingText.setTextColor(errorColor);
label.setTextColor(errorColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error);
};
next.setOnClickListener(_v->{
Uri inviteLink=Uri.parse(edit.getText().toString());
if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){
errorSetter.accept(getString(R.string.this_invite_is_invalid));
return;
}
UiUtils.showProgressForAlertButton(next, true);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
if(getActivity()==null || !alert.isShowing())
return;
String host=inviteLink.getHost();
inviteCode=result.inviteCode;
inviteCodeHost=host;
Instance instance=instancesCache.get(normalizeInstanceDomain(host));
if(instance==null){
loadInstanceInfo(host, false, err->{
String errorStr;
if(err instanceof String str){
errorStr=str;
}else if(err instanceof Throwable x){
errorStr=x.getMessage();
}else if(err instanceof MastodonErrorResponse mer){
errorStr=mer.error;
}else{
errorStr=getString(R.string.error);
}
errorSetter.accept(errorStr);
UiUtils.showProgressForAlertButton(next, false);
});
}else{
proceedWithAuthOrSignup(instance);
}
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null || !alert.isShowing())
return;
UiUtils.showProgressForAlertButton(next, false);
if(error instanceof MastodonErrorResponse mer){
errorSetter.accept(switch(mer.httpStatus){
case 404 -> getString(R.string.this_invite_is_invalid);
case 401 -> getString(R.string.this_invite_has_expired);
default -> mer.error;
});
}
}
})
.execNoAuth(inviteLink.getHost());
});
next.setEnabled(false);
edit.addTextChangedListener(new SimpleTextWatcher(e->{
boolean wasEmpty=!next.isEnabled();
next.setEnabled(e.length()>0);
if(supportingText.length()>0){
supportingText.setText("");
int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant);
supportingText.setTextColor(regularColor);
label.setTextColor(regularColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field);
}
if(wasEmpty!=(e.length()==0)){
int padEnd;
if(e.length()==0){
clear.setVisibility(View.GONE);
padEnd=V.dp(16);
}else{
clear.setVisibility(View.VISIBLE);
padEnd=V.dp(48);
}
edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom());
}
}));
clear.setOnClickListener(_v->edit.setText(""));
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
edit.setText(clipText);
supportingText.setText(R.string.invite_link_pasted);
}
}
currentInviteLinkAlert=alert;
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
alert.show();
}
@Override
@@ -444,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
filteredData.clear();
if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){
String actualQuery;
if(currentSearchQuery.startsWith("https:")){
actualQuery=Uri.parse(currentSearchQuery).getHost();
}else{
actualQuery=currentSearchQuery;
}
for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){
if(instance.domain.contains(actualQuery)){
filteredData.add(instance);
}
}

View File

@@ -111,6 +111,9 @@ public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAs
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
}

View File

@@ -5,6 +5,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
@@ -13,35 +14,38 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
private String accountID;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40);
itemLayoutRes=R.layout.item_account_list;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.popular_on_mastodon);
setTitle(R.string.onboarding_recommendations_title);
accountID=getArguments().getString("account");
loadData();
}
@@ -50,7 +54,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
// view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
@@ -59,9 +62,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
getToolbar().setContentInsetsRelative(V.dp(56), 0);
}
@Override
@@ -70,7 +71,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
}
})
.exec(accountID);
@@ -81,6 +82,20 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
// Unused in Moshidon
// TextView introText=new TextView(getActivity());
// introText.setTextAppearance(R.style.m3_body_large);
// introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
// introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
// introText.setText(R.string.onboarding_recommendations_intro);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
// mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty())
return;
@@ -156,6 +171,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
}
@Override

View File

@@ -11,6 +11,7 @@ import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import org.joinmastodon.android.R;
@@ -19,12 +20,17 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -35,7 +41,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
public class OnboardingProfileSetupFragment extends ToolbarFragment{
private Button btn;
private View buttonBar;
private String accountID;
@@ -43,9 +49,9 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
private ScrollView scroller;
private EditText nameEdit, bioEdit;
private ImageView avaImage, coverImage;
private Button addRow;
private ReorderableLinearLayout profileFieldsLayout;
private Uri avatarUri, coverUri;
private LinearLayout scrollContent;
private CheckableListItem<Void> discoverableItem;
private static final int AVATAR_RESULT=348;
private static final int COVER_RESULT=183;
@@ -73,8 +79,6 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
bioEdit=view.findViewById(R.id.bio);
avaImage=view.findViewById(R.id.avatar);
coverImage=view.findViewById(R.id.header);
addRow=view.findViewById(R.id.add_row);
profileFieldsLayout=view.findViewById(R.id.profile_fields);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
@@ -86,31 +90,20 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
if(savedInstanceState==null){
nameEdit.setText(account.displayName);
makeFieldsRow();
}else{
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
for(int i=0;i<fieldTitles.size();i++){
View row=makeFieldsRow();
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
title.setText(fieldTitles.get(i));
content.setText(fieldValues.get(i));
}
if(fieldTitles.size()==4)
addRow.setVisibility(View.GONE);
}
addRow.setOnClickListener(v->{
makeFieldsRow();
if(profileFieldsLayout.getChildCount()==4){
addRow.setVisibility(View.GONE);
}
});
profileFieldsLayout.setDragListener(this);
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
scrollContent=view.findViewById(R.id.scrollable_content);
discoverableItem=new CheckableListItem<>(R.string.make_profile_discoverable, 0, CheckableListItem.Style.SWITCH_SEPARATED, true, R.drawable.ic_campaign_24px, item->showDiscoverabilityAlert());
GenericListItemsAdapter<Void> fakeAdapter=new GenericListItemsAdapter<>(List.of(discoverableItem));
ListItemViewHolder<?> holder=fakeAdapter.onCreateViewHolder(scrollContent, fakeAdapter.getItemViewType(0));
fakeAdapter.bindViewHolder(holder, 0);
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
holder.itemView.setOnClickListener(v->holder.onClick());
scrollContent.addView(holder.itemView);
return view;
}
@@ -129,17 +122,8 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
}
protected void onButtonClick(){
ArrayList<AccountField> fields=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
AccountField fld=new AccountField();
fld.name=title.getText().toString();
fld.value=content.getText().toString();
fields.add(fld);
}
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, null)
.setDiscoverableIndexable(discoverableItem.checked, discoverableItem.checked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
@@ -163,39 +147,6 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private View makeFieldsRow(){
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
profileFieldsLayout.addView(view);
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
profileFieldsLayout.startDragging(view);
return true;
});
view.findViewById(R.id.delete).setOnClickListener(v->{
profileFieldsLayout.removeView(view);
if(addRow.getVisibility()==View.GONE)
addRow.setVisibility(View.VISIBLE);
});
return view;
}
@Override
public void onSwapItems(int oldIndex, int newIndex){}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
fieldTitles.add(title.getText().toString());
fieldValues.add(content.getText().toString());
}
outState.putStringArrayList("fieldTitles", fieldTitles);
outState.putStringArrayList("fieldValues", fieldValues);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode!=Activity.RESULT_OK)
@@ -215,4 +166,12 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
img.setForeground(null);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
}
private void showDiscoverabilityAlert(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discoverability)
.setMessage(R.string.discoverability_help)
.setPositiveButton(R.string.ok, null)
.show();
}
}

View File

@@ -1,18 +1,15 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -46,10 +43,13 @@ import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
@@ -58,7 +58,6 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends ToolbarFragment{
@@ -79,6 +78,7 @@ public class SignupFragment extends ToolbarFragment{
private ProgressDialog progressDialog;
private HashSet<EditText> errorFields=new HashSet<>();
private ElevationOnScrollListener onScrollListener;
private Set<String> serverSupportedTimezones, serverSupportedLocales;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -87,6 +87,8 @@ public class SignupFragment extends ToolbarFragment{
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
setTitle(R.string.signup_title);
serverSupportedTimezones=Arrays.stream(getResources().getStringArray(R.array.server_supported_timezones)).collect(Collectors.toSet());
serverSupportedLocales=Arrays.stream(getResources().getStringArray(R.array.server_supported_locales)).collect(Collectors.toSet());
}
@Nullable
@@ -190,7 +192,36 @@ public class SignupFragment extends ToolbarFragment{
edit.setError(null);
}
errorFields.clear();
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString(), ZoneId.systemDefault().getId())
String locale=null;
String timezone=ZoneId.systemDefault().getId();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
LocaleList localeList=getResources().getConfiguration().getLocales();
for(int i=0;i<localeList.size();i++){
Locale l=localeList.get(i);
if(serverSupportedLocales.contains(l.toLanguageTag())){
locale=l.toLanguageTag();
break;
}else if(serverSupportedLocales.contains(l.getLanguage())){
locale=l.getLanguage();
break;
}
}
}else{
Locale l=getResources().getConfiguration().locale;
if(serverSupportedLocales.contains(l.toLanguageTag())){
locale=l.toLanguageTag();
}else if(serverSupportedLocales.contains(l.getLanguage())){
locale=l.getLanguage();
}
}
if(!serverSupportedTimezones.contains(timezone))
timezone=null;
String inviteCode=getArguments().getString("inviteCode");
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){
@@ -271,7 +302,7 @@ public class SignupFragment extends ToolbarFragment{
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}

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