Compare commits

...

356 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
260 changed files with 5949 additions and 1277 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

@@ -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 105
versionName "2.3.0+fork.105.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']
}
@@ -102,9 +102,14 @@ android {
shrinkResources true
versionNameSuffix '-play'
}
githubRelease { 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
}
}

View File

@@ -1,104 +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 android.graphics.drawable.ColorDrawable;
import org.joinmastodon.android.model.Attachment;
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"/>
@@ -25,10 +26,6 @@
<intent>
<action android:name="android.intent.action.TRANSLATE" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
</queries>
<application
@@ -85,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"/>
@@ -110,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

@@ -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

@@ -57,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;
@@ -78,10 +77,10 @@ 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);
@@ -137,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);
@@ -160,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)];
@@ -213,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)
@@ -232,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)
@@ -240,6 +236,7 @@ public class GlobalUserPreferences{
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
.putBoolean("showMediaPreview", showMediaPreview)
.putBoolean("removeTrackingParams", removeTrackingParams)
.apply();
}

View File

@@ -111,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()){
@@ -187,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);
@@ -343,8 +330,6 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
}catch(BadParcelableException x){
Log.w(TAG, x);
}
} else if (intent.getBooleanExtra("compose", false)){
showCompose();
} 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){

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

@@ -91,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()

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

@@ -21,6 +21,7 @@ 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;
@@ -37,6 +38,7 @@ 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;
@@ -313,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){
@@ -326,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);
}

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;
@@ -491,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

@@ -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,19 +24,18 @@ 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.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
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;
@@ -70,7 +70,6 @@ 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;
@@ -79,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;
@@ -664,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){
@@ -696,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);
@@ -731,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());
}
@@ -743,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);
}
@@ -883,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++){

View File

@@ -65,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;
@@ -98,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;
@@ -512,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);
@@ -1174,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);
@@ -1504,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);
@@ -1662,7 +1666,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
}
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()) 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){

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;
@@ -53,7 +51,7 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
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

@@ -396,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

@@ -101,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);
}
@@ -232,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

@@ -161,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)
@@ -182,7 +184,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
});
}
}
tabBar.selectTab(currentTab);
return content;
}

View File

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

View File

@@ -817,18 +817,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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;
}
@@ -865,10 +859,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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){
@@ -965,11 +956,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
.show();
}
invalidateOptionsMenu();
}else if(id==R.id.manage_user_lists){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
}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;
}
@@ -1597,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

@@ -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

@@ -228,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;
}
}
}
}
@@ -304,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;
@@ -418,7 +429,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
UiUtils.loadCustomEmojiInTextView(replyButtonText);
replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
replyButtonAva.setClipToOutline(true);
replyButton.setOnClickListener(v->openReply());
replyButton.setOnClickListener(v->openReply(mainStatus, accountID));
replyButton.setOnLongClickListener(this::onReplyLongClick);
Account self=AccountSessionManager.get(accountID).self;
if(!TextUtils.isEmpty(self.avatar)){
@@ -570,11 +581,11 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
}
private void openReply(){
maybeShowPreReplySheet(mainStatus, ()->{
private void openReply(Status status, String accountID){
maybeShowPreReplySheet(status, ()->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
args.putParcelable("replyTo", Parcels.wrap(status));
args.putBoolean("fromThreadFragment", true);
Nav.go(getActivity(), ComposeFragment.class, args);
});
@@ -583,9 +594,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
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 -> {
UiUtils.lookupStatus(v.getContext(), mainStatus, accountID, session.getID(), status -> {
String pickedAccountID = session.getID();
UiUtils.lookupStatus(v.getContext(), mainStatus, pickedAccountID, accountID, status -> {
if (status == null) return;
openReply();
openReply(status, pickedAccountID);
});
}, null);
return true;

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

@@ -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

@@ -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

@@ -100,7 +100,6 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
ProgressBar topProgress=view.findViewById(R.id.top_progress);
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 75 : 66);
forwardSwitch.setChecked(GlobalUserPreferences.forwardReportDefault);
}
@Override

View File

@@ -1,11 +1,13 @@
package org.joinmastodon.android.fragments.settings;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
@@ -14,10 +16,11 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.recyclerview.widget.RecyclerView;
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>> implements HasAccountID, ProvidesAssistContent.ProvidesWebUri{
protected GenericListItemsAdapter<T> itemsAdapter;
protected String accountID;
@@ -83,4 +86,14 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
}
super.onApplyWindowInsets(insets);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/settings").build();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.Menu;
@@ -329,4 +330,8 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
return false;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(filter == null ? "/filters/new" : "/filters/"+ filter.id + "/edit").build();
}
}

View File

@@ -3,6 +3,11 @@ package org.joinmastodon.android.fragments.settings;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -11,6 +16,13 @@ import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.ToNumberPolicy;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
@@ -21,6 +33,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
@@ -28,20 +41,18 @@ import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.io.OutputStream;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ImageCache;
@@ -51,6 +62,8 @@ import me.grishka.appkit.utils.V;
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> implements HasAccountID{
private static final String TAG="SettingsAboutAppFragment";
private static final int IMPORT_RESULT=314;
private static final int EXPORT_RESULT=271;
private ListItem<Void> mediaCacheItem, copyCrashLogItem;
private CheckableListItem<Void> enablePreReleasesItem;
private AccountSession session;
@@ -58,7 +71,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log");
// MOSHIDON
private ListItem<Void> clearRecentEmojisItem;
private ListItem<Void> clearRecentEmojisItem, exportItem, importItem;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -73,13 +86,15 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
new ListItem<>(R.string.mo_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.mo_repo_url))),
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
exportItem=new ListItem<>(R.string.export_settings_title, R.string.export_settings_summary, R.drawable.ic_fluent_arrow_export_24_filled, this::onExportClick),
importItem=new ListItem<>(R.string.import_settings_title, R.string.import_settings_summary, R.drawable.ic_fluent_arrow_import_24_filled, this::onImportClick, 0, true),
clearRecentEmojisItem=new ListItem<>(R.string.mo_clear_recent_emoji, 0, this::onClearRecentEmojisClick),
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick),
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick),
copyCrashLogItem=new ListItem<>(getString(R.string.sk_settings_copy_crash_log), lastModified, 0, this::onCopyCrashLog)
));
if(GithubSelfUpdater.needSelfUpdating()){
if(GithubSelfUpdater.needSelfUpdating() && !BuildConfig.BUILD_TYPE.equals("nightly") ){
items.add(enablePreReleasesItem=new CheckableListItem<>(R.string.sk_updater_enable_pre_releases, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enablePreReleases, i->toggleCheckableItem(enablePreReleasesItem)));
}
@@ -146,6 +161,166 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
Toast.makeText(getContext(), R.string.mo_recent_emoji_cleared, Toast.LENGTH_SHORT).show();
}
private void onExportClick(ListItem<?> item){
// The magic will happen on the onActivityResult Method
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType("application/json");
intent.putExtra(Intent.EXTRA_TITLE,"moshidon-exported-settings.json");
startActivityForResult(intent, EXPORT_RESULT);
}
private void onImportClick(ListItem<?> item){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.import_settings_confirm)
.setIcon(R.drawable.ic_fluent_warning_24_regular)
.setMessage(R.string.import_settings_confirm_body)
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/json");
startActivityForResult(intent, IMPORT_RESULT);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==IMPORT_RESULT && resultCode==Activity.RESULT_OK){
Uri uri=data.getData();
if(uri==null){
return;
}
try{
InputStream inputStream=getContext().getContentResolver().openInputStream(uri);
if(inputStream==null)
return;
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
StringBuilder stringBuilder=new StringBuilder();
String line;
while((line=reader.readLine())!=null){
stringBuilder.append(line);
}
inputStream.close();
String jsonString=stringBuilder.toString();
Gson gson=new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
//check if json is not null
if(jsonString.isEmpty()) {
throw new IOException();
}
JsonObject jsonObject=JsonParser.parseString(jsonString).getAsJsonObject();
//check if json has required attributes
if(!(jsonObject.has("versionName") && jsonObject.has("versionCode") && jsonObject.has("GlobalUserPreferences"))){
Toast.makeText(getContext(), getContext().getString(R.string.import_settings_failed), Toast.LENGTH_SHORT).show();
return;
}
String versionName=jsonObject.get("versionName").getAsString();
int versionCode=jsonObject.get("versionCode").getAsInt();
Log.i(TAG, "onActivityResult: Reading exported settings ("+versionName+" "+versionCode+")");
// retrieve GlobalUserPreferences
Map<String, ?> jsonGlobalPrefs=gson.fromJson(jsonObject.getAsJsonObject("GlobalUserPreferences"), Map.class);
SharedPreferences.Editor globalPrefsEditor=GlobalUserPreferences.getPrefs().edit();
for(String key : jsonGlobalPrefs.keySet()){
Object value=jsonGlobalPrefs.get(key);
if(value==null)
continue;
savePrefValue(globalPrefsEditor, key, value);
}
// retrieve LocalPreferences for all logged in accounts
//TODO: maybe show a dialog for which accounts to import?
for(AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
if(!jsonObject.has(accountSession.self.id))
continue;
Map<String, ?> prefs=gson.fromJson(jsonObject.getAsJsonObject(accountSession.self.id), Map.class);
SharedPreferences.Editor prefEditor=accountSession.getRawLocalPreferences().edit();
for(String key : prefs.keySet()){
Object value=prefs.get(key);
if(value==null)
continue;
savePrefValue(prefEditor, key, value);
}
}
// restart app to apply new preferences
// https://stackoverflow.com/a/46848226
PackageManager packageManager=getContext().getPackageManager();
Intent intent=packageManager.getLaunchIntentForPackage(getContext().getPackageName());
ComponentName componentName=intent.getComponent();
Intent mainIntent=Intent.makeRestartActivityTask(componentName);
// Required for API 34 and later
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
mainIntent.setPackage(getContext().getPackageName());
getContext().startActivity(mainIntent);
Runtime.getRuntime().exit(0);
}catch(IOException e){
Log.w(TAG, e);
Toast.makeText(getContext(), getContext().getString(R.string.import_settings_failed), Toast.LENGTH_SHORT).show();
}
}
if(requestCode == EXPORT_RESULT && resultCode==Activity.RESULT_OK) {
try{
Gson gson = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("versionName", BuildConfig.VERSION_NAME);
jsonObject.addProperty("versionCode", BuildConfig.VERSION_CODE);
// GlobalUserPreferences
//TODO: remove prefs that should not be exported
JsonElement je = gson.toJsonTree(GlobalUserPreferences.getPrefs().getAll());
jsonObject.add("GlobalUserPreferences", je);
// add account local prefs
for(AccountSession accountSession: AccountSessionManager.getInstance().getLoggedInAccounts()) {
Map<String, ?> prefs = accountSession.getRawLocalPreferences().getAll();
//TODO: remove prefs that should not be exported
JsonElement accountPrefs = gson.toJsonTree(prefs);
jsonObject.add(accountSession.self.id, accountPrefs);
}
File file = new File(getContext().getCacheDir(), "moshidon-exported-settings.json");
FileWriter writer = new FileWriter(file);
writer.write(jsonObject.toString());
writer.flush();
writer.close();
// Got this from stackoverflow at https://stackoverflow.com/a/67046741
InputStream is = new FileInputStream(file);
OutputStream os = getContext().getContentResolver().openOutputStream(data.getData());
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}catch(IOException e){
Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_SHORT);
}
}
}
private void savePrefValue(SharedPreferences.Editor editor, String key, Object value) {
if(value.getClass().equals(Boolean.class))
editor.putBoolean(key, (Boolean) value);
// gson parses all numbers either long (for int) or double (the rest)
else if(value.getClass().equals(Long.class))
editor.putInt(key, ((Long) value).intValue());
else if(value.getClass().equals(Double.class))
editor.putFloat(key, ((Double) value).floatValue());
else
editor.putString(key, String.valueOf(value));
//explicitly immediately since the app will restarted soon after
// and it may not have the time to write the values in the background
editor.commit();
}
private void updateMediaCacheItem(){
long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);

View File

@@ -1,19 +1,12 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
@@ -30,7 +23,10 @@ import org.joinmastodon.android.utils.MastodonLanguage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> implements HasAccountID{
private ListItem<Void> languageItem;
@@ -41,7 +37,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
// MEGALODON
private MastodonLanguage.LanguageResolver languageResolver;
private ListItem<Void> prefixRepliesItem, replyVisibilityItem, customTabsItem;
private CheckableListItem<Void> forwardReportsItem, remoteLoadingItem, showBoostsItem, showRepliesItem, loadNewPostsItem, seeNewPostsBtnItem, overlayMediaItem;
private CheckableListItem<Void> remoteLoadingItem, showBoostsItem, showRepliesItem, loadNewPostsItem, seeNewPostsBtnItem, overlayMediaItem;
// MOSHIDON
private CheckableListItem<Void> mentionRebloggerAutomaticallyItem, hapticFeedbackItem, showPostsWithoutAltItem;
@@ -68,12 +64,11 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_fluent_arrow_repeat_all_24_regular, i->toggleCheckableItem(confirmBoostItem)),
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_fluent_delete_24_regular, i->toggleCheckableItem(confirmDeleteItem)),
prefixRepliesItem=new ListItem<>(R.string.sk_settings_prefix_reply_cw_with_re, getPrefixWithRepliesString(), R.drawable.ic_fluent_arrow_reply_24_regular, this::onPrefixRepliesClick),
forwardReportsItem=new CheckableListItem<>(R.string.sk_settings_forward_report_default, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.forwardReportDefault, R.drawable.ic_fluent_arrow_forward_24_regular, i->toggleCheckableItem(forwardReportsItem)),
loadNewPostsItem=new CheckableListItem<>(R.string.sk_settings_load_new_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.loadNewPosts, R.drawable.ic_fluent_arrow_sync_24_regular, i->onLoadNewPostsClick()),
seeNewPostsBtnItem=new CheckableListItem<>(R.string.sk_settings_see_new_posts_button, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNewPostsButton, R.drawable.ic_fluent_arrow_up_24_regular, i->toggleCheckableItem(seeNewPostsBtnItem)),
hapticFeedbackItem=new CheckableListItem<>(R.string.mo_haptic_feedback, R.string.mo_setting_haptic_feedback_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.hapticFeedback, R.drawable.ic_fluent_phone_vibrate_24_regular, i->toggleCheckableItem(hapticFeedbackItem)),
remoteLoadingItem=new CheckableListItem<>(R.string.sk_settings_allow_remote_loading, R.string.sk_settings_allow_remote_loading_explanation, CheckableListItem.Style.SWITCH, GlobalUserPreferences.allowRemoteLoading, R.drawable.ic_fluent_communication_24_regular, i->toggleCheckableItem(remoteLoadingItem)),
mentionRebloggerAutomaticallyItem=new CheckableListItem<>(R.string.mo_mention_reblogger_automatically, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.mentionRebloggerAutomatically, R.drawable.ic_fluent_comment_mention_24_regular, i->toggleCheckableItem(mentionRebloggerAutomaticallyItem)),
hapticFeedbackItem=new CheckableListItem<>(R.string.mo_haptic_feedback, R.string.mo_setting_haptic_feedback_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.hapticFeedback, R.drawable.ic_fluent_phone_vibrate_24_regular, i->toggleCheckableItem(hapticFeedbackItem), true),
mentionRebloggerAutomaticallyItem=new CheckableListItem<>(R.string.mo_mention_reblogger_automatically, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.mentionRebloggerAutomatically, R.drawable.ic_fluent_comment_mention_24_regular, i->toggleCheckableItem(mentionRebloggerAutomaticallyItem), true),
showBoostsItem=new CheckableListItem<>(R.string.sk_settings_show_boosts, 0, CheckableListItem.Style.SWITCH, lp.showBoosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, i->toggleCheckableItem(showBoostsItem)),
showRepliesItem=new CheckableListItem<>(R.string.sk_settings_show_replies, 0, CheckableListItem.Style.SWITCH, lp.showReplies, R.drawable.ic_fluent_arrow_reply_24_regular, i->toggleCheckableItem(showRepliesItem))
));
@@ -178,14 +173,6 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
private void onCustomTabsClick(ListItem<?> item){
// GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("http://example.com"));
ResolveInfo info=getActivity().getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
final String browserName;
if(info==null){
browserName="??";
}else{
browserName=info.loadLabel(getActivity().getPackageManager()).toString();
}
ArrayAdapter<CharSequence> adapter=new ArrayAdapter<>(getActivity(), R.layout.item_alert_single_choice_2lines_but_different, R.id.text,
new String[]{getString(R.string.in_app_browser), getString(R.string.system_browser)}){
@Override
@@ -198,12 +185,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent){
View view=super.getView(position, convertView, parent);
TextView subtitle=view.findViewById(R.id.subtitle);
if(position==0){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(browserName);
}
subtitle.setVisibility(View.GONE);
return view;
}
};
@@ -227,7 +209,6 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
GlobalUserPreferences.confirmUnfollow=confirmUnfollowItem.checked;
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
GlobalUserPreferences.forwardReportDefault=forwardReportsItem.checked;
GlobalUserPreferences.loadNewPosts=loadNewPostsItem.checked;
GlobalUserPreferences.showNewPostsButton=seeNewPostsBtnItem.checked;
GlobalUserPreferences.allowRemoteLoading=remoteLoadingItem.checked;

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.settings;
import android.net.Uri;
import android.os.Bundle;
import com.squareup.otto.Subscribe;
@@ -107,4 +108,9 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
data.add(makeListItem(ev.filter));
itemsAdapter.notifyItemInserted(data.size()-1);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/filters").build();
}
}

View File

@@ -1,7 +1,5 @@
package org.joinmastodon.android.fragments.settings;
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.content.Intent;
@@ -15,6 +13,7 @@ import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.PushSubscriptionManager;
@@ -48,6 +47,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
private HideableSingleViewRecyclerAdapter bannerAdapter;
private ImageView bannerIcon;
private TextView bannerText;
private TextView bannerTitle;
private Button bannerButton;
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
@@ -72,7 +72,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
lp=AccountSessionManager.get(accountID).getLocalPreferences();
getPushSubscription();
useUnifiedPush=!getDistributor(getContext()).isEmpty();
useUnifiedPush=!UnifiedPush.getDistributor(getContext()).isEmpty();
onDataLoaded(List.of(
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, i->onPauseNotificationsClick(false)),
@@ -158,6 +158,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
bannerTitle=banner.findViewById(R.id.title);
bannerText=banner.findViewById(R.id.text);
bannerIcon=banner.findViewById(R.id.icon);
bannerButton=banner.findViewById(R.id.button);
@@ -315,6 +316,20 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
bannerText.setText(R.string.notifications_disabled_in_system);
bannerButton.setText(R.string.open_system_notification_settings);
bannerButton.setOnClickListener(v->openSystemNotificationSettings());
}else if(BuildConfig.BUILD_TYPE.equals("fdroidRelease") && UnifiedPush.getDistributor(getContext()).isEmpty()){
bannerAdapter.setVisible(true);
bannerIcon.setImageResource(R.drawable.ic_fluent_warning_24_filled);
bannerTitle.setVisibility(View.VISIBLE);
bannerTitle.setText(R.string.mo_settings_unifiedpush_warning);
if(UnifiedPush.getDistributors(getContext(), new ArrayList<>()).isEmpty()) {
bannerText.setText(R.string.mo_settings_unifiedpush_warning_no_distributors);
bannerButton.setText(R.string.info);
bannerButton.setOnClickListener(v->UiUtils.launchWebBrowser(getContext(), "https://unifiedpush.org/"));
} else {
bannerText.setText(R.string.mo_settings_unifiedpush_warning_disabled);
bannerButton.setText(R.string.mo_settings_unifiedpush_enable);
bannerButton.setOnClickListener(v->onUnifiedPushClick());
}
}else if(pauseTime>System.currentTimeMillis()){
bannerAdapter.setVisible(true);
bannerIcon.setImageResource(R.drawable.ic_fluent_alert_snooze_24_regular);
@@ -327,7 +342,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
}
private void onUnifiedPushClick(){
if(getDistributor(getContext()).isEmpty()){
if(UnifiedPush.getDistributor(getContext()).isEmpty()){
List<String> distributors = UnifiedPush.getDistributors(getContext(), new ArrayList<>());
showUnifiedPushRegisterDialog(distributors);
return;
@@ -363,4 +378,9 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
rebindItem(unifiedPushItem);
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/settings/preferences/notifications").build();
}
}

View File

@@ -25,7 +25,7 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
private Instance instance;
//MOSHIDON
private CheckableListItem<Void> unlistedRepliesItem;
private CheckableListItem<Void> unlistedRepliesItem, removeTrackingParams;
@Override
@@ -38,7 +38,8 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
privacy=self.source.privacy;
onDataLoaded(List.of(
privacyItem=new ListItem<>(R.string.sk_settings_default_visibility, getPrivacyString(privacy), R.drawable.ic_fluent_eye_24_regular, this::onPrivacyClick, 0, false),
unlistedRepliesItem=new CheckableListItem<>(R.string.mo_change_default_reply_visibility_to_unlisted, R.string.mo_setting_default_reply_privacy_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.defaultToUnlistedReplies, R.drawable.ic_fluent_lock_open_24_regular, i->toggleCheckableItem(unlistedRepliesItem), true),
unlistedRepliesItem=new CheckableListItem<>(R.string.mo_change_default_reply_visibility_to_unlisted, R.string.mo_setting_default_reply_privacy_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.defaultToUnlistedReplies, R.drawable.ic_fluent_lock_open_24_regular, i->toggleCheckableItem(unlistedRepliesItem)),
removeTrackingParams=new CheckableListItem<>(R.string.mo_settings_remove_tracking_params, R.string.mo_settings_remove_tracking_params_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.removeTrackingParams, R.drawable.ic_fluent_eye_tracking_off_24_filled, i->toggleCheckableItem(removeTrackingParams), true),
lockedItem=new CheckableListItem<>(R.string.sk_settings_lock_account, 0, CheckableListItem.Style.SWITCH, self.locked, R.drawable.ic_fluent_person_available_24_regular, i->toggleCheckableItem(lockedItem))
));
@@ -89,6 +90,7 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
public void onPause(){
super.onPause();
GlobalUserPreferences.defaultToUnlistedReplies=unlistedRepliesItem.checked;
GlobalUserPreferences.removeTrackingParams=removeTrackingParams.checked;
GlobalUserPreferences.save();
AccountSession s=AccountSessionManager.get(accountID);
Account self=s.self;

View File

@@ -1,19 +1,28 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.jsoup.internal.StringUtil;
import java.util.EnumSet;
public class AltTextFilter extends LegacyFilter {
public AltTextFilter(FilterAction filterAction, FilterContext firstContext, FilterContext... restContexts) {
this.filterAction = filterAction;
isRemote = false;
context = EnumSet.of(firstContext, restContexts);
public AltTextFilter(FilterAction filterAction, EnumSet<FilterContext> filterContexts) {
this.filterAction=filterAction;
this.title=MastodonApp.context.getString(R.string.sk_no_alt_text);
this.isRemote=false;
this.context=filterContexts;
}
@Override
public boolean matches(Status status) {
return status.getContentStatus().mediaAttachments.stream().map(attachment -> attachment.description).anyMatch(StringUtil::isBlank);
}
@Override
public boolean isActive(){
return !GlobalUserPreferences.showPostsWithoutAlt;
}
}

View File

@@ -38,7 +38,7 @@ public class EmojiReaction {
reaction.staticUrl=info.staticUrl;
reaction.accounts=new ArrayList<>(Collections.singleton(me));
reaction.accountIds=new ArrayList<>(Collections.singleton(me.id));
reaction.request=new UrlImageLoaderRequest(info.url, V.sp(24), V.sp(24));
reaction.request=new UrlImageLoaderRequest(info.url, 0, V.sp(24));
return reaction;
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.model;
import android.text.Html;
import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
@@ -8,6 +9,7 @@ import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.parceler.Parcel;
import java.net.IDN;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -166,6 +168,31 @@ public class Instance extends BaseModel{
.orElse(false);
};
}
/**
* Returns true if the instance version is the same as or newer than the passed in version.
* @param major: The major version to check for.
* @param minor: the minor version to check for.
* @param patch: The patch version to check for.
*/
public boolean checkVersion(int major, int minor, int patch) {
try{
String[] parts=version.split("-", 2);
String[] numbers=parts[0].split("\\.", 3);
if(numbers.length < 3)
throw new IllegalArgumentException("Invalid version format. Expected format: major.minor.micro");
int majorVersion=Integer.parseInt(numbers[0]);
int minorVersion=Integer.parseInt(numbers[1]);
int patchVersion=Integer.parseInt(numbers[2]);
return (majorVersion > major ||
(majorVersion == major && minorVersion > minor) ||
(majorVersion == major && minorVersion == minor &&
patchVersion>= patch));
} catch(Exception e) {
Log.w("Instance", "checkVersion: failed to parse " + version + ", " + e);
return false;
}
}
public enum Feature {
BUBBLE_TIMELINE,

View File

@@ -14,7 +14,7 @@ import androidx.annotation.StringRes;
public class PushNotification extends BaseModel{
public String accessToken;
public String preferredLocale;
public long notificationId;
public String notificationId;
@RequiredField
public Type notificationType;
@RequiredField

View File

@@ -1,17 +1,27 @@
package org.joinmastodon.android.model;
import android.util.Patterns;
import androidx.annotation.NonNull;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Parcel
public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
private static final Pattern HIGHLIGHT_PATTER=Pattern.compile("(?<!\\w)(?:@([a-z0-9_]+)(@[a-z0-9_\\.\\-]*)?|#([^\\s.]+)|:([a-z0-9_]+))|" +Patterns.WEB_URL, Pattern.CASE_INSENSITIVE);
@RequiredField
public String id;
@RequiredField
@@ -87,7 +97,61 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
s.visibility=params.visibility;
s.language=params.language;
s.sensitive=params.sensitive;
// hide media preview only if status is marked as sensitive
s.sensitiveRevealed=!params.sensitive;
if(params.poll!=null) s.poll=params.poll.toPoll();
return s;
}
/**
* Creates a fake status, which has (somewhat) correctly formatted mentions, hashtags and URLs.
*
* @param accountID the ID of the account
* @return the formatted Status object
*/
public Status toFormattedStatus(String accountID){
AccountSession self=AccountSessionManager.get(accountID);
Status s=this.toStatus();
// the mastodon api does not return formatted (html) content, only the raw content, so we modify it
s.content=s.content.replace("\n", "<br>");
if(!s.content.contains("@") && !s.content.contains("#") && !s.content.contains(":"))
return s;
StringBuffer sb=new StringBuffer();
Matcher matcher=HIGHLIGHT_PATTER.matcher(s.content);
// I'm sure this will cause problems at some point...
while(matcher.find()){
String content=matcher.group();
String href="";
// add relevant links, so on-click actions work
// hashtags are done by the parser
if(content.startsWith("@"))
href=" href=\""+formatMention(content, self.domain)+"\" class=\"u-url mention\"";
else if(content.startsWith("https://"))
href=" href=\""+content+"\"";
matcher.appendReplacement(sb, "<a"+href+">"+content+"</a>");
}
matcher.appendTail(sb);
s.content=sb.toString();
return s;
}
/**
* Converts a string mention into a URL of the account.
* @param mention Mention in the form a of user name with an optional instance URL
* @param instanceURL URL of the home instance of the user
* @return Formatted HTML or the mention
*/
@NonNull
private static String formatMention(@NonNull String mention, @NonNull String instanceURL){
String[] parts=mention.split("@");
if(parts.length>1){
String username=parts[1];
String domain=parts.length==3 ? parts[2] : instanceURL;
return "https://"+domain+"/@"+username;
}
return mention;
}
}

View File

@@ -336,6 +336,7 @@ public class TimelineDefinition {
THUNDERSTORM(R.drawable.ic_fluent_weather_thunderstorm_24_regular, R.string.sk_icon_thunderstorm),
RAIN(R.drawable.ic_fluent_weather_rain_24_regular, R.string.sk_icon_rain),
SNOWFLAKE(R.drawable.ic_fluent_weather_snowflake_24_regular, R.string.sk_icon_snowflake),
GNOME(R.drawable.ic_gnome_logo, R.string.mo_icon_gnome),
HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true),
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),

View File

@@ -1,15 +1,15 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.RequiredField;
/**
* Represents an OAuth token used for authenticating with the API and performing actions.
*/
@AllFieldsAreRequired
public class Token extends BaseModel{
/**
* An OAuth token to be used for authorization.
*/
@RequiredField
public String accessToken;
/**
* The OAuth token type. Mastodon uses Bearer tokens.
@@ -23,5 +23,6 @@ public class Token extends BaseModel{
* When the token was generated.
* (unixtime)
*/
@RequiredField
public long createdAt;
}

View File

@@ -172,7 +172,18 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), null, v == acceptButton, relationship, rel -> {
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), 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(v.getContext()==null || rel==null) return;
itemView.setHasTransientState(false);
item.parentFragment.putRelationship(item.account.id, rel);
@@ -214,8 +225,8 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
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 && !((Animatable) image).isRunning())
((Animatable) image).start();

View File

@@ -172,7 +172,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
addButton.setSelected(false);
AccountSession session=item.parentFragment.getSession();
item.status.reactions.forEach(r->r.request=r.getUrl(item.playGifs)!=null
? new UrlImageLoaderRequest(r.getUrl(item.playGifs), V.sp(24), V.sp(24))
? new UrlImageLoaderRequest(r.getUrl(item.playGifs), 0, V.sp(24))
: null);
emojiKeyboard=new CustomEmojiPopupKeyboard(
(Activity) item.parentFragment.getContext(),
@@ -342,7 +342,9 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
@Override
public void setImage(int index, Drawable drawable){
drawable.setBounds(0, 0, V.sp(24), V.sp(24));
int height=V.sp(24);
int width=drawable.getIntrinsicWidth()*height/drawable.getIntrinsicHeight();
drawable.setBounds(0, 0, width, height);
btn.setCompoundDrawablesRelative(drawable, null, null, null);
if(drawable instanceof Animatable) ((Animatable) drawable).start();
}

View File

@@ -0,0 +1,64 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
public class ErrorStatusDisplayItem extends StatusDisplayItem{
private final Exception exception;
public ErrorStatusDisplayItem(String parentID, Status status, BaseStatusListFragment<?> parentFragment, Exception exception) {
super(parentID, parentFragment);
this.exception=exception;
this.status=status;
}
@Override
public Type getType() {
return Type.ERROR_ITEM;
}
public static class Holder extends StatusDisplayItem.Holder<ErrorStatusDisplayItem> {
private final Button openInBrowserButton;
public Holder(Context context, ViewGroup parent) {
super(context, R.layout.display_item_error, parent);
openInBrowserButton=findViewById(R.id.button_open_browser);
openInBrowserButton.setOnClickListener(v -> UiUtils.launchWebBrowser(v.getContext(), item.status.url));
findViewById(R.id.button_copy_error_details).setOnClickListener(this::copyErrorDetails);
}
@Override
public void onBind(ErrorStatusDisplayItem item) {
openInBrowserButton.setEnabled(item.status!=null && item.status.url!=null);
}
private void copyErrorDetails(View v) {
StringWriter stringWriter=new StringWriter();
PrintWriter printWriter=new PrintWriter(stringWriter);
item.exception.printStackTrace(printWriter);
String stackTrace=stringWriter.toString();
String errorDetails=String.format(
"App Version: %s\nOS Version: %s\nStatus URL: %s\nException: %s",
v.getContext().getString(R.string.mo_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
"Android " + Build.VERSION.RELEASE,
item.status.url,
stackTrace
);
UiUtils.copyText(v, errorDetails);
}
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
@@ -94,6 +95,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status;
favorites.setText(getFormattedPlural(R.plurals.x_favorites, item.status.favouritesCount));
favorites.setCompoundDrawablesRelativeWithIntrinsicBounds(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular, 0, 0, 0);
reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, item.status.reblogsCount));
if(s.editedAt!=null){
editHistory.setVisibility(View.VISIBLE);

View File

@@ -200,18 +200,10 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onReplyClick(View v){
if(item.status.preview) return;
if(item.status.isRemote){
UiUtils.lookupStatus(v.getContext(),
item.status, item.accountID, null,
status -> {
UiUtils.opacityIn(v);
openComposeView(status, item.accountID);
}
);
return;
}
UiUtils.opacityIn(v);
openComposeView(item.status, item.accountID);
applyInteraction(v, status -> {
UiUtils.opacityIn(v);
openComposeView(status, item.accountID);
});
}
private boolean onReplyLongClick(View v) {
@@ -243,22 +235,13 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
onBoostLongClick(v);
return;
}
if(item.status.isRemote){
UiUtils.lookupStatus(v.getContext(),
item.status, item.accountID, null,
status -> {
if(status == null)
return;
boost.setSelected(!status.reblogged);
vibrateForAction(boost, !status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(status, !status.reblogged, null, r->boostConsumer(v, r));
}
);
return;
}
boost.setSelected(!item.status.reblogged);
vibrateForAction(boost, !item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
applyInteraction(v, status -> {
if(status == null)
return;
boost.setSelected(!status.reblogged);
vibrateForAction(boost, !status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(status, !status.reblogged, null, r->boostConsumer(v, r));
});
}
private void boostConsumer(View v, Status r) {
@@ -275,22 +258,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
Consumer<StatusPrivacy> doReblog = (visibility) -> {
UiUtils.opacityOut(v);
if(item.status.isRemote){
UiUtils.lookupStatus(v.getContext(),
item.status, item.accountID, null,
status -> {
session.getStatusInteractionController()
.setReblogged(status, !status.reblogged, visibility, r->boostConsumer(v, r));
boost.setSelected(status.reblogged);
dialog.dismiss();
}
);
} else {
applyInteraction(v,status -> {
session.getStatusInteractionController()
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
boost.setSelected(item.status.reblogged);
.setReblogged(status, !status.reblogged, visibility, r->boostConsumer(v, r));
boost.setSelected(status.reblogged);
dialog.dismiss();
}
});
};
View separator = menu.findViewById(R.id.separator);
@@ -364,33 +337,18 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onFavoriteClick(View v){
if(item.status.preview) return;
if(item.status.isRemote){
UiUtils.lookupStatus(v.getContext(),
item.status, item.accountID, null,
status -> {
if(status == null)
return;
favorite.setSelected(!status.favourited);
vibrateForAction(favorite, !status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(status, !status.favourited, r->{
if (status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) {
v.startAnimation(spin);
}
UiUtils.opacityIn(v);
bindText(favorites, r.favouritesCount);
});
}
);
return;
}
favorite.setSelected(!item.status.favourited);
vibrateForAction(favorite, !item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
if (item.status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) {
v.startAnimation(spin);
}
UiUtils.opacityIn(v);
bindText(favorites, r.favouritesCount);
applyInteraction(v, status -> {
if(status == null)
return;
favorite.setSelected(!status.favourited);
vibrateForAction(favorite, !status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(status, !status.favourited, r->{
if (status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) {
v.startAnimation(spin);
}
UiUtils.opacityIn(v);
bindText(favorites, r.favouritesCount);
});
});
}
@@ -411,26 +369,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onBookmarkClick(View v){
if(item.status.preview) return;
if(item.status.isRemote){
UiUtils.lookupStatus(v.getContext(),
item.status, item.accountID, null,
status -> {
if(status == null)
return;
bookmark.setSelected(!status.bookmarked);
vibrateForAction(bookmark, !status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked, r->{
UiUtils.opacityIn(v);
});
}
);
return;
}
bookmark.setSelected(!item.status.bookmarked);
vibrateForAction(bookmark, !item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
UiUtils.opacityIn(v);
});
applyInteraction(v,
status -> {
if(status == null)
return;
bookmark.setSelected(!status.bookmarked);
vibrateForAction(bookmark, !status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked, r->{
UiUtils.opacityIn(v);
});
});
}
private boolean onBookmarkLongClick(View v) {
@@ -451,10 +399,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void onShareClick(View v){
if(item.status.preview) return;
UiUtils.opacityIn(v);
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, item.status.url);
v.getContext().startActivity(Intent.createChooser(intent, v.getContext().getString(R.string.share_toot_title)));
UiUtils.openSystemShareSheet(v.getContext(), item.status);
}
private boolean onShareLongClick(View v){
@@ -477,25 +422,37 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
return 0;
}
private void applyInteraction(View v, Consumer<Status> interactionConsumer) {
if(!item.status.isRemote){
interactionConsumer.accept(item.status);
return;
}
UiUtils.lookupStatus(v.getContext(),
item.status, item.accountID, null,
interactionConsumer
);
}
private static void vibrateForAction(View view, boolean isPositive) {
if (!GlobalUserPreferences.hapticFeedback) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.performHapticFeedback(isPositive ? HapticFeedbackConstants.CONFIRM : HapticFeedbackConstants.REJECT);
} else {
Vibrator vibrator = view.getContext().getSystemService(Vibrator.class);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
vibrator.vibrate(VibrationEffect.createPredefined(isPositive ? VibrationEffect.EFFECT_CLICK : VibrationEffect.EFFECT_DOUBLE_CLICK));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect = isPositive
? VibrationEffect.createOneShot(75L, 128)
: VibrationEffect.createWaveform(new long[]{0L, 75L, 75L, 75L}, new int[]{0, 128, 0, 128}, -1);
vibrator.vibrate(effect);
} else {
if (isPositive) vibrator.vibrate(75L);
else vibrator.vibrate(new long[]{0L, 75L, 75L, 75L}, -1);
}
Vibrator vibrator = view.getContext().getSystemService(Vibrator.class);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
vibrator.vibrate(VibrationEffect.createPredefined(isPositive ? VibrationEffect.EFFECT_CLICK : VibrationEffect.EFFECT_DOUBLE_CLICK));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect = isPositive
? VibrationEffect.createOneShot(75L, 128)
: VibrationEffect.createWaveform(new long[]{0L, 75L, 75L, 75L}, new int[]{0, 128, 0, 128}, -1);
vibrator.vibrate(effect);
} else {
if (isPositive) vibrator.vibrate(75L);
else vibrator.vibrate(new long[]{0L, 75L, 75L, 75L}, -1);
}
}
}

View File

@@ -175,11 +175,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
fragment.removeNotification(item.notification);
}
}));
collapseBtn.setOnClickListener(l -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
collapseBtn.setOnClickListener(l -> item.parentFragment.onToggleExpanded(item.status, item.isForQuote, getItemID()));
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic())
optionsMenu.getMenu().setGroupDividerEnabled(true);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
@@ -289,7 +289,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
}else if(id==R.id.share){
UiUtils.openSystemShareSheet(activity, item.status.url);
UiUtils.openSystemShareSheet(activity, item.status);
}else if(id==R.id.open_with_account){
UiUtils.pickAccount(item.parentFragment.getActivity(), item.accountID, R.string.sk_open_with_account, R.drawable.ic_fluent_person_swap_24_regular, session ->UiUtils.openURL(
item.parentFragment.getActivity(), session.getID(), item.status.url, false
), null);
}
return true;
});
@@ -418,7 +422,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void setImage(int index, Drawable drawable){
if(index>0){
item.emojiHelper.setImageDrawable(index-1, drawable);
name.invalidate();
name.setText(name.getText());
}else{
avatar.setImageDrawable(drawable);
}
@@ -489,17 +493,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
Account account=item.user;
Menu menu=optionsMenu.getMenu();
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
SubMenu accountsMenu = openWithAccounts != null ? openWithAccounts.getSubMenu() : null;
if (hasMultipleAccounts && accountsMenu != null) {
openWithAccounts.setVisible(true);
accountsMenu.clear();
UiUtils.populateAccountsMenu(item.accountID, accountsMenu, s-> UiUtils.openURL(
item.parentFragment.getActivity(), s.getID(), item.status.url, false
));
} else if (openWithAccounts != null) {
openWithAccounts.setVisible(false);
}
String username = account.getShortUsername();
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);

View File

@@ -8,7 +8,6 @@ import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -18,12 +17,15 @@ import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Optional;
import java.util.regex.Matcher;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
private final UrlImageLoaderRequest imgRequest;
@@ -142,7 +144,35 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
}
private void onClick(View v){
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url);
String url=item.status.card.url;
// Mastodon.social sometimes adds an additional redirect page
// this is really disruptive on mobile, especially since it breaks the loopUp/openURL functionality
Uri parsedURL=Uri.parse(url);
if(parsedURL.getPath()!=null && parsedURL.getPath().startsWith("/redirect/")){
url=findRedirectedURL(parsedURL).orElse(url);
}
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), url);
}
private Optional<String> findRedirectedURL(Uri url){
// find actually linked url in status content
Matcher matcher=HtmlParser.URL_PATTERN.matcher(item.status.content);
boolean isAccountRedirect=url.getPath().startsWith("/redirect/accounts");
String foundURL;
while(matcher.find()){
foundURL=matcher.group(3);
if(TextUtils.isEmpty(matcher.group(4)))
foundURL="http://"+foundURL;
// SAFETY: Cannot be null, as otherwise the matcher wouldn't find it
// also, group is marked as non-null
assert foundURL!=null && url.getLastPathSegment()!=null;
if(foundURL.endsWith(url.getLastPathSegment()) ||
(isAccountRedirect && foundURL.matches("https://"+url.getHost()+"/@[a-zA-Z0-9_]+@[a-zA-Z0-9._]+$"))){
// found correct URL
return Optional.of(foundURL);
}
}
return Optional.empty();
}
}
}

View File

@@ -141,7 +141,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
text.invalidate();
text.setText(text.getText());
}
if(image instanceof Animatable)
((Animatable) image).start();

View File

@@ -1,10 +1,15 @@
package org.joinmastodon.android.ui.displayitems;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
@@ -13,12 +18,10 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Collections;
import java.util.Locale;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
@@ -29,7 +32,8 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private CharSequence translatedText;
public final Poll.Option option;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private boolean showResults;
public boolean showResults;
public boolean isAnimating;
private float votesFraction; // 0..1
private boolean isMostVoted;
private final int optionIndex;
@@ -80,6 +84,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private final View button;
private final ImageView icon;
private final Drawable progressBg;
private static final int ANIMATION_DURATION=500;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_poll_option, parent);
@@ -121,12 +126,17 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
}
text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), android.R.attr.textColorPrimary));
percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer));
if (item.isAnimating) {
showResults(item.showResults);
item.isAnimating= false;
}
}
@Override
public void setImage(int index, Drawable image){
item.emojiHelper.setImageDrawable(index, image);
text.invalidate();
text.setText(text.getText());
if(image instanceof Animatable){
((Animatable) image).start();
}
@@ -135,7 +145,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
@Override
public void clearImage(int index){
item.emojiHelper.setImageDrawable(index, null);
text.invalidate();
text.setText(text.getText());
}
private void onButtonClick(View v){
@@ -145,7 +155,34 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
public void showResults(boolean shown) {
item.showResults = shown;
item.calculateResults();
rebind();
Drawable bg=progressBg;
long animationDuration = (long) (ANIMATION_DURATION*item.votesFraction);
int startLevel=shown ? 0 : progressBg.getLevel();
int targetLevel=shown ? Math.round(10000f*item.votesFraction) : 0;
ObjectAnimator animator=ObjectAnimator.ofInt(bg, "level", startLevel, targetLevel);
animator.setDuration(animationDuration);
animator.setInterpolator(new DecelerateInterpolator());
button.setBackground(bg);
if(shown){
itemView.setSelected(item.poll.ownVotes!=null && item.poll.ownVotes.contains(item.optionIndex));
// animate percent
percent.setVisibility(View.VISIBLE);
ValueAnimator percentAnimation=ValueAnimator.ofInt(0, Math.round(100f*item.votesFraction));
percentAnimation.setDuration(animationDuration);
percentAnimation.setInterpolator(new DecelerateInterpolator());
percentAnimation.addUpdateListener(animation -> percent.setText(String.format(Locale.getDefault(), "%d%%", (int) animation.getAnimatedValue())));
percentAnimation.start();
}else{
animator.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
}
});
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
percent.setVisibility(View.GONE);
}
animator.start();
}
}
}

View File

@@ -152,8 +152,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
int firstHelperCount=item.emojiHelper.getImageCount();
CustomEmojiHelper helper=index<firstHelperCount ? item.emojiHelper : item.extra.emojiHelper;
helper.setImageDrawable(firstHelperCount>0 ? index%firstHelperCount : index, image);
text.invalidate();
extraText.invalidate();
text.setText(text.getText());
extraText.setText(extraText.getText());
}
@Override

View File

@@ -114,7 +114,7 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
@Override
public void setImage(int index, Drawable image){
item.emojiHelper.setImageDrawable(index, image);
title.invalidate();
title.setText(title.getText());
}
@Override

View File

@@ -10,13 +10,17 @@ import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
@@ -28,30 +32,35 @@ import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
@@ -79,6 +88,10 @@ public abstract class StatusDisplayItem{
public static final int FLAG_IS_FOR_QUOTE=1 << 7;
public static final int FLAG_NO_MEDIA_PREVIEW=1 << 8;
private final static Pattern QUOTE_MENTION_PATTERN=Pattern.compile("(?:<p>)?\\s?(?:RE:\\s?(<br\\s?\\/?>)?)?<a href=\"https:\\/\\/[^\"]+\"[^>]*><span class=\"invisible\">https:\\/\\/<\\/span><span class=\"ellipsis\">[^<]+<\\/span><span class=\"invisible\">[^<]+<\\/span><\\/a>(?:<\\/p>)?$");
private final static Pattern QUOTE_PATTERN=Pattern.compile("https://[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,8}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$");
public void setAncestryInfo(
boolean hasDescendantNeighbor,
boolean hasAncestoringNeighbor,
@@ -141,6 +154,7 @@ public abstract class StatusDisplayItem{
case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type);
case SECTION_HEADER -> null; // new SectionHeaderStatusDisplayItem.Holder(activity, parent);
case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent);
case ERROR_ITEM -> new ErrorStatusDisplayItem.Holder(activity, parent);
case DUMMY -> new DummyStatusDisplayItem.Holder(activity);
};
}
@@ -166,205 +180,202 @@ public abstract class StatusDisplayItem{
Status statusForContent=status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus s ? s : null;
try{
ScheduledStatus scheduledStatus=parentObject instanceof ScheduledStatus s ? s : null;
// Hide statuses that have a filter action of hide
if(!new StatusFilterPredicate(accountID, filterContext, FilterAction.HIDE).test(status))
return new ArrayList<StatusDisplayItem>() ;
HeaderStatusDisplayItem header=null;
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
HeaderStatusDisplayItem header=null;
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
if((flags&FLAG_NO_HEADER)==0){
ReblogOrReplyLineStatusDisplayItem replyLine=null;
boolean threadReply=statusForContent.inReplyToAccountId!=null &&
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
if((flags & FLAG_NO_HEADER)==0){
ReblogOrReplyLineStatusDisplayItem replyLine = null;
boolean threadReply = statusForContent.inReplyToAccountId != null &&
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
Account account=knownAccounts.get(statusForContent.inReplyToAccountId);
replyLine=buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
}
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
if(status.reblog!=null){
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
statusForContent.rebloggedBy=status.account;
String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName());
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, null, status, status.account));
}else if(!(status.tags.isEmpty() ||
fragment instanceof HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment
) && fragment.getParentFragment() instanceof HomeTabFragment home){
home.getHashtags().stream()
.filter(followed->status.tags.stream()
.anyMatch(hashtag->followed.name.equalsIgnoreCase(hashtag.name)))
.findAny()
// post contains a hashtag the user is following
.ifPresent(hashtag->items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, hashtag.name, List.of(),
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag),
status
)));
}
if(replyLine!=null){
Optional<ReblogOrReplyLineStatusDisplayItem> primaryLine=items.stream()
.filter(i->i instanceof ReblogOrReplyLineStatusDisplayItem)
.map(ReblogOrReplyLineStatusDisplayItem.class::cast)
.findFirst();
if(primaryLine.isPresent()){
primaryLine.get().extra=replyLine;
}else{
items.add(replyLine);
}
}
if((flags&FLAG_CHECKABLE)!=0)
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
else
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus));
}
if(status.reblog!=null){
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
LegacyFilter applyingFilter=null;
if(status.filtered!=null){
ArrayList<FilterResult> filters= new ArrayList<>(status.filtered);
statusForContent.rebloggedBy = status.account;
// Only add client filters if there are no pre-existing status filter
if(filters.isEmpty())
filters.addAll(AccountSessionManager.get(accountID).getClientSideFilters(status));
String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName());
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, null, status, status.account));
} else if (!(status.tags.isEmpty() ||
fragment instanceof HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment
) && fragment.getParentFragment() instanceof HomeTabFragment home) {
home.getHashtags().stream()
.filter(followed -> status.tags.stream()
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
.findAny()
// post contains a hashtag the user is following
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, hashtag.name, List.of(),
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag),
status
)));
}
if (replyLine != null) {
Optional<ReblogOrReplyLineStatusDisplayItem> primaryLine = items.stream()
.filter(i -> i instanceof ReblogOrReplyLineStatusDisplayItem)
.map(ReblogOrReplyLineStatusDisplayItem.class::cast)
.findFirst();
if (primaryLine.isPresent()) {
primaryLine.get().extra = replyLine;
} else {
items.add(replyLine);
for(FilterResult filter : filters){
LegacyFilter f=filter.filter;
if(f.isActive() && filterContext!=null && f.context.contains(filterContext)){
applyingFilter=f;
break;
}
}
}
if((flags & FLAG_CHECKABLE)!=0)
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
else
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus));
}
ArrayList<StatusDisplayItem> contentItems;
if(statusForContent.hasSpoiler()){
if(AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed=true;
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
if((flags&FLAG_IS_FOR_QUOTE)!=0){
for(StatusDisplayItem item : spoilerItem.contentItems){
item.isForQuote=true;
}
}
items.add(spoilerItem);
contentItems=spoilerItem.contentItems;
}else{
contentItems=items;
}
LegacyFilter applyingFilter=null;
if(status.filtered!=null){
for(FilterResult filter:status.filtered){
LegacyFilter f=filter.filter;
if(f.isActive() && filterContext != null && f.context.contains(filterContext)){
applyingFilter=f;
break;
if(statusForContent.quote!=null){
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
if(quoteInlineIndex!=-1)
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
else {
// hide non-official quote patters
Matcher matcher=QUOTE_MENTION_PATTERN.matcher(status.content);
if(matcher.find()){
String quoteMention=matcher.group();
statusForContent.content=statusForContent.content.replace(quoteMention, "");
}
}
}
// Moshidon
if(applyingFilter==null){
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, FilterAction.WARN);
predicate.test(status);
applyingFilter = predicate.getApplyingFilter();
}
}
ArrayList<StatusDisplayItem> contentItems;
if(statusForContent.hasSpoiler()){
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
if((flags & FLAG_IS_FOR_QUOTE)!=0){
for(StatusDisplayItem item:spoilerItem.contentItems){
item.isForQuote=true;
}
}
items.add(spoilerItem);
contentItems=spoilerItem.contentItems;
}else{
contentItems=items;
}
if(statusForContent.quote!=null) {
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
if (quoteInlineIndex!=-1)
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
}
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
if(!TextUtils.isEmpty(statusForContent.content)){
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
contentItems.add(text);
}else if(!hasSpoiler && header!=null){
header.needBottomPadding=true;
}else if(hasSpoiler){
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
}
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty() && (flags & FLAG_NO_MEDIA_PREVIEW)==0){
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant);
for (Attachment att : imageAttachments) {
if (att.blurhashPlaceholder == null) {
att.blurhashPlaceholder = new ColorDrawable(color);
}
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
statusForContent.sensitiveRevealed=false;
statusForContent.sensitive=true;
} else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
statusForContent.sensitiveRevealed=true;
contentItems.add(mediaGrid);
}
if((flags & FLAG_NO_MEDIA_PREVIEW)!=0){
contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent));
}
for(Attachment att:statusForContent.mediaAttachments){
if(att.type==Attachment.Type.AUDIO){
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
}
if(att.type==Attachment.Type.UNKNOWN){
contentItems.add(new FileStatusDisplayItem(parentID, fragment, att));
}
}
if(statusForContent.poll!=null){
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
}
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags & FLAG_NO_MEDIA_PREVIEW)==0));
}
if(statusForContent.quote!=null && !(parentObject instanceof Notification)){
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
if(!TextUtils.isEmpty(statusForContent.content)){
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
if(applyingFilter!=null)
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent, (flags&FLAG_NO_TRANSLATE)!=0);
contentItems.add(text);
}else if(!hasSpoiler && header!=null){
header.needBottomPadding=true;
}else if(hasSpoiler){
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE));
}
if(contentItems!=items && statusForContent.spoilerRevealed){
items.addAll(contentItems);
}
AccountLocalPreferences lp=fragment.getLocalPrefs();
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
statusForContent.reactions!=null){
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
}
FooterStatusDisplayItem footer=null;
if((flags & FLAG_NO_FOOTER)==0){
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
footer.hideCounts=hideCounts;
items.add(footer);
}
boolean inset=(flags & FLAG_INSET)!=0;
boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0;
// add inset dummy so last content item doesn't clip out of inset bounds
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){
items.add(new DummyStatusDisplayItem(parentID, fragment));
// in case we ever need the dummy to display a margin for the media grid again:
// (i forgot why we apparently don't need this anymore)
// !contentItems.isEmpty() && contentItems
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
}
GapStatusDisplayItem gap=null;
if((flags & FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment))
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
int i=1;
for(StatusDisplayItem item:items){
if(inset)
item.inset=true;
if(isForQuote){
item.status=statusForContent;
item.isForQuote=true;
}
item.index=i++;
}
if(items!=contentItems && !statusForContent.spoilerRevealed){
for(StatusDisplayItem item:contentItems){
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty() && (flags&FLAG_NO_MEDIA_PREVIEW)==0){
int color=UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant);
for(Attachment att : imageAttachments){
if(att.blurhashPlaceholder==null){
att.blurhashPlaceholder=new ColorDrawable(color);
}
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags&FLAG_MEDIA_FORCE_HIDDEN)!=0){
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
statusForContent.sensitiveRevealed=false;
statusForContent.sensitive=true;
}else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
statusForContent.sensitiveRevealed=true;
contentItems.add(mediaGrid);
}
if((flags&FLAG_NO_MEDIA_PREVIEW)!=0){
contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent));
}
for(Attachment att : statusForContent.mediaAttachments){
if(att.type==Attachment.Type.AUDIO){
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
}
if(att.type==Attachment.Type.UNKNOWN){
contentItems.add(new FileStatusDisplayItem(parentID, fragment, att));
}
}
if(statusForContent.poll!=null){
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
}
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags&FLAG_NO_MEDIA_PREVIEW)==0));
}
if(statusForContent.quote!=null && (flags & FLAG_INSET)==0){
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER|FLAG_INSET|FLAG_NO_EMOJI_REACTIONS|FLAG_IS_FOR_QUOTE));
} else if((flags & FLAG_INSET)==0 && statusForContent.mediaAttachments.isEmpty()){
tryAddNonOfficialQuote(statusForContent, fragment, accountID, filterContext);
}
if(contentItems!=items && statusForContent.spoilerRevealed){
items.addAll(contentItems);
}
AccountLocalPreferences lp=fragment.getLocalPrefs();
if((flags&FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
statusForContent.reactions!=null){
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
}
FooterStatusDisplayItem footer=null;
if((flags&FLAG_NO_FOOTER)==0){
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
footer.hideCounts=hideCounts;
items.add(footer);
}
boolean inset=(flags&FLAG_INSET)!=0;
boolean isForQuote=(flags&FLAG_IS_FOR_QUOTE)!=0;
// add inset dummy so last content item doesn't clip out of inset bounds
if((inset || footer==null) && (flags&FLAG_CHECKABLE)==0 && !isForQuote){
items.add(new DummyStatusDisplayItem(parentID, fragment));
// in case we ever need the dummy to display a margin for the media grid again:
// (i forgot why we apparently don't need this anymore)
// !contentItems.isEmpty() && contentItems
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
}
GapStatusDisplayItem gap=null;
if((flags&FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment))
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
int i=1;
for(StatusDisplayItem item : items){
if(inset)
item.inset=true;
if(isForQuote){
@@ -373,15 +384,31 @@ public abstract class StatusDisplayItem{
}
item.index=i++;
}
}
if(items!=contentItems && !statusForContent.spoilerRevealed){
for(StatusDisplayItem item : contentItems){
if(inset)
item.inset=true;
if(isForQuote){
item.status=statusForContent;
item.isForQuote=true;
}
item.index=i++;
}
}
List<StatusDisplayItem> nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items;
WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null :
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter);
return applyingFilter==null ? items : new ArrayList<>(gap!=null
? List.of(warning, gap)
: Collections.singletonList(warning)
);
List<StatusDisplayItem> nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items;
WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null :
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter);
if(warning!=null)
warning.inset=inset;
return applyingFilter==null ? items : new ArrayList<>(gap!=null
? List.of(warning, gap)
: Collections.singletonList(warning)
);
} catch(Exception e) {
Log.e("StatusDisplayItem", "buildItems: failed to build StatusDisplayItem " + e);
return new ArrayList<>(Collections.singletonList(new ErrorStatusDisplayItem(parentID, statusForContent, fragment, e)));
}
}
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> items){
@@ -393,6 +420,60 @@ public abstract class StatusDisplayItem{
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll, status));
}
/**
* Tries to adds a non-official quote to a status.
* A non-official quote is a quote on an instance that does not support quotes officially.
*/
private static void tryAddNonOfficialQuote(Status status, BaseStatusListFragment fragment, String accountID, FilterContext filterContext) {
Matcher matcher=QUOTE_PATTERN.matcher(status.getStrippedText());
if(!matcher.find())
return;
String quoteURL=matcher.group();
if (!UiUtils.looksLikeFediverseUrl(quoteURL))
return;
new GetSearchResults(quoteURL, GetSearchResults.Type.STATUSES, true, null, 0, 0).setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults results){
AccountSessionManager.get(accountID).filterStatuses(results.statuses, filterContext);
if (results.statuses == null || results.statuses.isEmpty())
return;
Status quote=results.statuses.get(0);
new GetAccountRelationships(Collections.singletonList(quote.account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> relationships){
if(relationships.isEmpty())
return;
Relationship relationship=relationships.get(0);
String selfId=AccountSessionManager.get(accountID).self.id;
if(!status.account.id.equals(selfId) && (relationship.domainBlocking || relationship.muting || relationship.blocking)) {
// do not show posts that are quoting a muted/blocked user
fragment.removeStatus(status);
return;
}
status.quote=results.statuses.get(0);
fragment.updateStatusWithQuote(status);
}
@Override
public void onError(ErrorResponse error){}
})
.exec(accountID);
}
@Override
public void onError(ErrorResponse error){
Log.w("StatusDisplayItem", "onError: failed to find quote status with URL: " + quoteURL + " " + error);
}
}).exec(accountID);
}
public enum Type{
HEADER,
REBLOG_OR_REPLY_LINE,
@@ -417,6 +498,7 @@ public abstract class StatusDisplayItem{
SECTION_HEADER,
HEADER_CHECKABLE,
NOTIFICATION_HEADER,
ERROR_ITEM,
FILTER_SPOILER,
DUMMY
}

View File

@@ -101,7 +101,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
float textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height);
collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight);
wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, item.isForQuote, getItemID()));
}
@Override
@@ -155,7 +155,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
boolean expandable = tooBig && !item.status.hasSpoiler();
item.parentFragment.onEnableExpandable(Holder.this, expandable);
item.parentFragment.onEnableExpandable(Holder.this, expandable, item.isForQuote);
}
boolean expandButtonShown=item.status.textExpandable && !item.status.textExpanded;
@@ -173,7 +173,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public void setImage(int index, Drawable image){
getEmojiHelper().setImageDrawable(index, image);
text.invalidate();
text.setText(text.getText());
if(image instanceof Animatable){
((Animatable) image).start();
if(image instanceof MovieDrawable)
@@ -184,7 +184,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public void clearImage(int index){
getEmojiHelper().setImageDrawable(index, null);
text.invalidate();
text.setText(text.getText());
}
private CustomEmojiHelper getEmojiHelper(){

View File

@@ -8,14 +8,12 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.AltTextFilter;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import java.util.List;
// Mind the gap!
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
public List<StatusDisplayItem> filteredItems;
@@ -24,8 +22,8 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, LegacyFilter applyingFilter){
super(parentID, parentFragment);
this.status=status;
this.filteredItems = filteredItems;
this.applyingFilter = applyingFilter;
this.filteredItems=filteredItems;
this.applyingFilter=applyingFilter;
}
@Override
@@ -33,31 +31,34 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
return Type.WARNING;
}
public static class Holder extends StatusDisplayItem.Holder<WarningFilteredStatusDisplayItem>{
public final View warningWrap;
public final Button showBtn;
public final TextView text;
public List<StatusDisplayItem> filteredItems;
public static class Holder extends StatusDisplayItem.Holder<WarningFilteredStatusDisplayItem>{
public final View warningWrap;
public final Button showBtn;
public final TextView text;
public List<StatusDisplayItem> filteredItems;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_warning, parent);
warningWrap=findViewById(R.id.warning_wrap);
showBtn=findViewById(R.id.reveal_btn);
showBtn.setOnClickListener(i -> item.parentFragment.onWarningClick(this));
itemView.setOnClickListener(v->item.parentFragment.onWarningClick(this));
text=findViewById(R.id.text);
}
@Override
public void onBind(WarningFilteredStatusDisplayItem item) {
filteredItems = item.filteredItems;
String title = item.applyingFilter instanceof AltTextFilter ? item.parentFragment.getString(R.string.sk_no_alt_text) : item.applyingFilter.title;
text.setText(item.parentFragment.getString(R.string.sk_filtered, title));
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_warning, parent);
warningWrap=findViewById(R.id.warning_wrap);
showBtn=findViewById(R.id.reveal_btn);
showBtn.setOnClickListener(i->item.parentFragment.onWarningClick(this));
itemView.setOnClickListener(v->item.parentFragment.onWarningClick(this));
text=findViewById(R.id.text);
}
@Override
public void onClick(){
@Override
public void onBind(WarningFilteredStatusDisplayItem item){
filteredItems=item.filteredItems;
String title=item.applyingFilter.title;
text.setText(item.parentFragment.getString(R.string.sk_filtered, title));
}
}
if(item.inset){
itemView.setClipToOutline(true);
itemView.setOutlineProvider(OutlineProviders.roundedRect(8));
}
}
@Override
public void onClick(){}
}
}

View File

@@ -59,7 +59,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.utils.FileProvider;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileOutputStream;
@@ -207,32 +207,32 @@ public class PhotoViewer implements ZoomPanView.Listener{
toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
if(status!=null) {
toolbar.getMenu()
.add(R.string.download)
.setIcon(R.drawable.ic_fluent_arrow_download_24_regular)
.setOnMenuItemClickListener(item -> {
saveCurrentFile();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.getMenu()
.add(R.string.button_share)
.setIcon(R.drawable.ic_fluent_share_24_regular)
.setOnMenuItemClickListener(item -> {
shareCurrentFile();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
if(status!=null){
toolbar.getMenu()
.add(R.string.info)
.setIcon(R.drawable.ic_fluent_info_24_regular)
.setOnMenuItemClickListener(item -> {
.setOnMenuItemClickListener(item->{
showInfoSheet();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
} else {
toolbar.getMenu()
.add(R.string.download)
.setIcon(R.drawable.ic_fluent_arrow_download_24_regular)
.setOnMenuItemClickListener(item -> {
saveCurrentFile();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.getMenu()
.add(R.string.button_share)
.setIcon(R.drawable.ic_fluent_share_24_regular)
.setOnMenuItemClickListener(item -> {
shareCurrentFile();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
uiOverlay.setAlpha(0f);
@@ -482,40 +482,29 @@ public class PhotoViewer implements ZoomPanView.Listener{
private void shareCurrentFile(){
Attachment att=attachments.get(pager.getCurrentItem());
Intent intent = new Intent(Intent.ACTION_SEND);
if(att.type==Attachment.Type.IMAGE){
UrlImageLoaderRequest req=new UrlImageLoaderRequest(att.url);
try{
File file=ImageCache.getInstance(activity).getFile(req);
if(file==null){
shareAfterDownloading(att);
return;
}
MastodonAPIController.runInBackground(()->{
File imageDir = new File(activity.getCacheDir(), ".");
File renamedFile;
file.renameTo(renamedFile = new File(imageDir, Uri.parse(att.url).getLastPathSegment()));
Uri outputUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", renamedFile);
// setting type to image
intent.setType(mimeTypeForFileName(outputUri.getLastPathSegment()));
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
// calling startactivity() to share
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.button_share)));
});
}catch(IOException x){
Log.w(TAG, "shareCurrentFile: ", x);
Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show();
}
}else{
if(att.type!=Attachment.Type.IMAGE){
shareAfterDownloading(att);
return;
}
UrlImageLoaderRequest req=new UrlImageLoaderRequest(att.url);
try{
File file=ImageCache.getInstance(activity).getFile(req);
if(file==null){
shareAfterDownloading(att);
return;
}
MastodonAPIController.runInBackground(()->{
File imageDir=new File(activity.getCacheDir(), ".");
File renamedFile;
file.renameTo(renamedFile=new File(imageDir, Uri.parse(att.url).getLastPathSegment()));
shareFile(renamedFile);
});
}catch(IOException x){
Log.w(TAG, "shareCurrentFile: ", x);
Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show();
}
}
private void saveCurrentFile(){
@@ -625,6 +614,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
private void shareAfterDownloading(Attachment att){
Uri uri=Uri.parse(att.url);
Toast.makeText(activity, R.string.downloading, Toast.LENGTH_SHORT).show();
MastodonAPIController.runInBackground(()->{
try {
OkHttpClient client = new OkHttpClient();
@@ -649,20 +640,22 @@ public class PhotoViewer implements ZoomPanView.Listener{
outputStream.close();
inputStream.close();
Intent intent = new Intent(Intent.ACTION_SEND);
Uri outputUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", file);
intent.setType(mimeTypeForFileName(outputUri.getLastPathSegment()));
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.button_share)));
shareFile(file);
} catch(IOException e){
Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show();
}
});
}
private void shareFile(@NonNull File file) {
Intent intent = new Intent(Intent.ACTION_SEND);
Uri outputUri = UiUtils.getFileProviderUri(activity, file);
intent.setDataAndType(outputUri, mimeTypeForFileName(outputUri.getLastPathSegment()));
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.button_share)));
}
private void onAudioFocusChanged(int change){
if(change==AudioManager.AUDIOFOCUS_LOSS || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK){
pauseVideo();
@@ -790,17 +783,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setFavorited(status, !status.favourited, r->{});
}
}else if(id==R.id.btn_share){
if(status!=null){
shareCurrentFile();
}
// }else if(id==R.id.btn_share){
// if(status!=null){
// shareCurrentFile();
// }
}else if(id==R.id.btn_bookmark){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked);
}
}else if(id==R.id.btn_download){
saveCurrentFile();
}
// else if(id==R.id.btn_download){
// saveCurrentFile();
// }
}
});
sheet.setStatus(status);

View File

@@ -102,9 +102,9 @@ public class PhotoViewerInfoSheet extends BottomSheet{
boostBtn.setOnClickListener(clickListener);
favoriteBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_share).setOnClickListener(clickListener);
// findViewById(R.id.btn_share).setOnClickListener(clickListener);
bookmarkBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_download).setOnClickListener(clickListener);
// findViewById(R.id.btn_download).setOnClickListener(clickListener);
}
private void showAltTextHelp(){

View File

@@ -62,7 +62,7 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountSwitcherSheet extends BottomSheet{
private final Activity activity;
private final HomeFragment fragment;
private final boolean externalShare, openInApp;
private final boolean accountChooser, openInApp;
private BiConsumer<String, Boolean> onClick;
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
@@ -71,17 +71,22 @@ public class AccountSwitcherSheet extends BottomSheet{
private Runnable onLoggedOutCallback;
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
this(activity, fragment, false, false);
this(activity, fragment, 0, 0, null, false);
}
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp){
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, @DrawableRes int headerIcon, @StringRes int headerTitle, String exceptFor, boolean openInApp){
super(activity);
this.activity=activity;
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.accountChooser=headerTitle!=0;
// currently there is only one use case for a end row button (openInApp)
// if more are needed ti should be generified
this.openInApp=openInApp;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream()
.filter(accountSession -> !accountSession.getID().equals(exceptFor))
.map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
@@ -95,20 +100,21 @@ public class AccountSwitcherSheet extends BottomSheet{
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
if (externalShare) {
if (accountChooser) {
FrameLayout shareHeading = new FrameLayout(activity);
activity.getLayoutInflater().inflate(R.layout.item_external_share_heading, shareHeading);
((TextView) shareHeading.findViewById(R.id.title)).setText(openInApp
? R.string.sk_external_share_or_open_title
: R.string.sk_external_share_title);
((ImageView) shareHeading.findViewById(R.id.icon)).setImageDrawable(getContext().getDrawable(headerIcon));
((TextView) shareHeading.findViewById(R.id.title)).setText(getContext().getString(headerTitle));
adapter.addAdapter(new SingleViewRecyclerAdapter(shareHeading));
setOnDismissListener((d) -> activity.finish());
// we're using the sheet for interactAs picking, so the activity should not be closed
setOnDismissListener(exceptFor!=null ? null : (d) -> activity.finish());
}
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
if (!externalShare) {
if (!accountChooser) {
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
Nav.go(activity, CustomWelcomeFragment.class, null);
dismiss();
@@ -301,9 +307,9 @@ public class AccountSwitcherSheet extends BottomSheet{
public void onBind(AccountSession item){
HtmlParser.setTextWithCustomEmoji(name, item.self.getDisplayName(), item.self.emojis);
username.setText(item.getFullUsername());
radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);
if (externalShare) view.setCheckable(false);
radioButton.setVisibility(accountChooser ? View.GONE : View.VISIBLE);
extraBtnWrap.setVisibility(accountChooser && openInApp ? View.VISIBLE : View.GONE);
if (accountChooser) view.setCheckable(false);
else {
String accountId = fragment != null
? fragment.getAccountID()
@@ -338,7 +344,8 @@ public class AccountSwitcherSheet extends BottomSheet{
onClick.accept(item.getID(), false);
return;
}
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null){
AccountSessionManager accountSessionManager=AccountSessionManager.getInstance();
if(accountSessionManager.tryGetAccount(item.getID())!=null && !view.isChecked()){
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
((MainActivity)activity).restartActivity();
}
@@ -346,7 +353,7 @@ public class AccountSwitcherSheet extends BottomSheet{
@Override
public boolean onLongClick(){
if (externalShare) return false;
if (accountChooser) return false;
confirmLogOut(item.getID());
return true;
}

View File

@@ -24,7 +24,8 @@ public class CustomEmojiSpan extends ReplacementSpan{
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
return Math.round(paint.descent()-paint.ascent());
int size = Math.round(paint.descent()-paint.ascent());
return drawable!=null ? (int) (drawable.getIntrinsicWidth()*(size/(float) drawable.getIntrinsicHeight())) : size;
}
@Override
@@ -45,7 +46,8 @@ public class CustomEmojiSpan extends ReplacementSpan{
}
canvas.save();
canvas.translate(x, top);
canvas.scale(size/(float)dw, size/(float)dh, 0f, 0f);
float scale = size/(float)dh;
canvas.scale(scale, scale, 0f, 0f);
drawable.draw(canvas);
canvas.restore();
}
@@ -56,7 +58,6 @@ public class CustomEmojiSpan extends ReplacementSpan{
}
public UrlImageLoaderRequest createImageLoaderRequest(){
int size=V.dp(20);
return new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? emoji.url : emoji.staticUrl, size, size);
return new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? emoji.url : emoji.staticUrl, 0, V.dp(20));
}
}

View File

@@ -123,6 +123,9 @@ public class HtmlParser{
int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success);
int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error);
if(source.endsWith("\n"))
source=source.stripTrailing();
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
@@ -137,14 +140,12 @@ public class HtmlParser{
String href=el.attr("href");
LinkSpan.Type linkType;
String text=el.text();
if(el.hasClass("hashtag")){
if(text.startsWith("#")){
linkType=LinkSpan.Type.HASHTAG;
href=text.substring(1);
linkObject=tagsByTag.get(text.substring(1).toLowerCase());
}else{
linkType=LinkSpan.Type.URL;
}
if(el.hasClass("hashtag") || text.startsWith("#")){
// MOSHIDON: we have slightly refactored this so that the hashtags properly work in akkoma
// TODO: upstream this
linkType=LinkSpan.Type.HASHTAG;
href=text.substring(1);
linkObject=tagsByTag.get(text.substring(1).toLowerCase());
}else if(el.hasClass("mention")){
String id=idsByUrl.get(href);
if(id!=null){
@@ -321,12 +322,11 @@ public class HtmlParser{
}
public static void applyFilterHighlights(Context context, SpannableStringBuilder text, List<FilterResult> filters){
if (filters == null) return;
int fgColor=UiUtils.getThemeColor(context, R.attr.colorM3Error);
int bgColor=UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer);
for(FilterResult filter:filters){
if(!filter.filter.isActive())
continue;;
continue;
for(String word:filter.keywordMatches){
Matcher matcher=Pattern.compile("\\b"+Pattern.quote(word)+"\\b", Pattern.CASE_INSENSITIVE).matcher(text);
while(matcher.find()){

View File

@@ -48,6 +48,8 @@ public class CustomEmojiHelper{
}
public void setImageDrawable(int image, Drawable drawable){
if(spans.isEmpty())
return;
for(CustomEmojiSpan span:spans.get(image)){
span.setDrawable(drawable);
}

View File

@@ -11,6 +11,7 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import java.util.List;
@@ -42,13 +43,16 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
if(inset){
if(rect.isEmpty()){
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder){
rect.set(child.getX(), i == 0 && pos > 0 && displayItems.get(pos - 1).inset ? V.dp(-10) : child.getY(), child.getX() + child.getWidth(), child.getY() + child.getHeight() + V.dp(4));
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof WarningFilteredStatusDisplayItem.Holder){
float topInset=i == 0 && pos > 0 && displayItems.get(pos - 1).inset ? V.dp(-10) : child.getY();
if(holder instanceof WarningFilteredStatusDisplayItem.Holder)
topInset-=V.dp(4);
rect.set(child.getX(), topInset, child.getX() + child.getWidth(), child.getY() + child.getHeight() + V.dp(4));
}else {
rect.set(child.getX(), i == 0 && pos > 0 && displayItems.get(pos - 1).inset ? V.dp(-10) : child.getY(), child.getX() + child.getWidth(), child.getY() + child.getHeight());
}
}else{
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder){
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof WarningFilteredStatusDisplayItem.Holder){
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight()) + V.dp(4);
}else {
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());

View File

@@ -52,8 +52,6 @@ import android.util.Pair;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
@@ -63,7 +61,6 @@ import android.view.ViewPropertyAnimator;
import android.view.WindowInsets;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
@@ -72,11 +69,10 @@ import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.E;
import org.joinmastodon.android.FileProvider;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController;
@@ -90,7 +86,6 @@ import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
@@ -121,19 +116,21 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Searchable;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.BlockAccountConfirmationSheet;
import org.joinmastodon.android.ui.sheets.MuteAccountConfirmationSheet;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.utils.Tracking;
import org.parceler.Parcels;
import java.io.File;
import java.lang.reflect.Field;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.IDN;
import java.net.URI;
@@ -154,7 +151,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -181,6 +177,7 @@ import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
@@ -200,6 +197,8 @@ public class UiUtils {
}
public static void launchWebBrowser(Context context, String url) {
if(GlobalUserPreferences.removeTrackingParams)
url=Tracking.removeTrackingParameters(url);
try {
if (GlobalUserPreferences.useCustomTabs) {
new CustomTabsIntent.Builder()
@@ -418,7 +417,6 @@ public class UiUtils {
CustomEmojiSpan[] spans = text.getSpans(0, text.length(), CustomEmojiSpan.class);
if (spans.length == 0)
return;
int emojiSize = V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji = Arrays.stream(spans).collect(Collectors.groupingBy(s -> s.emoji));
for (Map.Entry<Emoji, List<CustomEmojiSpan>> emoji : spansByEmoji.entrySet()) {
ViewImageLoader.load(new ViewImageLoader.Target() {
@@ -429,14 +427,14 @@ public class UiUtils {
for (CustomEmojiSpan span : emoji.getValue()) {
span.setDrawable(d);
}
view.invalidate();
view.setText(view.getText());
}
@Override
public View getView() {
return view;
}
}, null, new UrlImageLoaderRequest(emoji.getKey().url, emojiSize, emojiSize), null, false, true);
}, null, new UrlImageLoaderRequest(emoji.getKey().url, 0, V.dp(20)), null, false, true);
}
}
@@ -935,17 +933,20 @@ public class UiUtils {
}
public static void handleFollowRequest(Activity activity, Account account, String accountID, @Nullable String notificationID, boolean accepted, Relationship relationship, Consumer<Relationship> resultCallback) {
public static void handleFollowRequest(Activity activity, Account account, String accountID, @Nullable String notificationID, boolean accepted, Relationship relationship, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback) {
progressCallback.accept(true);
if (accepted) {
new AuthorizeFollowRequest(account.id).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship rel) {
E.post(new FollowRequestHandledEvent(accountID, true, account, rel));
progressCallback.accept(false);
resultCallback.accept(rel);
}
@Override
public void onError(ErrorResponse error) {
progressCallback.accept(false);
resultCallback.accept(relationship);
error.showToast(activity);
}
@@ -957,11 +958,13 @@ public class UiUtils {
E.post(new FollowRequestHandledEvent(accountID, false, account, rel));
if (notificationID != null)
E.post(new NotificationDeletedEvent(notificationID));
progressCallback.accept(false);
resultCallback.accept(rel);
}
@Override
public void onError(ErrorResponse error) {
progressCallback.accept(false);
resultCallback.accept(relationship);
error.showToast(activity);
}
@@ -1206,18 +1209,9 @@ public class UiUtils {
}
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
List<AccountSession> sessions = AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s -> !s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems(
sessions.stream().map(AccountSession::getFullUsername).toArray(String[]::new),
(dialog, which) -> sessionConsumer.accept(sessions.get(which))
)
.setTitle(titleRes == 0 ? R.string.choose_account : titleRes)
.setIcon(iconRes);
if (transformDialog != null) transformDialog.accept(builder);
builder.show();
AccountSwitcherSheet sheet = new AccountSwitcherSheet((Activity) context, null, iconRes, titleRes == 0 ? R.string.choose_account : titleRes, exceptFor, false);
sheet.setOnClick((accountId, open) ->sessionConsumer.accept(AccountSessionManager.get(accountId)));
sheet.show();
}
public static void restartApp() {
@@ -1354,10 +1348,6 @@ public class UiUtils {
openURL(context, accountID, url, true);
}
public static void openURL(Context context, String accountID, String url, Object parentObject) {
openURL(context, accountID, url, !(parentObject instanceof Status || parentObject instanceof Account));
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, (clazz, args) -> {
if (clazz == null) {
@@ -1475,7 +1465,7 @@ public class UiUtils {
return;
}
Optional<Account> account = results.accounts.stream()
.filter(a -> uri.equals(Uri.parse(a.url))).findAny();
.filter(a -> uri.getPath().contains(a.username)).findAny();
if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
go.accept(ProfileFragment.class, args);
@@ -1497,6 +1487,8 @@ public class UiUtils {
}
public static void copyText(View v, String text) {
if(GlobalUserPreferences.removeTrackingParams)
text=Tracking.cleanUrlsInText(text);
Context context = v.getContext();
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()) { // Android 13+ SystemUI shows its own thing when you put things into the clipboard
@@ -1523,6 +1515,10 @@ public class UiUtils {
return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"));
}
public static boolean isMagic() {
return !TextUtils.isEmpty(getSystemProperty("ro.build.version.magic"));
}
public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
@@ -1543,7 +1539,7 @@ public class UiUtils {
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
UiUtils.pickAccount(activity, accountID, 0, R.drawable.ic_fluent_compose_28_regular, session -> {
args.putString("account", session.getID());
Nav.go(activity, ComposeFragment.class, args);
}, null);
@@ -1645,17 +1641,6 @@ public class UiUtils {
return intent;
}
public static void populateAccountsMenu(String excludeAccountID, Menu menu, Consumer<AccountSession> onClick) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
sessions.stream().filter(s -> !s.getID().equals(excludeAccountID)).forEach(s -> {
String username = "@"+s.self.username+"@"+s.domain;
menu.add(username).setOnMenuItemClickListener((c) -> {
onClick.accept(s);
return true;
});
});
}
public static void showFragmentForNotification(Context context, Notification n, String accountID, Bundle extras) {
if (extras == null) extras = new Bundle();
extras.putString("account", accountID);
@@ -1754,10 +1739,48 @@ public class UiUtils {
}
}
public static void openSystemShareSheet(Context context, String url){
public static Uri getFileProviderUri(Context context, File file){
return FileProvider.getUriForFile(context, context.getPackageName()+".fileprovider", file);
}
public static void openSystemShareSheet(Context context, Object obj){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
Account account;
String url;
String previewTitle;
if(obj instanceof Account acc){
account=acc;
url=acc.url;
previewTitle=context.getString(R.string.share_sheet_preview_profile, account.displayName);
}else if(obj instanceof Status st){
account=st.account;
url=st.url;
String postText=st.getStrippedText();
if(TextUtils.isEmpty(postText)){
previewTitle=context.getString(R.string.share_sheet_preview_profile, account.displayName);
}else{
if(postText.length()>100)
postText=postText.substring(0, 100)+"...";
previewTitle=context.getString(R.string.share_sheet_preview_post, account.displayName, postText);
}
}else{
throw new IllegalArgumentException("Unsupported share object type");
}
intent.putExtra(Intent.EXTRA_TEXT, url);
intent.putExtra(Intent.EXTRA_TITLE, previewTitle);
ImageCache cache=ImageCache.getInstance(context);
try{
File ava=cache.getFile(new UrlImageLoaderRequest(account.avatarStatic));
if(ava==null || !ava.exists())
ava=cache.getFile(new UrlImageLoaderRequest(account.avatar));
if(ava!=null && ava.exists()){
intent.setClipData(ClipData.newRawUri(null, getFileProviderUri(context, ava)));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}catch(IOException ignore){}
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_toot_title)));
}

View File

@@ -113,7 +113,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
contextMenu.inflate(R.menu.profile);
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
menuButton.setOnClickListener(v->showMenuFromButton());
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic())
contextMenu.getMenu().setGroupDividerEnabled(true);
UiUtils.enablePopupMenuIcons(fragment.getContext(), contextMenu);
@@ -148,9 +148,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
pronouns.setVisibility(pronounsString.isPresent() ? View.VISIBLE : View.GONE);
pronounsString.ifPresent(p -> HtmlParser.setTextWithCustomEmoji(pronouns, p, item.account.emojis));
if(item.account.bot) {
botIcon.setVisibility(View.VISIBLE);
}
botIcon.setVisibility(item.account.bot ? View.VISIBLE : View.GONE);
/* unused in megalodon
boolean hasVerifiedLink=item.verifiedLink!=null;
@@ -187,8 +185,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
bio.invalidate();
name.setText(name.getText());
bio.setText(bio.getText());
}
if(image instanceof Animatable a && !a.isRunning())
@@ -298,10 +296,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
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);
fragment.startActivity(Intent.createChooser(intent, item.getTitle()));
UiUtils.openSystemShareSheet(fragment.getActivity(), account);
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(fragment.getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){

View File

@@ -95,7 +95,7 @@ public class MastodonLanguage {
private final MastodonLanguage fallbackLanguage;
public LanguageResolver(Instance instanceInfo) {
String fallbackLanguageTag = (instanceInfo.languages != null && !instanceInfo.languages.isEmpty()) ? instanceInfo.languages.get(0) : ENGLISH.languageTag;
String fallbackLanguageTag = (instanceInfo != null && instanceInfo.languages != null && !instanceInfo.languages.isEmpty()) ? instanceInfo.languages.get(0) : ENGLISH.languageTag;
fallbackLanguage = allLanguages.stream()
.filter(l->l.languageTag.equalsIgnoreCase(fallbackLanguageTag)).findAny()
.orElse(ENGLISH);

View File

@@ -1,120 +0,0 @@
package org.joinmastodon.android.utils;
import static org.joinmastodon.android.model.FilterAction.HIDE;
import static org.joinmastodon.android.model.FilterAction.WARN;
import static org.joinmastodon.android.model.FilterContext.ACCOUNT;
import static org.joinmastodon.android.model.FilterContext.HOME;
import static org.joinmastodon.android.model.FilterContext.NOTIFICATIONS;
import static org.joinmastodon.android.model.FilterContext.PUBLIC;
import static org.joinmastodon.android.model.FilterContext.THREAD;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.AltTextFilter;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// TODO: This whole class has been ditched upstream. I plan to eventually refactor it to only have the still relevant clientFilters code
public class StatusFilterPredicate implements Predicate<Status>{
private final List<LegacyFilter> clientFilters;
private final List<LegacyFilter> filters;
private final FilterContext context;
private final FilterAction action;
private LegacyFilter applyingFilter;
/**
* @param context null makes the predicate pass automatically
* @param action defines what the predicate should check:
* status should not be hidden or should not display with warning
*/
public StatusFilterPredicate(List<LegacyFilter> filters, FilterContext context, FilterAction action){
this.filters = filters;
this.context = context;
this.action = action;
this.clientFilters = getClientFilters();
}
public StatusFilterPredicate(List<LegacyFilter> filters, FilterContext context){
this(filters, context, HIDE);
}
/**
* @param context null makes the predicate pass automatically
* @param action defines what the predicate should check:
* status should not be hidden or should not display with warning
*/
public StatusFilterPredicate(String accountID, FilterContext context, FilterAction action){
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
this.context = context;
this.action = action;
this.clientFilters = getClientFilters();
}
private List<LegacyFilter> getClientFilters() {
List<LegacyFilter> filters = new ArrayList<>();
if(!GlobalUserPreferences.showPostsWithoutAlt) {
filters.add(new AltTextFilter(WARN, HOME, PUBLIC, ACCOUNT, THREAD, NOTIFICATIONS));
}
return filters;
}
/**
* @param context null makes the predicate pass automatically
*/
public StatusFilterPredicate(String accountID, FilterContext context){
this(accountID, context, HIDE);
}
/**
* @return whether the status should be displayed without being hidden/warned about.
* will always return true if the context is null.
* true = display this status,
* false = filter this status
*/
@Override
public boolean test(Status status){
if (context == null) return true;
Stream<LegacyFilter> matchingFilters = status.filtered != null
// use server-provided per-status info (status.filtered) if available
? status.filtered.stream().map(f -> f.filter)
// or fall back to cached filters
: filters.stream().filter(filter -> filter.matches(status));
Optional<LegacyFilter> applyingFilter = matchingFilters
// discard expired filters
.filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now()))
// only apply filters for given context
.filter(filter -> filter.context.contains(context))
// treating filterAction = null (from filters list) as FilterAction.HIDE
.filter(filter -> filter.filterAction == null ? action == HIDE : filter.filterAction == action)
.findAny();
//Apply client filters if no server filter is triggered
if (applyingFilter.isEmpty() && !clientFilters.isEmpty()) {
applyingFilter = clientFilters.stream()
.filter(filter -> filter.context.contains(context))
.filter(filter -> filter.filterAction == null ? action == HIDE : filter.filterAction == action)
.filter(filter -> filter.matches(status))
.findAny();
}
this.applyingFilter = applyingFilter.orElse(null);
return applyingFilter.isEmpty();
}
public LegacyFilter getApplyingFilter() {
return applyingFilter;
}
}

View File

@@ -0,0 +1,107 @@
package org.joinmastodon.android.utils;
import android.net.Uri;
import android.util.Patterns;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.regex.Matcher;
// Inspired by https://github.com/GeopJr/Tuba/blob/91a036edff9ab1ffb38d5b54a33023e5db551051/src/Utils/Tracking.vala
public class Tracking{
/* https://github.com/brave/brave-core/blob/face8d58ab81422480c8c05b9ba5d518e1a2d227/components/query_filter/utils.cc#L23-L119 */
private static final String[] TRACKING_IDS={
// Strip any utm_ based ones
"utm_",
// https://github.com/brave/brave-browser/issues/4239
"fbclid", "gclid", "msclkid", "mc_eid",
// New Facebook one
"mibexid",
// https://github.com/brave/brave-browser/issues/9879
"dclid",
// https://github.com/brave/brave-browser/issues/13644
"oly_anon_id", "oly_enc_id",
// https://github.com/brave/brave-browser/issues/11579
"_openstat",
// https://github.com/brave/brave-browser/issues/11817
"vero_conv", "vero_id",
// https://github.com/brave/brave-browser/issues/13647
"wickedid",
// https://github.com/brave/brave-browser/issues/11578
"yclid",
// https://github.com/brave/brave-browser/issues/8975
"__s",
// https://github.com/brave/brave-browser/issues/17451
"rb_clickid",
// https://github.com/brave/brave-browser/issues/17452
"s_cid",
// https://github.com/brave/brave-browser/issues/17507
"ml_subscriber", "ml_subscriber_hash",
// https://github.com/brave/brave-browser/issues/18020
"twclid",
// https://github.com/brave/brave-browser/issues/18758
"gbraid", "wbraid",
// https://github.com/brave/brave-browser/issues/9019
"_hsenc", "__hssc", "__hstc", "__hsfp", "hsCtaTracking",
// https://github.com/brave/brave-browser/issues/22082
"oft_id", "oft_k", "oft_lk", "oft_d", "oft_c", "oft_ck", "oft_ids", "oft_sk",
// https://github.com/brave/brave-browser/issues/11580
"igshid",
// Instagram Threads
"ad_id", "adset_id", "campaign_id", "ad_name", "adset_name", "campaign_name", "placement",
// Reddit
"share_id", "ref", "ref_share",
};
/**
* Tries to remove tracking parameters from a URL.
*
* @param url The original URL with tracking parameters
* @return The URL with the tracking parameters removed.
*/
@NonNull
public static String removeTrackingParameters(@NonNull String url){
Uri uri=Uri.parse(url);
if(uri==null || !uri.isHierarchical())
return url;
Uri.Builder uriBuilder=uri.buildUpon().clearQuery();
// Iterate over existing parameters and add them back if they are not tracking parameters
for(String paramName : uri.getQueryParameterNames()){
if(!isTrackingParameter(paramName)){
for(String paramValue : uri.getQueryParameters(paramName)){
uriBuilder.appendQueryParameter(paramName, paramValue);
}
}
}
return uriBuilder.build().toString();
}
/**
* Cleans URLs within the provided text, removing the tracking parameters from them.
*
* @param text The text that may contain URLs.
* @return The given text with cleaned URLs.
*/
public static String cleanUrlsInText(String text){
Matcher matcher=Patterns.WEB_URL.matcher(text);
StringBuffer sb=new StringBuffer();
while(matcher.find()){
String url=matcher.group();
matcher.appendReplacement(sb, removeTrackingParameters(url));
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* Returns true if the given parameter is used for tracking.
*/
private static boolean isTrackingParameter(String parameter){
return Arrays.stream(TRACKING_IDS).anyMatch(trackingId->parameter.toLowerCase().contains(trackingId));
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2.75,4.504a0.75,0.75 0,0 1,0.743 0.648l0.007,0.102v13.499a0.75,0.75 0,0 1,-1.493 0.101L2,18.753v-13.5a0.75,0.75 0,0 1,0.75 -0.75ZM15.21,6.387 L15.293,6.293a1,1 0,0 1,1.32 -0.083l0.094,0.083 4.997,4.998a1,1 0,0 1,0.083 1.32l-0.083,0.093 -4.996,5.004a1,1 0,0 1,-1.499 -1.32l0.083,-0.094L18.581,13L6,13a1,1 0,0 1,-0.993 -0.883L5,12a1,1 0,0 1,0.883 -0.993L6,11h12.584l-3.291,-3.293a1,1 0,0 1,-0.083 -1.32l0.083,-0.094 -0.083,0.094Z"
android:fillColor="#212121"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21.25,4.5a0.75,0.75 0,0 1,0.743 0.648L22,5.25v13.5a0.75,0.75 0,0 1,-1.493 0.102l-0.007,-0.102L20.5,5.25a0.75,0.75 0,0 1,0.75 -0.75ZM12.21,6.387 L12.293,6.293a1,1 0,0 1,1.32 -0.083l0.094,0.083 4.997,4.998a1,1 0,0 1,0.083 1.32l-0.083,0.093 -4.996,5.004a1,1 0,0 1,-1.499 -1.32l0.083,-0.094L15.581,13L3,13a1,1 0,0 1,-0.993 -0.883L2,12a1,1 0,0 1,0.883 -0.993L3,11h12.584l-3.291,-3.293a1,1 0,0 1,-0.083 -1.32l0.083,-0.094 -0.083,0.094Z"
android:fillColor="#212121"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M25.78,3.28C26.073,2.987 26.073,2.513 25.78,2.22C25.487,1.927 25.013,1.927 24.72,2.22L11.47,15.47L11.004,17L12.53,16.53L25.78,3.28ZM6.25,3C4.455,3 3,4.455 3,6.25V21.75C3,23.545 4.455,25 6.25,25H21.75C23.545,25 25,23.545 25,21.75V11.205C25,10.79 24.664,10.455 24.25,10.455C23.836,10.455 23.5,10.79 23.5,11.205V21.75C23.5,22.716 22.716,23.5 21.75,23.5H6.25C5.284,23.5 4.5,22.716 4.5,21.75V6.25C4.5,5.284 5.284,4.5 6.25,4.5H16.795C17.21,4.5 17.545,4.164 17.545,3.75C17.545,3.336 17.21,3 16.795,3H6.25Z"
android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.28,2.22a0.75,0.75 0,1 0,-1.06 1.06l0.127,0.127c-0.223,0.397 -0.35,0.855 -0.35,1.343v3.502l0.007,0.102a0.75,0.75 0,0 0,1.493 -0.102V4.75l0.007,-0.128 0.006,-0.051 3.675,3.675a7.573,7.573 0,0 0,-2.02 2.251,6.262 6.262,0 0,0 -0.358,0.716l-0.006,0.015c-0.002,0.005 -0.162,0.75 0.436,0.974a0.75,0.75 0,0 0,0.964 -0.436l0.001,-0.002 0.008,-0.02 0.044,-0.1c0.043,-0.09 0.11,-0.226 0.206,-0.391a6.072,6.072 0,0 1,1.8 -1.932l1.494,1.494a3.5,3.5 0,1 0,4.93 4.93l4.744,4.744a1.26,1.26 0,0 1,-0.18 0.013h-3.5l-0.103,0.007a0.75,0.75 0,0 0,0.102 1.493h3.5l0.168,-0.005a2.732,2.732 0,0 0,1.176 -0.345l0.128,0.128a0.75,0.75 0,0 0,1.061 -1.06L3.28,2.22ZM11.45,8.268 L10.122,6.94A9.051,9.051 0,0 1,12 6.75c2.726,0 4.535,1.1 5.655,2.22a7.573,7.573 0,0 1,1.18 1.527,6.294 6.294,0 0,1 0.34,0.67l0.018,0.046 0.006,0.015 0.002,0.005v0.002l0.001,0.002a0.75,0.75 0,0 1,-0.439 0.965,0.758 0.758,0 0,1 -0.965,-0.438l-0.008,-0.02s-0.023,-0.055 -0.044,-0.1a4.776,4.776 0,0 0,-0.206 -0.391,6.073 6.073,0 0,0 -0.945,-1.223c-0.88,-0.88 -2.32,-1.78 -4.595,-1.78a8.22,8.22 0,0 0,-0.55 0.018ZM21.997,18.815l-1.5,-1.5V15.75a0.75,0.75 0,0 1,1.493 -0.102l0.007,0.102v3.065ZM6.682,3.5 L5.182,2h3.065a0.75,0.75 0,0 1,0.102 1.493l-0.102,0.007H6.682ZM2.747,15a0.75,0.75 0,0 1,0.743 0.648l0.007,0.102v3.502l0.007,0.128a1.25,1.25 0,0 0,1.115 1.116l0.128,0.006h3.5l0.102,0.007a0.75,0.75 0,0 1,0 1.486l-0.102,0.007h-3.5l-0.167,-0.005a2.75,2.75 0,0 1,-2.578 -2.57l-0.005,-0.175V15.75l0.007,-0.102A0.75,0.75 0,0 1,2.747 15ZM19.247,2l0.168,0.005a2.75,2.75 0,0 1,2.577 2.57l0.005,0.175v3.502l-0.007,0.102a0.75,0.75 0,0 1,-1.486 0l-0.007,-0.102V4.75l-0.006,-0.128a1.25,1.25 0,0 0,-1.116 -1.116l-0.128,-0.006h-3.5l-0.102,-0.007a0.75,0.75 0,0 1,0 -1.486L15.747,2h3.5Z"
android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="103.97" android:viewportWidth="85.6" android:width="19.759546dp">
<path android:fillColor="@color/fluent_default_icon_tint" android:pathData="m74.46,0c-11.52,0 -18.66,8.37 -20.99,16.54 -1.17,4.08 -1.28,8.19 -0.1,11.64 1.18,3.45 4.25,6.36 8.26,6.36 4,0 7.83,-2.21 11.44,-5.17 3.61,-2.95 6.93,-6.79 9.28,-10.75 2.35,-3.96 3.91,-8.07 2.96,-12.02 -0.48,-1.97 -1.78,-3.83 -3.68,-4.96 -1.91,-1.13 -4.29,-1.63 -7.17,-1.63zM40.1,4c-2.84,0.56 -5.36,2.67 -6.65,5.04 -1.48,2.71 -1.87,5.83 -1.68,8.83 0.19,3 0.98,5.9 2.28,8.28 1.29,2.38 3.22,4.58 6.17,4.76 1.58,0.1 2.97,-0.57 4.03,-1.41 1.06,-0.84 1.92,-1.9 2.67,-3.11 1.52,-2.4 2.64,-5.4 3.26,-8.47 0.62,-3.08 0.76,-6.22 -0.2,-9.03 -0.96,-2.81 -4,-5.3 -7.22,-5.3h-0c-1.04,0.07 -1.77,0.19 -2.65,0.42zM74.46,5.29c2.25,0 3.68,0.42 4.46,0.88 0.78,0.47 1.06,0.89 1.24,1.66 0.37,1.53 -0.39,4.73 -2.37,8.08 -1.98,3.35 -4.99,6.82 -8.08,9.35 -2.76,2.26 -5.64,3.65 -7.48,3.92 -0.22,0.03 -0.42,0.05 -0.6,0.05 -1.77,0 -2.55,-0.75 -3.25,-2.78 -0.7,-2.04 -0.74,-5.24 0.18,-8.46 1.84,-6.45 6.89,-12.7 15.9,-12.7zM42.74,8.87c1.55,0 1.76,0.34 2.24,1.72 0.47,1.38 0.51,3.81 0.02,6.28 -0.49,2.47 -1.48,5 -2.55,6.69 -0.53,0.84 -1.09,1.47 -1.48,1.78 -0.39,0.31 -0.52,0.27 -0.42,0.28 -0.12,-0.01 -1.01,-0.46 -1.85,-2 -0.84,-1.54 -1.5,-3.83 -1.64,-6.09 -0.15,-2.26 0.23,-4.46 1.04,-5.95 0.81,-1.49 1.85,-2.38 3.97,-2.66 0.26,-0.03 0.48,-0.05 0.68,-0.05zM22.67,11.53c-0.99,0.02 -1.98,0.25 -2.95,0.66 -3.04,1.25 -4.97,3.81 -5.62,6.49 -0.65,2.68 -0.26,5.43 0.59,7.93 0.85,2.5 2.17,4.78 3.82,6.51 1.65,1.74 3.85,3.23 6.54,2.69 2.76,-0.56 3.98,-2.91 4.74,-5.16 0.76,-2.25 1.07,-4.85 0.98,-7.46 -0.08,-2.61 -0.54,-5.22 -1.7,-7.46 -1.16,-2.24 -3.49,-4.26 -6.4,-4.2zM22.79,16.82c0.69,-0.02 0.98,0.18 1.59,1.34 0.6,1.16 1.04,3.15 1.11,5.19 0.06,2.04 -0.23,4.17 -0.71,5.61 -0.48,1.44 -1.14,1.73 -0.78,1.66h-0v0c0.09,-0.02 -0.65,-0.1 -1.64,-1.15 -1,-1.05 -2.04,-2.77 -2.65,-4.57 -0.61,-1.8 -0.78,-3.65 -0.46,-4.98 0.32,-1.33 0.89,-2.18 2.49,-2.85 0.48,-0.2 0.82,-0.25 1.05,-0.26zM8.02,23.93c-1.45,-0.05 -2.92,0.46 -4.2,1.35 -2.49,1.72 -3.76,4.44 -3.82,6.99 -0.06,2.55 0.82,4.93 2.06,7 1.24,2.07 2.86,3.84 4.68,5.08 1.82,1.24 4.13,2.19 6.52,1.14 2.34,-1.03 3.02,-3.29 3.3,-5.32 0.28,-2.03 0.12,-4.26 -0.35,-6.46 -0.47,-2.2 -1.26,-4.37 -2.49,-6.19 -1.23,-1.82 -3.14,-3.5 -5.69,-3.58zM7.85,29.22c0.28,0.01 0.79,0.24 1.48,1.26 0.69,1.02 1.34,2.66 1.7,4.34 0.36,1.69 0.45,3.44 0.29,4.63 -0.16,1.13 -0.54,1.34 -0.2,1.19 0.19,-0.09 -0.37,0.03 -1.4,-0.67 -1.04,-0.7 -2.26,-1.99 -3.12,-3.43 -0.86,-1.43 -1.34,-2.99 -1.31,-4.15 0.03,-1.16 0.31,-1.92 1.54,-2.77 0.58,-0.4 0.86,-0.42 1.02,-0.42zM50.43,33.42c-8.43,-0.14 -18.01,1.86 -26.16,6.06 -8.15,4.21 -15,10.78 -17.01,19.79 -2.21,9.88 2.23,20.9 9.71,29.56 7.47,8.66 18.2,15.14 29.59,15.14 11.79,0 24.28,-9.92 26.76,-23.02v-0c0.3,-1.6 0.09,-3.25 -0.67,-4.57 -0.76,-1.32 -1.94,-2.22 -3.18,-2.8 -2.48,-1.16 -5.34,-1.3 -8.18,-1.01 -2.84,0.29 -5.66,1.05 -7.96,2.31 -1.15,0.63 -2.2,1.38 -3.03,2.42 -0.83,1.04 -1.41,2.53 -1.21,4.06 0.36,2.73 -0.54,4.08 -1.64,4.68 -1.1,0.6 -2.9,0.67 -5.28,-1.14 -2.11,-1.61 -2.94,-2.91 -3.16,-3.72 -0.22,-0.82 -0.11,-1.43 0.61,-2.51 1.44,-2.17 5.65,-5.13 10.54,-8.16 4.88,-3.02 10.41,-6.27 14.7,-10.25 4.29,-3.98 7.5,-9.12 6.48,-15.17 -0.68,-4.07 -3.67,-7.1 -7.43,-8.92 -3.76,-1.82 -8.41,-2.66 -13.47,-2.74zM50.34,38.71c4.5,0.08 8.5,0.88 11.26,2.22 2.76,1.33 4.16,2.94 4.51,5.03 0.65,3.84 -1.21,7.02 -4.86,10.41 -3.65,3.39 -8.94,6.56 -13.88,9.63 -4.95,3.06 -9.59,5.86 -12.16,9.73 -1.29,1.93 -1.97,4.42 -1.31,6.84 0.67,2.43 2.42,4.52 5.06,6.53 3.59,2.73 7.82,3.34 11.04,1.57 3.22,-1.77 4.9,-5.68 4.34,-10.01 0.01,0.06 -0.08,0.15 0.11,-0.09 0.19,-0.24 0.68,-0.67 1.41,-1.06 1.45,-0.79 3.75,-1.47 5.96,-1.69 2.21,-0.23 4.36,0.05 5.4,0.54 0.52,0.24 0.74,0.48 0.83,0.64 0.09,0.16 0.18,0.34 0.06,0.97 -1.93,10.22 -12.95,18.71 -21.56,18.71 -9.34,0 -18.91,-5.57 -25.58,-13.3 -6.67,-7.73 -10.22,-17.45 -8.54,-24.95 1.57,-7.04 7.03,-12.5 14.27,-16.24 7.24,-3.74 16.14,-5.6 23.64,-5.47z"/>
</vector>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginHorizontal="16dp"
android:padding="16dp"
android:clipToPadding="false"
android:background="@drawable/bg_settings_banner">
<ImageView
android:id="@+id/icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="16dp"
android:scaleType="center"
android:importantForAccessibility="no"
android:tint="?colorM3OnPrimaryContainer"
android:background="@drawable/white_circle"
android:backgroundTint="?colorM3PrimaryContainer"
android:src="@drawable/ic_fluent_warning_24_regular" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_toEndOf="@id/icon"
android:layout_marginBottom="2dp"
android:textAppearance="@style/m3_title_medium"
android:textColor="?colorM3OnSurface"
android:singleLine="true"
android:gravity="center_vertical"
android:text="@string/mo_error_display_title"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_below="@id/title"
android:textAppearance="@style/m3_body_medium"
android:minHeight="20dp"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurface"
android:text="@string/mo_error_display_text"/>
<Button
android:id="@+id/button_open_browser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text"
android:layout_toEndOf="@id/icon"
android:layout_marginStart="-16dp"
android:layout_marginBottom="-10dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:minWidth="0dp"
android:text="@string/open_in_browser"
tools:text="@string/resume_notifications_now"/>
<Button
android:id="@+id/button_copy_error_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text"
android:layout_toEndOf="@id/button_open_browser"
android:layout_marginBottom="-10dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:minWidth="0dp"
android:text="@string/mo_error_display_copy_error_details"/>
</RelativeLayout>

View File

@@ -29,8 +29,7 @@
android:layout_weight="1"
android:singleLine="true"
android:ellipsize="end"
android:visibility="visible"
/>
android:visibility="visible"/>
</LinearLayout>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/button_wrap"
android:layout_marginEnd="8dp"
android:textAppearance="@style/m3_headline_small"
android:textColor="?colorM3OnSurface"
android:maxLines="3"
android:ellipsize="end"
android:minHeight="48dp"
android:gravity="center_vertical"
tools:text="Microsoft Chose Profit Over Security"/>
<FrameLayout
android:id="@+id/button_wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/title"
android:layout_alignBottom="@id/title"
android:layout_alignParentEnd="true">
<Button
android:id="@+id/profile_action_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_gravity="center"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:paddingHorizontal="16dp"
android:text="@string/mo_trending_link_read"
tools:text="@string/mark_all_notifications_read" />
</FrameLayout>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_marginTop="4dp"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="@string/article_by_author"/>
</RelativeLayout>

View File

@@ -34,7 +34,7 @@
<Button
android:id="@+id/btn_boost"
android:layout_width="wrap_content"
android:layout_width="92dp"
android:layout_height="64dp"
android:text="@string/button_reblog"
android:drawableTop="@drawable/ic_boost"
@@ -47,24 +47,24 @@
<Button
android:id="@+id/btn_favorite"
android:layout_width="wrap_content"
android:layout_width="92dp"
android:layout_height="64dp"
android:text="@string/button_favorite"
android:drawableTop="@drawable/ic_fluent_star_24_selector"
style="@style/Widget.Mastodon.M3.Button.IconWithLabel"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<!-- <Space-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="1dp"-->
<!-- android:layout_weight="1"/>-->
<Button
android:id="@+id/btn_share"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:text="@string/button_share"
android:drawableTop="@drawable/ic_fluent_share_24_regular"
style="@style/Widget.Mastodon.M3.Button.IconWithLabel"/>
<!-- <Button-->
<!-- android:id="@+id/btn_share"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="64dp"-->
<!-- android:text="@string/button_share"-->
<!-- android:drawableTop="@drawable/ic_fluent_share_24_regular"-->
<!-- style="@style/Widget.Mastodon.M3.Button.IconWithLabel"/>-->
<Space
android:layout_width="0dp"
@@ -73,24 +73,24 @@
<Button
android:id="@+id/btn_bookmark"
android:layout_width="wrap_content"
android:layout_width="92dp"
android:layout_height="64dp"
android:text="@string/add_bookmark"
android:drawableTop="@drawable/ic_fluent_bookmark_24_selector"
style="@style/Widget.Mastodon.M3.Button.IconWithLabel"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<!-- <Space-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="1dp"-->
<!-- android:layout_weight="1"/>-->
<Button
android:id="@+id/btn_download"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:text="@string/download"
android:drawableTop="@drawable/ic_fluent_arrow_download_24_regular"
style="@style/Widget.Mastodon.M3.Button.IconWithLabel"/>
<!-- <Button-->
<!-- android:id="@+id/btn_download"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="64dp"-->
<!-- android:text="@string/download"-->
<!-- android:drawableTop="@drawable/ic_fluent_arrow_download_24_regular"-->
<!-- style="@style/Widget.Mastodon.M3.Button.IconWithLabel"/>-->
</LinearLayout>

View File

@@ -23,8 +23,6 @@
<!-- <item android:id="@+id/share" android:title="@string/button_share" android:icon="@drawable/ic_fluent_share_24_regular"/>-->
<item android:id="@+id/copy_link" android:title="@string/sk_copy_link_to_post" android:icon="@drawable/ic_fluent_link_24_regular"/>
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser" android:icon="@drawable/ic_fluent_globe_24_regular"/>
<item android:id="@+id/open_with_account" android:title="@string/sk_open_with_account" android:icon="@drawable/ic_fluent_person_swap_24_regular">
<menu android:id="@+id/accounts" />
</item>
<item android:id="@+id/open_with_account" android:title="@string/sk_open_with_account" android:icon="@drawable/ic_fluent_person_swap_24_regular"/>
</group>
</menu>

View File

@@ -15,8 +15,6 @@
<group android:id="@+id/menu_group3">
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser" android:icon="@drawable/ic_fluent_globe_24_regular"/>
<item android:id="@+id/share" android:title="@string/share_user" android:icon="@drawable/ic_fluent_share_24_regular"/>
<item android:id="@+id/open_with_account" android:title="@string/sk_open_with_account" android:visible="false" android:icon="@drawable/ic_fluent_person_swap_24_regular">
<menu android:id="@+id/accounts" />
</item>
<item android:id="@+id/open_with_account" android:title="@string/sk_open_with_account" android:visible="false" android:icon="@drawable/ic_fluent_person_swap_24_regular"/>
</group>
</menu>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/open_link"
android:icon="@drawable/ic_fluent_open_24_regular"
android:showAsAction="always"
android:title="@string/mo_trending_link_read"/>
</menu>

View File

@@ -34,4 +34,6 @@
<string name="mo_instance_info_moderated_servers">خوادم تحت الإشراف</string>
<string name="mo_donate_url">https://github.com/sponsors/LucasGGamerM</string>
<string name="mo_emoji_recent">المُستخدَمة حديثًا</string>
<string name="mo_muted_accounts">الحسابات المكتومة</string>
<string name="mo_blocked_accounts">الحسابات المحظورة</string>
</resources>

View File

@@ -264,7 +264,7 @@
<string name="sk_icon_academic_cap">قبعة جامعية</string>
<string name="sk_icon_tag">ملصقة</string>
<string name="sk_add_timeline_tag_error_empty">لا يجب أن يُترَك الوسم فارغًا</string>
<string name="sk_unfinished_attachments">إصلاح المرفقات؟</string>
<string name="sk_unfinished_attachments">تحميل المرفقات</string>
<plurals name="sk_posts_count_label">
<item quantity="zero">لا منشور</item>
<item quantity="one">منشور واحد</item>
@@ -293,7 +293,7 @@
<string name="sk_settings_forward_report_default">إعادة ”تحويل الإبلاغ“ افتراضيا</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_hide_fab">إخفاء زر التحرير مبدئيا</string>
<string name="sk_enter_emoji_hint">اضغط للتفاعل بوجوه تعبيرية</string>
<string name="sk_enter_emoji_hint">أكتب وجها تعبيريا أو قم ببحث</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_enter_emoji_toast">يُرجى إدخال إيموجي</string>
@@ -324,4 +324,20 @@
<string name="sk_settings_enable_marquee">تمكين تمرير النص في عناوين الأشرطة</string>
<string name="sk_settings_tabs_disable_swipe">تعطيل التمرير بين الألسِنة</string>
<string name="sk_settings_color_palette_default">افتراضي (%s)</string>
<string name="sk_poll_hide_results">إخفاء النتائج</string>
<string name="sk_poll_multiple_choice">خيارات متعددة</string>
<string name="sk_private_note_update_failed">فشل حفظ الملاحظة</string>
<string name="sk_delete_note">حذف الملاحظة الخاصة</string>
<string name="sk_settings_crash_log_unavailable">غير متوفر … بعد</string>
<string name="sk_add_note">إضافة ملاحظة خاصة</string>
<string name="sk_poll_show_results">إظهار النتائج</string>
<string name="sk_open_post_preview">معاينة المنشور</string>
<string name="sk_post_preview">معاينة</string>
<string name="sk_confirm_changes">أكِّد التغييرات</string>
<string name="sk_crash_log_copied">تم نسخ سجل الأعطال</string>
<string name="sk_post_scheduled">تم جدولة المنشور</string>
<string name="sk_alt_text_missing">مرفق واحد على الأقل ليس له وصف.</string>
<string name="sk_settings_auto_reveal_anyone">ردود مِن الجميع</string>
<string name="sk_favorited_as">تمت الإضافة إلى المفضلة كـ %s</string>
<string name="sk_bookmarked_as">تم وضع فاصل مرجعي كـ %s</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -24,4 +24,8 @@
<string name="mo_mute_label">Durada:</string>
<string name="mo_duration_minutes_5">5 minuts</string>
<string name="mo_mention_reblogger_automatically">En respondre, menciona automàticament el compte que ha impulsat la publicació</string>
<string name="mo_color_palette_black_and_white">Blanc i Negre</string>
<string name="mo_welcome_text">Per començar, per favor introduïu el nom de la vostra instància a continuació</string>
<string name="mo_no_image_desc">Les imatges incloses no tenen descripció. Penseu a afegir-ne un per permetre que les persones amb discapacitat visual hi puguin participar.</string>
<string name="mo_enable_dividers">Mostrar els separadors de les publicacions</string>
</resources>

View File

@@ -100,4 +100,25 @@
<string name="mo_confirm_to_mute_hashtag">Sind Sie sicher, dass Sie diesen Hashtag stummschalten möchten\?</string>
<string name="mo_filter_notifications">Benachrichtigungen filtern</string>
<string name="mo_notification_filter_reset">Auf Standardwerte zurücksetzen</string>
<string name="mo_instance_view_info">Serverinformationen anzeigen</string>
<string name="mo_mute_notifications">Benachrichtigungen dieses Users verstecken?</string>
<string name="mo_settings_remove_tracking_params">Private Links</string>
<string name="import_settings_title">Einstellungen importieren</string>
<string name="mo_error_display_title">Beitrag kann nicht angezeigt werden</string>
<string name="mo_error_display_copy_error_details">Details kopieren</string>
<string name="mo_trending_link_read">Lesen</string>
<string name="import_settings_summary">Zuvor exportierte Einstellungen und Timelines importieren</string>
<string name="mo_error_display_text">Beim Laden dieses Beitrags ist ein Fehler aufgetreten. Wenn das Problem weiterhin besteht, melde es bitte auf unserer Issues-Seite zusammen mit den Fehlerdetails.</string>
<string name="mo_settings_remove_tracking_params_summary">Tracking-Informationen aus Links entfernen</string>
<string name="export_settings_summary">Alle Einstellungen und Timelines der angemeldeten Konten exportieren</string>
<string name="mo_settings_unifiedpush_warning">UnifiedPush nicht aktiviert</string>
<string name="mo_settings_unifiedpush_warning_no_distributors">Keine UnifiedPush-Verteiler installiert. Du wirst keine Benachrichtigungen erhalten.</string>
<string name="mo_settings_unifiedpush_warning_disabled">UnifiedPush ist nicht aktiviert. Du wirst keine Benachrichtigungen erhalten.</string>
<string name="mo_settings_unifiedpush_enable">Aktivieren</string>
<string name="import_settings_confirm">Import der Einstellungen bestätigen?</string>
<string name="import_settings_confirm_body">Alle aktuellen Einstellungen und Timelines werden überschrieben! Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="import_settings_failed">Importieren der Einstellungen fehlgeschlagen</string>
<string name="export_settings_share">Einstellungen exportieren</string>
<string name="export_settings_fail">Exportieren der Einstellungen fehlgeschlagen</string>
<string name="export_settings_title">Einstellungen exportieren</string>
</resources>

View File

@@ -92,7 +92,7 @@
<string name="sk_bookmark_as">Lesezeichen mit anderem Konto</string>
<string name="sk_bookmarked_as">Lesezeichen gesetzt als %s</string>
<string name="sk_already_bookmarked">Bereits in den Lesezeichen</string>
<string name="sk_favorite_as">Favorit mit anderem Konto</string>
<string name="sk_favorite_as">Favorisieren mit anderem Konto</string>
<string name="sk_favorited_as">Favorisiert als %s</string>
<string name="sk_already_favorited">Bereits favorisiert</string>
<string name="sk_reblog_as">Mit einem anderen Konto teilen</string>
@@ -183,7 +183,7 @@
<string name="sk_icon_bot">Bot</string>
<string name="sk_icon_language">Sprache</string>
<string name="sk_icon_location">Standort</string>
<string name="sk_icon_microphone">Mikrophon</string>
<string name="sk_icon_microphone">Mikrofon</string>
<string name="sk_icon_microscope">Mikroskop</string>
<string name="sk_icon_keyboard">Keyboard</string>
<string name="sk_icon_coffee">Kaffee</string>
@@ -220,7 +220,7 @@
<string name="sk_icon_gauge">Gage</string>
<string name="sk_icon_headphones">Kopfhörer</string>
<string name="sk_icon_human">Mensch</string>
<string name="sk_icon_megaphone">Megaphon</string>
<string name="sk_icon_megaphone">Megafon</string>
<string name="sk_icon_light_bulb">Glühbirne</string>
<string name="sk_icon_globe">Globus</string>
<string name="sk_attach_file">Datei anhängen</string>
@@ -246,7 +246,7 @@
<string name="sk_reported">hat gemeldet</string>
<string name="sk_sign_ups">Registrierungen</string>
<string name="sk_new_reports">Neue Meldungen</string>
<string name="sk_settings_see_new_posts_button">Neue Beiträge anzeigen-Button</string>
<string name="sk_settings_see_new_posts_button">Neue Beiträge anzeigen-Button</string>
<string name="sk_settings_server_version">Server-Version: %s</string>
<string name="sk_notify_poll_results">Umfrage-Ergebnisse</string>
<string name="sk_settings_prefix_reply_cw_with_re">Voranstellen von „re:“ an CWs in Antworten an</string>
@@ -254,7 +254,7 @@
<string name="sk_expand">Erweitern</string>
<string name="sk_collapse">Einklappen</string>
<string name="sk_settings_collapse_long_posts">Sehr lange Beiträge einklappen</string>
<string name="sk_unfinished_attachments">Anhänge laden hoch</string>
<string name="sk_unfinished_attachments">Anhänge werden hochgeladen</string>
<string name="sk_settings_hide_interaction">Interaktions-Buttons verstecken</string>
<string name="sk_unfinished_attachments_message">Einige Anhänge sind nicht fertig hochgeladen.</string>
<string name="sk_followed_as">Mit %s gefolgt</string>
@@ -269,7 +269,7 @@
<string name="sk_notification_action_replied">Antwort an %s gesendet</string>
<string name="sk_show_thread">Thread öffnen</string>
<string name="sk_compact_reblog_reply_line">Kompakte Geteilt-/Geantwortet-Zeile</string>
<string name="sk_reply_line_above_avatar">Als Antwort auf-Zeile über Profilbild</string>
<string name="sk_reply_line_above_avatar">Als Antwort auf-Zeile über Profilbild</string>
<string name="sk_settings_confirm_before_reblog">Vor dem Teilen bestätigen</string>
<string name="sk_reacted">%s hat reagiert</string>
<string name="sk_reacted_with">%1$s hat mit %2$s reagiert</string>
@@ -290,10 +290,10 @@
<string name="sk_settings_default_content_type">Standard-Inhaltstyp</string>
<string name="sk_instance_info_unavailable">Informationen zur Instanz momentan nicht verfügbar</string>
<string name="sk_open_in_app">In App öffnen</string>
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string>
<string name="sk_settings_content_types_explanation">Dadurch kann beim Erstellen von Beiträgen ein Inhaltstyp wie z.B. Markdown angegeben werden. Nicht alle Instanzen unterstützen das.</string>
<string name="sk_settings_allow_remote_loading">Infos von Remote-Instanzen laden</string>
<string name="sk_no_remote_info_hint">keine Remote-Infos abrufbar</string>
<string name="sk_error_loading_profile">Konnte das Profil via %s nicht laden</string>
<string name="sk_error_loading_profile">Konnte das Profil nicht via %s laden</string>
<string name="sk_settings_allow_remote_loading_explanation">Für vollständigere Auflistung von Follower_innen, Likes und Boosts können die Informationen von der Ursprungs-Instanz geladen werden.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Automatisches Aufdecken von gleichen CWs in Antworten von</string>
<string name="sk_settings_auto_reveal_nobody">Nie</string>
@@ -342,7 +342,7 @@
<string name="sk_settings_continues_playback_summary">Musik im Hintergrund nicht pausieren, wenn in der App Medien abgespielt werden</string>
<string name="sk_settings_unifiedpush_choose">Verteiler auswählen</string>
<string name="sk_settings_display_pronouns_in_threads">Pronomen in Threads anzeigen</string>
<string name="sk_settings_display_pronouns_in_user_listings">Pronomen in User-Auflistungen anzeigen</string>
<string name="sk_settings_display_pronouns_in_user_listings">Pronomen in Nutzer-Auflistungen anzeigen</string>
<string name="sk_tab_home">Start</string>
<string name="sk_tab_search">Suche</string>
<string name="sk_tab_notifications">Benachrichtigungen</string>
@@ -423,7 +423,7 @@
<string name="sk_icon_sun">Sonne</string>
<string name="sk_icon_rain">Regen</string>
<string name="sk_icon_thunderstorm">Gewitter</string>
<string name="sk_private_note_update_failed">Notiz speichern fehlgeschlagen</string>
<string name="sk_private_note_update_failed">Speichern der Notiz fehlgeschlagen</string>
<string name="sk_delete_note">Private Notiz löschen</string>
<string name="sk_confirm_changes">Änderungen bestätigen</string>
<string name="sk_settings_crash_log_unavailable">Keines verfügbar… noch</string>
@@ -433,5 +433,8 @@
<string name="sk_open_post_preview">Vorschau öffnen</string>
<string name="sk_post_preview">Vorschau</string>
<string name="sk_private_note_confirm_delete">Private Notiz über %s löschen\?</string>
<string name="sk_poll_multiple_choice">Mehrfachauswahl</string>
<string name="sk_poll_multiple_choice">Mehrfachauswahl</string>
<string name="sk_poll_hide_results">Ergebnisse ausblenden</string>
<string name="sk_poll_show_results">Ergebnisse anzeigen</string>
<string name="sk_posted">%s gepostet</string>
</resources>

View File

@@ -1,4 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<string name="mo_personal_note_confirm">Επιβεβαίωσε τις αλλαγές για σημείωση</string>
<string name="mo_fab_compose">Σύνταξη</string>
<string name="mo_instance_admin">Διαχειρίζεται από</string>
<string name="mo_instance_contact">Επικοινωνία</string>
<string name="mo_personal_note_update_failed">Αποτυχία αποθήκευσης σημείωσης</string>
<string name="mo_settings_contribute">Συνείσφερε στο Moshidon</string>
<string name="mo_update_available">Το Moshidon %s είναι έτοιμο για λήψη.</string>
<string name="mo_update_ready">Το Moshidon %s έχει κατέβει και είναι έτοιμο για εγκατάσταση.</string>
<string name="mo_no_image_desc_title">Χωρίς περιγραφή εικόνας</string>
<string name="mo_no_image_desc">Οι εικόνες όυ περιλαμβάνονται δεν έχουν περιγραφή. Παρακαλώ σκέψου να προσθέσεις μία ώστε, να επιτρέψεις στα άτομα με προβλήματα όρασης να συμμετάσχουν.</string>
<string name="mo_emoji_recent">Χρησιμοποιήθηκε πρόσφατα</string>
<string name="mo_clear_recent_emoji">Εκκαθάριση πρόσφατων χρησιμοποιημένων emoji</string>
<string name="mo_disable_relocate_publish_button_to_enable_customization">Απενεργοποίησε το Μετακίνηση κουμπιού δημοσίευσης για να επιτρέψεις την παραμετροποίηση</string>
<string name="mo_severity_suspend">Αποκλεισμένοι</string>
<string name="mo_open_camera">Λήψη φωτογραφίας</string>
<string name="mo_confirm_unfollow">Επιβεβαίωση για άρση ακολούθησης %s</string>
<string name="mo_haptic_feedback">Απτική ανάδραση</string>
<string name="mo_relocate_publish_button">Μετακίνηση κουμπιού δημοσίευσης</string>
<string name="mo_hide_compose_button_while_scrolling_setting">Απόκρυψη κουμπιού σύνταξης κατά τη κύλιση</string>
<string name="mo_disable_reminder_to_add_alt_text">Απενεργοποίηση υπενθύμισης προσθήκης εναλλακτικού κειμένου</string>
<string name="mo_sending_error">Σφάλμα δημοσίευσης</string>
<string name="mo_color_palette_black_and_white">Άσπρο και Μαύρο</string>
<string name="mo_personal_note">Πρόσθεσε μια σημείωση για αυτό το προφίλ</string>
<string name="mo_filtered">Φιλτραρισμένα: %s</string>
<string name="mo_welcome_text">Για να ξεκινήσεις, παρακαλώ εισήγαγε το όνομα τομέα της οντότητας σου παρακάτω.</string>
<string name="mo_notification_management_settings">Διαχείριση Ειδοποιήσεων</string>
<string name="mo_color_palette_nord">Nord</string>
<string name="mo_camera_not_available">Δεν υπάρχει διαθέσιμη κάμερα!</string>
<string name="mo_add_custom_server_local_timeline">Προσθήκη τοπικής ροής ενός προσαρμοσμένου διακομιστή</string>
<string name="mo_muted_conversation_successfully">Έγινε επιτυχής σίγαση της συζήτησης</string>
<string name="mo_unmuted_conversation_successfully">Καταργήθηκε επιτυχώς η σίγαση της συζήτησης</string>
<string name="mo_confirm_to_unmute_conversation">Σίγουρα θε2 να καταργήσεις τη σίγαση αυτής της συζήτησης;</string>
<string name="mo_notification_action_replied">Απάντηση με επιτυχία στην ανάρτηση του χρήστη %s</string>
<string name="mo_duration_minutes_5">5 λεπτά</string>
<string name="mo_duration_minutes_30">30 λεπτά</string>
<string name="mo_duration_hours_1">1 ώρα</string>
<string name="mo_duration_days_1">1 ημέρα</string>
<string name="mo_duration_days_3">3 ημέρες</string>
<string name="mo_change_default_reply_visibility_to_unlisted">Απαντήση ως «Εκτός λίστας» από προεπιλογή</string>
<string name="mo_composer_behavior">Συμπεριφορά του Συντάκτη</string>
<string name="mo_download_latest_nightly_release">Εγκατάσταση τελευταίας έκδοσης nightly</string>
<string name="mo_mute_label">Διάρκεια:</string>
<string name="mo_severity_silence">Σε σίγαση</string>
<string name="mo_duration_hours_6">6 ώρες</string>
<string name="mo_share_open_url">Άνοιγμα στην Εφαρμογή</string>
<string name="mo_poll_option_add">Προσθήκη νέας επιλογής δημοσκόπησης</string>
<string name="mo_duration_indefinite">Άπειρη</string>
<string name="mo_duration_days_7">7 ημέρες</string>
<string name="mo_setting_haptic_feedback_summary">Δόνηση κατά την αλληλεπίδραση με αναρτήσεις</string>
<string name="mo_setting_true_black_summary">Ίσως εξοικονομήσει ενέργεια σε οθόνες AMOLED</string>
<string name="mo_swap_bookmark_with_reblog">Εναλλαγή σελιδοδείκτη με ενέργεια αναδημοσίευσης</string>
<string name="mo_show_media_preview">Εμφάνιση προεπισκόπησης πολυμέσων στις ροές</string>
<string name="mo_settings_unifiedpush_warning">UnifiedPush μη ενεργό</string>
<string name="mo_mention_reblogger_automatically">Αυτόματη επισήμανση λογαριασμού που αναδημοσίευσε την ανάρτηση, στις απαντήσεις</string>
<string name="mo_confirm_unfollow_title">Άρση ακολούθησης Λογαριασμού</string>
<string name="mo_settings_unifiedpush_warning_no_distributors">Δεν έχουν εγκατασταθεί διανομείς UnifiedPush. Δεν θα λάβεις ειδοποιήσεις.</string>
<string name="mo_mute_notifications">Απόκρυψη ειδοποιήσεων από αυτόν τον χρήστη;</string>
<string name="import_settings_confirm_body">Όλες οι τρέχουσες ρυθμίσεις και ροές θα επικαλυφθούν! Αυτή η ενέργεια δε μπορεί να αναιρεθεί.</string>
<string name="import_settings_summary">Εισαγωγή προηγουμένως εξαγόμενων ρυθμίσεων και ροών</string>
<string name="mo_enable_dividers">Εμφάνιση διαχωριστικών ανάρτησης</string>
<string name="mo_setting_marquee_summary">Απενεργοποιεί την κύλιση τίτλων</string>
<string name="mo_setting_uniform_summary">Χρήση του εικονιδίου εφαρμογής για όλες τις ειδοποιήσεις</string>
<string name="mo_setting_reduced_motion_summary">Απενεργοποιεί τα εφέ κινήσεις για τις αλληλεπιδράσεις</string>
<string name="mo_setting_play_gif_summary">Αυτόματη αναπαραγωγή GIF σε άβαταρ και emoji</string>
<string name="mo_load_remote_followers">Φόρτωση ακόλουθων και ακολουθουμένων απομακρυσμένων προφίλ</string>
<string name="mo_muting">Σίγαση…</string>
<string name="mo_unmuting">Κατάργηση σίγασης…</string>
<string name="mo_mute_conversation">Σίγαση συζήτησης</string>
<string name="mo_unmute_conversation">Άρση σίγασης συζήτησης</string>
<string name="mo_confirm_to_mute_conversation">Θες να κάνεις σίγαση αυτής της συζήτησης;</string>
<string name="mo_swap_bookmark_with_reblog_summary">Σελιδοδείκτης ή αναδημοσίευση αναρτήσεων από τις ειδοποιήσεις</string>
<string name="mo_settings_show_posts_without_alt_summary">Οι αναρτήσεις θα κρύβονται σε όλες τις ροές αλλά, μπορεί να αποκαλύπτονται στα νήματα και ειδοποιήσεις</string>
<string name="mo_notification_audience_settings">Κοινό Ειδοποιήσεων</string>
<string name="mo_mute_hashtag">Σίγαση ετικέτας</string>
<string name="mo_unmute_hashtag">Άρση σίγασης ετικέτας</string>
<string name="mo_confirm_to_mute_hashtag">Σίγουρα θες να κάνεις σίγαση αυτής της ετικέτας;</string>
<string name="mo_confirm_to_unmute_hashtag">Θες σίγουρα να κάνεις άρση σίγασης αυτής της ετικέτας;</string>
<string name="mo_instance_registration">Εγγραφή</string>
<string name="mo_instance_registration_open">Άνοιγμα</string>
<string name="mo_instance_registration_approval">Απαιτείται έγκριση</string>
<string name="mo_instance_info_open_timeline">Τοπικό χρονολόγιο</string>
<string name="mo_instance_info_moderated_servers">Συντονιζόμενοι διακομιστές</string>
<string name="mo_setting_remote_follower_summary">Εμφάνιση ακόλουθων από άλλες οντότητες</string>
<string name="mo_setting_relocate_publish_summary">Μετακίνηση του κουμπιού δημοσίευσης στη κάτω μπάρα</string>
<string name="mo_setting_default_reply_privacy_summary">Οι απαντήσεις δεν θα περιλαμβάνονται στις δυνατότητες ανακάλυψης</string>
<string name="mo_setting_interaction_count_summary">Εμφάνιση πόσα άτομα αλληλεπίδραση με μια ανάρτηση στη ροή</string>
<string name="mo_setting_disable_swipe_summary">Σύρσιμο για αλλαγή της προσβαλλόμενης ροής</string>
<string name="mo_settings_show_posts_without_alt">Εμφάνιση αναρτήσεων πολυμέσων χωρίς εναλλακτικό κείμενο</string>
<string name="mo_double_tap_to_swipe_between_tabs">Διπλό άγγιγμα για κύλιση μεταξύ καρτελών</string>
<string name="mo_filter_notifications">Φιλτράρισμα ειδοποιήσεων</string>
<string name="mo_blocked_accounts">Αποκλεισμένοι λογαριασμοί</string>
<string name="mo_recent_emoji_cleared">Έγινε εκκαθάριση πρόσφατων emoji</string>
<string name="mo_notification_filter_reset">Επαναφορά στη προεπιλογή</string>
<string name="mo_muted_accounts">Λογαριασμοί σε σίγαση</string>
<string name="mo_double_tap_to_search">Διπλό άγγιγμα για άνοιγμα αναζήτησης</string>
<string name="mo_instance_view_info">Δες Πληροφορίες Διακομιστή</string>
<string name="mo_instance_users">Χρήστες</string>
<string name="mo_instance_status">Κατάσταση</string>
<string name="mo_settings_unifiedpush_warning_disabled">Το UnifiedPush είναι ενεργό. Δε θα λάβεις ειδοποιήσεις.</string>
<string name="mo_settings_unifiedpush_enable">Ενεργοποίηση</string>
<string name="import_settings_confirm">Επιβεβαίωση για εισαγωγή ρυθμίσεων;</string>
<string name="import_settings_failed">Αδυναμία εισαγωγής αναρτήσεων</string>
<string name="export_settings_share">Εξαγωγή Ρυθμίσεων</string>
<string name="export_settings_fail">Αποτυχία εξαγωγής ρυθμίσεων</string>
<string name="export_settings_title">Εξαγωγή ρυθμίσεων</string>
<string name="export_settings_summary">Εξαγωγή όλων των ρυθμίσεων και ροών των συνδεδεμένων λογαριασμών</string>
<string name="import_settings_title">Εισαγωγή ρυθμίσεων</string>
<string name="mo_error_display_title">Αποτυχία εμφάνισης ανάρτησης</string>
<string name="mo_error_display_text">Κάτι πήγε στραβά κατά τη φόρτωση της ανάρτησης. Αν το πρόβλημα παραμένει, παρακαλώ ανάφερε το στην σελίδα Issues μαζί με τις λεπτομέρειες του σφάλματος.</string>
<string name="mo_error_display_copy_error_details">Αντιγραφή λεπτομερειών</string>
<string name="mo_trending_link_read">Ανάγνωση</string>
<string name="mo_settings_remove_tracking_params_summary">Απομάκρυνση πληροφοριών ανίχνευσης από συνδέσμους</string>
<string name="mo_settings_remove_tracking_params">Ιδιωτικοί Σύνδεσμοι</string>
</resources>

View File

@@ -1,3 +1,440 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<string name="sk_timeline_home">Αρχική</string>
<string name="sk_pinned_posts">Καρφιτσωμένα</string>
<string name="sk_delete_and_redraft">Διαγραφή και επανασύνταξη</string>
<string name="sk_confirm_delete_and_redraft">Θες σίγουρα να διαγράψεις και να επανασύνταξεις αυτή την ανάρτηση;</string>
<string name="sk_pin_post">Καρφίτσωμα στο προφίλ</string>
<string name="sk_confirm_pin_post">Θες να καρφιτσώσεις αυτήν την ανάρτηση στο προφίλ σου;</string>
<string name="sk_pinning">Καρφίτσωμα ανάρτησης…</string>
<string name="sk_settings_show_boosts">Εμφάνιση ενισχύσεων</string>
<string name="sk_confirm_unpin_post_title">Ξεκαρφίτσωμα ανάρτησης από το προφίλ</string>
<string name="sk_unpinning">Ξεκαρφίτσωμα ανάρτησης…</string>
<string name="sk_image_description">Περιγραφή εικόνας</string>
<string name="sk_visibility_unlisted">Εκτός λίστας</string>
<string name="sk_settings_show_replies">Εμφάνιση απαντήσεων</string>
<string name="sk_welcome_title">Καλώς ήρθες!</string>
<string name="sk_welcome_text">Ο καρχαρίας σε χαιρετάει! Για να ξεκινήσεις, βάλε το όνομα τομέα της οντότητας σου, παρακάτω.</string>
<string name="sk_translated_using">Μεταφράστηκε από %s</string>
<string name="sk_post_language">Γλώσσα: %s</string>
<string name="sk_available_languages">Διαθέσιμες γλώσσες</string>
<string name="sk_language_name">%1$s (%2$s)</string>
<string name="sk_trending_posts_info_banner">Αυτές οι αναρτήσεις λαμβάνουν προσοχή στο Fediverse.</string>
<string name="sk_settings_always_reveal_content_warnings">Πάντα αποκάλυψη των προειδοποιήσεων περιεχομένου</string>
<string name="sk_settings_enable_marquee">Ενεργοποίηση κυλιόμενου κειμένου στις γραμμές τίτλου</string>
<string name="sk_notification_type_status">Αναρτήσεις</string>
<string name="sk_notification_type_posts">Ειδοποιήσεις ανάρτησης</string>
<string name="sk_color_palette_brown">Καφέ</string>
<string name="sk_translate_show_original">Εμφάνιση αρχικού</string>
<string name="sk_list_name_hint">Όνομα λίστας</string>
<string name="sk_delete_list">Διαγραφή λίστας</string>
<string name="sk_delete_list_confirm">Θες σίγουρα να διαγράψεις τη λίστα «%s»;</string>
<string name="sk_edit_list_title">Επεξεργασία λίστας</string>
<string name="sk_list_replies_policy_none">Κανένας</string>
<string name="sk_timeline_federated">Ομοσπονδία</string>
<string name="sk_settings_color_palette">Παλέτα χρωμάτων</string>
<string name="sk_color_palette_material3">Σύστημα</string>
<string name="sk_color_palette_pink">Ροζ</string>
<string name="sk_color_palette_purple">Μοβ</string>
<string name="sk_color_palette_green">Πράσινο</string>
<string name="sk_delete_notification_confirm_action">Διαγραφή ειδοποίησης</string>
<string name="sk_delete_notification">Διαγραφή ειδοποίησης</string>
<string name="sk_delete_notification_confirm">Θες σίγουρα να διαγράψεις την ειδοποίηση;</string>
<string name="sk_settings_translation_availability_note_available">Το %s υποστηρίζει μετάφραση!</string>
<string name="sk_settings_translation_availability_note_unavailable">Το %s δεν φαίνεται να υποστηρίζει μετάφραση.</string>
<string name="sk_clear_all_notifications_confirm_action">Διαγραφή όλων</string>
<string name="sk_clear_all_notifications_confirm">Θες σίγουρα να διαγράψεις όλες τις ειδοποιήσεις;</string>
<string name="sk_loading_fediverse_resource_title">Αναζήτηση για αυτό στο Fediverse</string>
<string name="sk_undo_reblog">Αναίρεση ενίσχυσης</string>
<string name="sk_reblog_with_visibility">Ενίσχυση με ορατότητα</string>
<string name="sk_loading_resource_on_instance_title">Αναζήτηση για αυτό στο %s</string>
<string name="sk_open_with_account">Άνοιγμα με άλλο λογαριασμό</string>
<string name="sk_forward_report_to">Προώθηση σε %s</string>
<string name="sk_confirm_save_draft">Αποθήκευση προχείρου;</string>
<string name="sk_color_palette_blue">Μπλε</string>
<string name="sk_settings_color_palette_default">Προεπιλογή (%s)</string>
<string name="sk_example_domain">example.social</string>
<string name="sk_settings_tabs_disable_swipe">Απενεργοποίηση κύλισης μεταξύ καρτελών</string>
<string name="sk_settings_rules">Κανόνες</string>
<string name="sk_settings_about">Σχετικά με την εφαρμογή</string>
<string name="sk_settings_donate">Δωρεά</string>
<string name="sk_settings_enable_delete_notifications">Ενεργοποίηση διαγραφής ειδοποιήσεων</string>
<string name="sk_settings_publish_button_text">Κείμενο κουμπιού δημοσίευσης</string>
<string name="sk_settings_publish_button_text_title">Παραμετροποίηση κειμένου κουμπιού Δημοσίευσης</string>
<string name="sk_compose_no_schedule">Μην το προγραμματίσεις</string>
<string name="sk_compose_no_draft">Μην το κάνεις πρόχειρο</string>
<string name="sk_settings_reduce_motion">Μείωση κίνησης στα εφέ κίνησης</string>
<string name="sk_timeline_bubble">Φυσαλίδα</string>
<string name="sk_announcements">Ανακοινώσεις</string>
<string name="sk_recent_searches_placeholder">Πάτα για να αρχίσεις την αναζήτηση</string>
<string name="sk_remove_follower">Αφαίρεση απο ακόλουθο</string>
<string name="sk_quote_post">Δημοσίευσε περί αυτού</string>
<string name="sk_hashtags_you_follow">Ετικέτες που ακολουθείς</string>
<string name="sk_resource_not_found">Η πηγή δε βρέθηκε</string>
<string name="sk_bookmark_as">Σελιδοδείκτη με άλλο λογαριασμό</string>
<string name="sk_bookmarked_as">Σε σελιδοδείκτη ως %s</string>
<string name="sk_already_bookmarked">Ήδη με σελιδοδείκτη</string>
<string name="sk_already_favorited">Ήδη αγαπημένο</string>
<string name="sk_settings_uniform_icon_for_notifications">Ενιαίο εικονίδιο για όλες τις ειδοποιήσεις</string>
<string name="sk_unsent_posts">Άρση αποστολής αναρτήσεων</string>
<string name="sk_draft">Πρόχειρο</string>
<string name="sk_confirm_delete_draft_title">Διαγραφή προχείρου</string>
<string name="sk_confirm_delete_scheduled_post_title">Διαγραφή προγραμματισμένης ανάρτησης</string>
<string name="sk_confirm_delete_scheduled_post">Θες σίγουρα να διαγράψεις αυτή την προγραμματισμένη ανάρτηση;</string>
<string name="sk_draft_or_schedule">Πρόχειρο ή προγραμματισμός</string>
<string name="sk_compose_draft">Η ανάρτηση θα αποθηκευτεί ως πρόχειρη.</string>
<string name="sk_compose_scheduled">Προγραμματίστηκε για</string>
<string name="sk_draft_saved">Το πρόχειρο αποθηκεύτηκε</string>
<string name="sk_post_scheduled">Η ανάρτηση προγραμματίστηκε</string>
<string name="sk_scheduled_too_soon_title">Η στιγμή προγραμματισμού είναι πολύ κοντινή</string>
<string name="sk_mark_as_read">Επισήμανση ως αναγνωσμένο</string>
<string name="sk_settings_single_notification">Εμφάνιση μόνο μια ειδοποίησης</string>
<string name="sk_settings_unifiedpush">Χρήση UnifiedPush</string>
<string name="sk_settings_unifiedpush_choose">Διάλεξε έναν διανομέα</string>
<string name="sk_settings_unifiedpush_no_distributor">Δε βρέθηκε διανομέας</string>
<string name="sk_create">Δημιουργία</string>
<string name="sk_create_list_title">Δημιουργία λίστας</string>
<string name="sk_list_replies_policy_list">Λίστα μελών</string>
<string name="sk_list_replies_policy_followed">Χρήστες που ακολουθείς</string>
<string name="sk_quoting_user">Παρατίθεται ο χρήστης %s</string>
<string name="sk_update_available">Το Megalodon %s είναι έτοιμο για λήψη.</string>
<string name="sk_update_ready">Το Megalodon %s έχει ληφθεί και είναι έτοιμο για εγκατάσταση.</string>
<string name="sk_check_for_update">Έλεγχος για ενημερώσεις</string>
<string name="sk_no_update_available">Καμία ενημέρωση</string>
<string name="sk_list_timelines">Λίστες</string>
<string name="sk_follow_requests">Αιτήματα ακολούθησης</string>
<string name="sk_reject_follow_request">Απόρριψη αιτήματος ακολούθησης</string>
<string name="sk_settings_reply_visibility">Ορατότητα απάντησης</string>
<string name="sk_settings_reply_visibility_following">Απαντήσεις στους ακόλουθους μου</string>
<string name="sk_settings_reply_visibility_self">Απαντήσεις σε μένα</string>
<string name="sk_settings_show_interaction_counts">Εμφάνιση αριθμού αλληλεπιδράσεων</string>
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
<string name="sk_mark_media_as_sensitive">Επισήμανση πολυμέσων ως ευαίσθητα</string>
<string name="sk_user_post_notifications_off">Οι ειδοποιήσεις ανάρτησης για τον χρήστη %s είναι άνεργες</string>
<string name="sk_federated_timeline">Ομοσπονδία</string>
<string name="sk_favorited_as">Ορίστηκε αγαπημένο ως %s</string>
<string name="sk_reblogged_as">Ενισχύθηκε ως %s</string>
<string name="sk_settings_posting">Προτιμήσεις ανάρτησης</string>
<string name="sk_settings_filters">Ρύθμιση φίλτρων</string>
<string name="sk_settings_auth">Ρύθμισης ασφαλείας</string>
<string name="sk_lists_with_user">Λίστες με τον χρήστη %s</string>
<string name="sk_settings_contribute">Συνεισφορά στο Megalodon</string>
<string name="sk_color_palette_yellow">Κίτρινο</string>
<string name="sk_settings_profile">Ρύθμιση προφίλ</string>
<string name="sk_confirm_delete_and_redraft_title">Διαγραφή και επανασύνταξη Ανάρτησης</string>
<string name="sk_unpin_post">Ξεκαρφίτσωμα από το προφίλ</string>
<string name="sk_confirm_unpin_post">Θες σίγουρα να ξεκαρφιτσώσεις αυτή την ανάρτηση;</string>
<string name="sk_confirm_pin_post_title">Καρφίτσωμα ανάρτησης στο προφίλ</string>
<string name="sk_federated_timeline_info_banner">Αυτές είναι οι πιο πρόσφατες αναρτήσεις από άτομα σε ομοσπονδία με σένα.</string>
<string name="sk_bubble_timeline_info_banner">Αυτές είναι οι πιο πρόσφατες αναρτήσεις από το δίκτυο που επιμελήθηκαν οι διαχειριστές της οντότητας σου.</string>
<string name="sk_settings_show_federated_timeline">Εμφάνιση ροής ομοσπονδίας</string>
<string name="sk_color_palette_red">Κόκκινο</string>
<string name="sk_translate_post">Μετάφραση</string>
<string name="sk_confirm_clear_recent_languages">Θες σίγουρα να διαγράψεις τις γλώσσες που χρησιμοποίησες πρόσφατα;</string>
<string name="sk_favorite_as">Αγαπημένο με άλλο λογαριασμό</string>
<string name="sk_already_reblogged">Ήδη ενισχυμένο</string>
<string name="sk_reply_as">Απάντηση με άλλο λογαριασμό</string>
<string name="sk_confirm_delete_draft">Θες σίγουρα να διαγράψεις αυτή την πρόχειρη ανάρτηση;</string>
<string name="sk_schedule_post">Προγραμματισμός ανάρτησης</string>
<string name="sk_settings_continues_playback">Επικάλυψη ήχου</string>
<string name="sk_trending_links_info_banner">Αυτές οι ειδήσεις συζητούνται σε όλο το Fediverse.</string>
<string name="sk_clear_recent_languages">Εκκαθάριση πρόσφατα χρησιμοποιημένων γλωσσών</string>
<string name="sk_schedule">Προγραμματισμός</string>
<string name="sk_schedule_or_draft">Προγραμματισμός ή πρόχειρο</string>
<string name="sk_settings_unifiedpush_no_distributor_body">Πρέπει να εγκαταστήσεις έναν διανομέα για να δουλέψει το UnifiedPush. Για περισσότερες πληροφορίες, πήγαινε στο https://unifiedpush.org/</string>
<string name="sk_settings_continues_playback_summary">Επέτρεψε την αναπαραγωγή των ήδη αναπαραγωμένων πολυμέσων, επικαλύπτοντας τη νέα αναπαραγωγή</string>
<string name="sk_settings_reply_visibility_all">Όλες οι απαντήσεις</string>
<string name="sk_your_lists">Οι λίστες σου</string>
<string name="sk_timeline_local">Τοπική</string>
<string name="sk_copy_link_to_post">Αντιγραφή συνδέσμου ανάρτησης</string>
<string name="sk_reblog_as">Ενίσχυση με άλλο λογαριασμό</string>
<string name="sk_settings_load_new_posts">Αυτόματη φόρτωση νέων αναρτήσεων</string>
<string name="sk_user_post_notifications_on">Οι ειδοποιήσεις ανάρτησης για τον χρήστη %s είναι ενεργές</string>
<string name="sk_accept_follow_request">Αποδοχή αιτήματος ακολούθησης</string>
<string name="sk_clear_all_notifications">Εκκαθάριση όλων των ειδοποιήσεων</string>
<string name="sk_settings_translate_only_opened">Μετάφραση μόνο ανοιγμένων αναρτήσεων</string>
<string name="sk_scheduled_too_soon">Η ανάρτηση πρέπει να προγραμματίζεται τουλάχιστον 10 λεπτά από τώρα.</string>
<string name="sk_confirm_save_changes">Αποθήκευση αλλαγών;</string>
<string name="sk_mark_as_draft">Επισήμανση ως πρόχειρο</string>
<string name="sk_settings_about_instance">Σχετικά με την οντότητα</string>
<string name="sk_list_replies_policy">Εμφάνιση απαντήσεων σε</string>
<string name="sk_app_name">Megalodon</string>
<string name="sk_settings_auto_reveal_nobody">Ποτέ</string>
<string name="sk_settings_auto_reveal_author">Απαντήσεις από τον ίδιο συντάκτη</string>
<string name="sk_alt_text_missing_title">Λείπει εναλλακτικό κείμενο</string>
<string name="sk_icon_microphone">Μικρόφωνο</string>
<string name="sk_icon_keyboard">Αρμόνιο</string>
<string name="sk_icon_coffee">Καφές</string>
<string name="sk_icon_microscope">Μικροσκόπιο</string>
<string name="sk_icon_clapper_board">Κλακέτα</string>
<string name="sk_icon_sport">Άθλημα</string>
<string name="sk_icon_aperture">Διάφραγμα</string>
<string name="sk_icon_train">Τρένο</string>
<string name="sk_icon_leaves">Φύλλα</string>
<string name="sk_icon_gavel">Προεδρική σφύρα</string>
<string name="sk_icon_human">Άνθρωπος</string>
<string name="sk_icon_globe">Υδρόγειος</string>
<string name="sk_icon_headphones">Ακουστικά</string>
<string name="sk_post_edited">επεξεργάστηκε</string>
<string name="sk_attach_file">Επισύναψη αρχείου</string>
<string name="sk_searching">Αναζητείται…</string>
<string name="sk_settings_glitch_mode_explanation">Ενεργοποίησέ το αν η οικιακή σου οντότητα τρέχει Glitch. Δεν απαιτείται για Hometown ή Akkoma.</string>
<string name="sk_content_type_plain">Σκέτο κείμενο</string>
<string name="sk_no_remote_info_hint">Απομακρυσμένες πληροφορίες μη διαθέσιμες</string>
<string name="sk_error_loading_profile">Αποτυχία φόρτωσης προφίλ μέσω %s</string>
<string name="sk_timelines">Ροές</string>
<string name="sk_icon_bug">Έντομο</string>
<string name="sk_icon_pizza">Πίτσα</string>
<string name="sk_edit_timelines">Επεξεργασία ροών</string>
<string name="sk_filtered">Φιλτραρισμένο: %s</string>
<string name="sk_external_share_or_open_title">Κοινοποίηση ή άνοιγμα με λογαριασμό</string>
<string name="sk_icon_beaker">Ποτήρι ζέσης</string>
<string name="sk_icon_bed">Κρεβάτι</string>
<string name="sk_notification_type_update">Επεξεργασμένες αναρτήσεις</string>
<string name="sk_external_share_title">Κοινοποίηση με λογαριασμό</string>
<string name="sk_content_type">Τύπος περιεχομένου</string>
<string name="sk_in_reply">Σε απάντηση</string>
<string name="sk_remove_follower_success">Επιτυχής αφαίρεση ακολούθου</string>
<string name="sk_changelog">Λίστα αλλαγών</string>
<string name="sk_remove_follower_confirm">Να καταργηθεί ο χρήστης %s ως ακόλουθος αποκλείοντάς τον και κάνοντας άρση αμέσως;</string>
<string name="sk_do_remove_follower">Αφαίρεση</string>
<string name="sk_icon_city">Πόλη</string>
<string name="sk_icon_cat">Γάτα</string>
<string name="sk_icon_pin">Καρφίτσωμα</string>
<string name="sk_alt_text_missing">Τουλάχιστον ένα συνημμένο δεν έχει περιγραφή.</string>
<string name="sk_publish_anyway">Δημοσίευση ούτως ή άλλως</string>
<string name="sk_timeline_posts">Αναρτήσεις</string>
<string name="sk_timelines_add">Προσθήκη</string>
<string name="sk_timeline">Ροή</string>
<string name="sk_list">Λίστα</string>
<string name="sk_hashtag">Ετικέτα</string>
<string name="sk_pin_timeline">Καρφίτσωμα ροής</string>
<string name="sk_pinned_timeline">Καρφιτσώθηκε στην αρχική</string>
<string name="sk_unpin_timeline">Ξεκαρφίτσωμα ροής</string>
<string name="sk_unpinned_timeline">Ξεκαρφιτσώθηκε απ\' την αρχική</string>
<string name="sk_icon_dog">Σκύλος</string>
<string name="sk_icon_turtle">Χελώνα</string>
<string name="sk_icon_balloon">Μπαλόνι</string>
<string name="sk_icon_image">Εικόνα</string>
<string name="sk_icon_bot">Ρομπότ</string>
<string name="sk_icon_language">Γλώσσα</string>
<string name="sk_icon_location">Τοποθεσία</string>
<string name="sk_icon_megaphone">Μεγάφωνο</string>
<string name="sk_icon_laugh">Γέλιο</string>
<string name="sk_icon_pi">Πι</string>
<string name="sk_icon_color_palette">Παλέτα χρωμάτων</string>
<string name="sk_icon_academic_cap">Ακαδημαϊκό καπέλο</string>
<string name="sk_icon_tag">Ετικέτα</string>
<string name="sk_icon_stethoscope">Στηθοσκόπιο</string>
<string name="sk_icon_weather">Καιρός</string>
<string name="sk_icon_games">Παιχνίδια</string>
<string name="sk_icon_code">Κώδικας</string>
<string name="sk_icon_light_bulb">Λάμπα</string>
<string name="sk_icon_music">Μουσική</string>
<string name="sk_icon_people">Άνθρωποι</string>
<string name="sk_icon_health">Υγεία</string>
<string name="sk_icon_chat">Συνομιλία</string>
<string name="sk_icon_shield">Ασπίδα</string>
<string name="sk_icon_book">Βιβλίο</string>
<string name="sk_icon_bicycle">Ποδήλατο</string>
<string name="sk_icon_map">Χάρτης</string>
<string name="sk_icon_backpack">Σακίδιο</string>
<string name="sk_icon_briefcase">Χαρτοφύλακας</string>
<string name="sk_icon_fire">Φωτιά</string>
<string name="sk_icon_feed">Ροή</string>
<string name="sk_icon_recycle_bin">Κάδος ανακύκλωσης</string>
<string name="sk_settings_server_version">Έκδοση διακομιστή: %s</string>
<string name="sk_icon_diamond">Διαμάντι</string>
<string name="sk_icon_umbrella">Ομπρέλα</string>
<string name="sk_add_timeline_tag_error_empty">Η ετικέτα δε μπορεί να είναι κενή</string>
<string name="sk_updater_enable_pre_releases">Ενεργοποίηση προ-κυκολοφορίας</string>
<string name="sk_inline_local_only">Μόνο τοπικά</string>
<string name="sk_inline_direct">Μόνο με επισήμανση</string>
<string name="sk_separator">·</string>
<string name="sk_local_only">Μόνο τοπική οντότητα</string>
<string name="sk_instance_features">Δυνατότητες οντότητας</string>
<string name="sk_settings_glitch_instance">Λειτουργία μόνο-τοπική του Glitch</string>
<string name="sk_signed_up">έγινε εγγραφή</string>
<string name="sk_reported">αναφέρθηκε</string>
<string name="sk_sign_ups">Χρήστες που εγγράφονται</string>
<string name="sk_new_reports">Νέες αναφορές</string>
<string name="sk_settings_prefix_reply_cw_with_re">Πρόθεμα ΠΠ με \"re:\" κατά την απάντηση</string>
<string name="sk_settings_hide_fab">Αυτόματη απόκρυψη κουμπιού Σύνταξης</string>
<string name="sk_notification_action_replied">Εστάλη απάντηση στον χρήστη %s</string>
<string name="sk_reply_line_above_avatar">Η γραμμή \"σε απάντηση σε\" πάνω από το άβαταρ</string>
<string name="sk_show_thread">Εμφάνιση νήματος</string>
<string name="sk_content_type_unspecified">Απροσδιόριστος</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Ενεργοποίηση μορφοποίησης ανάρτησης</string>
<string name="sk_settings_default_content_type">Προεπιλεγμένος τύπος περιεχομένου</string>
<string name="sk_instance_info_unavailable">Πληροφορίες οντότητας προσωρινά μη διαθέσιμες</string>
<string name="sk_open_in_app">Άνοιγμα στην εφαρμογή</string>
<string name="sk_open_in_app_failed">Αδυναμία ανοίγματος στην εφαρμογή</string>
<string name="sk_settings_allow_remote_loading">Φόρτωση πληροφοριών απομακρυσμένων οντοτήτων</string>
<string name="sk_settings_auto_reveal_anyone">Απαντήσεις απ\' όλους</string>
<string name="sk_settings_prefix_replies_always">Σε απάντηση προς όλους</string>
<string name="sk_settings_prefix_replies_never">Ποτέ</string>
<string name="sk_exclusive_list">Αποκλειστική λίστα</string>
<string name="sk_list_exclusive_switch">Κάνε την λίστα αποκλειστική</string>
<string name="sk_advanced_options_show">Εμφάνιση προχωρημένων επιλογών</string>
<string name="sk_advanced_options_hide">Απόκρυψη προχωρημένων επιλογών</string>
<string name="sk_spoiler_show">Εμφάνιση περιεχομένου</string>
<string name="sk_pronouns_label">Αντωνυμίες</string>
<string name="sk_switch_timeline">Εναλλαγή ροής</string>
<string name="sk_settings_instance">Οντότητα</string>
<string name="sk_settings_true_black">Απόλυτα μαύρη λειτουργία</string>
<string name="sk_settings_display_pronouns_in_timelines">Εμφάνιση αντωνυμιών στις ροές</string>
<string name="sk_settings_display_pronouns_in_threads">Εμφάνιση αντωνυμιών στα νήματα</string>
<string name="sk_settings_hide_interaction">Απόκρυψη κουμπιών αλληλεπίδρασης</string>
<string name="sk_followed_as">Ακολουθείται από %s</string>
<string name="sk_timeline_icon">Εικονίδιο</string>
<string name="sk_icon_heart">Καρδιά</string>
<string name="sk_icon_star">Αστέρι</string>
<string name="sk_remove">Αφαίρεση</string>
<string name="sk_icon_rabbit">Κουνέλι</string>
<string name="sk_reacted_with">Ο χρήστης %1$s αντέδρασε με %2$s</string>
<string name="sk_reacted">Ο χρήστης %s αντέδρασε</string>
<string name="sk_expand">Επέκταση</string>
<string name="sk_collapse">Σύμπτυξη</string>
<string name="sk_settings_collapse_long_posts">Σύμπτυξη πολύ μεγάλων αναρτήσεων</string>
<string name="sk_unfinished_attachments">Μεταμορφώνονται συνημμένα</string>
<string name="sk_icon_sun">Ήλιος</string>
<string name="sk_icon_sunset">Ηλιοβασίλεμα</string>
<string name="sk_icon_thunderstorm">Καταιγίδα</string>
<string name="sk_icon_water">Νερό</string>
<string name="sk_edit_timeline">Επεξεργασία ροής</string>
<string name="sk_icon_cloud">Σύννεφο</string>
<string name="sk_add_timeline">Προσθήκη ροής</string>
<string name="sk_edit_timeline_tag_main">Αναρτήσεις που περιέχουν την ετικέτα…</string>
<string name="sk_edit_timeline_tag_any">...ή οποιοδήποτε απ\' αυτές</string>
<string name="sk_edit_timeline_tag_all">...και όλες αυτές</string>
<string name="sk_edit_timeline_tag_none">... αλλά καμία απ\' αυτές</string>
<string name="sk_edit_timeline_tag_hint">Εισήγαγε ετικέτα…</string>
<string name="sk_edit_timeline_tags_hint">Εισήγαγε ετικέτες…</string>
<string name="sk_no_results">Χωρίς αποτέλεσμα</string>
<string name="sk_save_draft">Αποθήκευση προχείρου;</string>
<string name="sk_save_draft_message">Θες να αποθηκεύσεις τις αλλαγές σ\' αυτό το πρόχειρο ή θα δημοσιεύσεις τώρα;</string>
<string name="sk_settings_show_alt_indicator">Ένδειξη για εναλλακτικά κείμενα</string>
<string name="sk_settings_show_no_alt_indicator">Ένδειξη για έλλειψη εναλλακτικών κειμένων</string>
<string name="sk_alt_button">ALT</string>
<string name="sk_gif_badge">GIF</string>
<string name="sk_search_fediverse">Αναζήτηση στο Fediverse</string>
<string name="sk_icon_math_formula">Μαθηματική φόρμουλα</string>
<string name="sk_icon_snowflake">Χιονονιφάδα</string>
<string name="sk_no_alt_text">Χωρίς διαθέσιμο εναλλακτικό κείμενο</string>
<string name="sk_settings_support_local_only">Ο διακομιστής υποστηρίζει μόνο-τοπικές αναρτήσεις</string>
<string name="sk_settings_disable_alt_text_reminder">Απενεργοποίηση υπενθύμισης για εναλλακτικό κείμενο</string>
<string name="sk_notify_posts_info_banner">Αν ενεργοποιήσεις τις ειδοποιήσεις ανάρτησης για κάποια άτομα, οι νέες αναρτήσεις τους θα εμφανιστούν εδώ.</string>
<string name="sk_icon_news">Ειδήσεις</string>
<string name="sk_edit_timeline_tags_explanation">Λάβε υπόψη ότι ο διακομιστής χειρίζεται αυτές τις λειτουργίες. Ο συνδυασμός τους ενδέχεται να μην υποστηρίζεται.</string>
<string name="sk_icon_important">Σημαντικό</string>
<string name="sk_icon_gauge">Μετρητής</string>
<string name="sk_icon_verified">Επαληθεύτηκε</string>
<string name="sk_icon_doctor">Γιατρός</string>
<string name="sk_hashtag_timeline_local_only_switch">Εμφάνιση μόνο τοπικών αναρτήσεων;</string>
<string name="sk_settings_see_new_posts_button">Κουμπί \"Δες νέες αναρτήσεις\"</string>
<string name="sk_settings_local_only_explanation">Η οικιακή σου οντότητα πρέπει να υποστηρίζει μόνο-τοπική ανάρτηση για να λειτουργήσει. Οι περισσότερες τροποποιημένες εκδόσεις του Mastodon το κάνουν, αλλά το Mastodon όχι.</string>
<string name="sk_notify_poll_results">Αποτελέσματα δημοσκόπησης</string>
<string name="sk_icon_rain">Βροχή</string>
<string name="sk_notify_update">Επεξεργάζεται μια ενισχυμένη ανάρτηση</string>
<string name="sk_follow_as">Ακολούθησε από άλλον λογαριασμό</string>
<string name="sk_compact_reblog_reply_line">Συμπαγής γραμμή ενίσχυσης/απάντησης</string>
<string name="sk_settings_confirm_before_reblog">Επιβεβαίωση πριν την ενίσχυση</string>
<string name="sk_list_exclusive_switch_explanation">Μέλη της αποκλειστικής λίστας δε θα εμφανίζονται στην αρχική σου ροή - αν το υποστηρίζει η οντότητά σου.</string>
<string name="sk_unfinished_attachments_message">Κάποια απ\' τα συνημμένα δεν έχουν μεταμορφωθεί πλήρως.</string>
<string name="sk_settings_allow_remote_loading_explanation">Προσπάθεια ανάκτησης ακριβέστερων καταχωρίσεων για ακόλουθους, \"μού αρέσει\" και ενισχύσεις φορτώνοντας τις πληροφορίες από την οντότητα προέλευσης.</string>
<string name="sk_disable_pill_shaped_active_indicator">Απενεργοποίηση ένδειξης μορφής-χαπιού ενεργής καρτέλας</string>
<string name="sk_settings_default_content_type_explanation">Αυτό σου επιτρέπει να προεπιλέξεις έναν τύπο περιεχομένου κατά τη δημιουργία νέων αναρτήσεων, παρακάμπτοντας την τιμή που έχει οριστεί στις \"Προτιμήσεις ανάρτηση ς\".</string>
<string name="sk_settings_content_types_explanation">Επιτρέπει τη ρύθμιση ενός τύπου περιεχομένου όπως το Markdown κατά τη δημιουργία μιας ανάρτησης. Λάβε υπόψη ότι δεν το υποστηρίζουν όλες οι οντότητες.</string>
<string name="sk_settings_like_icon">Χρήση καρδιάς ως εικονίδιο για αγαπημένα</string>
<string name="sk_recently_used">Χρησιμοποιήθηκε πρόσφατα</string>
<string name="sk_load_missing_posts_above">Φόρτωση νεότερων αναρτήσεων</string>
<string name="sk_load_missing_posts_below">Φόρτωση παλαιότερων αναρτήσεων</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Αυτόματη εμφάνιση ίσων ΠΠ στις απαντήσεις</string>
<string name="sk_mute_label">Διάρκεια</string>
<string name="sk_duration_days_7">7 ημέρες</string>
<string name="sk_settings_prefix_replies_to_others">Μόνο προς απάντηση σε άλλους</string>
<string name="sk_settings_forward_report_default">Προεπιογή διακόπτη \"Προώθηση αναφοράς\"</string>
<string name="sk_tab_home">Αρχική</string>
<string name="sk_tab_search">Αναζήτηση</string>
<string name="sk_tab_profile">Προφίλ</string>
<string name="sk_settings_show_emoji_reactions_always">Εμφάνιση του κουμπιού προσθήκη πάντα</string>
<plurals name="sk_users_reacted_with">
<item quantity="one">Ένας χρήστης αντέδρασε με %2$s</item>
<item quantity="other">%1$,d χρήστες αντέδρασαν με %2$s</item>
</plurals>
<string name="sk_button_react">Αντέδρασε με εμότζι</string>
<string name="sk_duration_indefinite">Άπειρη</string>
<string name="sk_duration_minutes_5">5 λεπτά</string>
<string name="sk_duration_minutes_30">30 λεπτά</string>
<string name="sk_duration_hours_1">1 ώρα</string>
<string name="sk_duration_hours_6">6 ώρες</string>
<string name="sk_duration_days_1">1 ημέρα</string>
<string name="sk_duration_days_3">3 ημέρες</string>
<string name="sk_notification_mention">Επισημάνθηκες από τον χρήστη %s</string>
<string name="sk_suicide_search_terms">Αυτοκτονία</string>
<string name="sk_search_suicide_title">Σε περίπτωση που είσαι σε κρίση…</string>
<string name="sk_search_suicide_hotlines">Βρες γραμμή βοήθειας</string>
<string name="sk_do_not_show_again">Μην εμφανιστεί ξανά</string>
<string name="sk_suicide_helplines_url">http://suicide-help.gr/</string>
<string name="sk_post_contains_media">Η ανάρτηση περιέχει πολυμέσα</string>
<plurals name="sk_time_seconds">
<item quantity="one">%d δευτερόλεπτο</item>
<item quantity="other">%d δευτερόλεπτα</item>
</plurals>
<plurals name="sk_time_minutes">
<item quantity="one">%d λεπτό</item>
<item quantity="other">%d λεπτά</item>
</plurals>
<plurals name="sk_time_hours">
<item quantity="one">%d ώρα</item>
<item quantity="other">%d ώρες</item>
</plurals>
<plurals name="sk_time_days">
<item quantity="one">%d ημέρα</item>
<item quantity="other">%d ημέρες</item>
</plurals>
<string name="sk_settings_underlined_links">Υπογραμμισμένοι σύνδεσμοι</string>
<string name="sk_set_as_default">Ορισμός ως προεπιλογή</string>
<string name="sk_private_note_hint">Προσθήκη προσωπικής σημείωσης γι\' αυτό το προφίλ</string>
<string name="sk_private_note_update_failed">Αποτυχία αποθήκευσης σημείωσης</string>
<string name="sk_private_note_confirm_delete">Διγραφή προσωπικής σημείωσης για %s;</string>
<string name="sk_delete_note">Διαγραφή προσωπικής σημείωσης</string>
<string name="sk_add_note">Προσθήκη προσωπικής σημείωσης</string>
<string name="sk_poll_multiple_choice">Πολλαπλές επιλογές</string>
<string name="sk_poll_show_results">Εμφάνιση αποτελεσμάτων</string>
<string name="sk_poll_hide_results">Απόκρυψη αποτελεσμάτων</string>
<string name="sk_edit_alt_text">Επεξεργασία εναλλακτικού κειμένου</string>
<string name="sk_settings_default_visibility">Προεπιλεγμένη ορατότητα ανάρτησης</string>
<string name="sk_settings_lock_account">Χειροκίνητη αποδοχή νέων ακολούθων</string>
<string name="sk_timeline_cache_cleared">Εκκαθαρίστηκε η κρυφή μνήμη ροής αρχικής</string>
<string name="sk_settings_show_emoji_reactions">Εμφάνιση αντιδράσεων εμότζι στις ροές</string>
<string name="sk_settings_show_emoji_reactions_hide_empty">Απόκρυψη κενών αντιδράσεων εμότζι</string>
<string name="sk_settings_show_emoji_reactions_only_opened">Μόνο όταν ανοιχθεί η ανάρτηση</string>
<string name="sk_confirm_changes">Επιβεβαίωση αλλαγών</string>
<string name="sk_settings_crash_log_unavailable">Καμία διαθέσιμη... ακόμα</string>
<string name="sk_crash_log_copied">Καταγραφή σφαλμάτων αντιγράφηκε</string>
<string name="sk_open_post_preview">Προεπισκόπηση ανάρτησης</string>
<string name="sk_post_preview">Προεπισκόπηση</string>
<string name="sk_settings_copy_crash_log">Αντιγραφή τελευταίων καταγραφών σφαλμάτων</string>
<string name="sk_button_mutuals">Κοινοί</string>
<string name="sk_muted_accounts">Λογαριασμοί σε σίγαση</string>
<string name="sk_blocked_accounts">Αποκλεισμένοι λογαριασμοί</string>
<string name="sk_settings_display_pronouns_in_user_listings">Εμφάνιση αντωνυμιών στις λίστες χρήστη</string>
<string name="sk_tab_notifications">Ειδοποιήσεις</string>
<string name="sk_settings_show_labels_in_navigation_bar">Εμφάνιση ετικετών καρτέλας στη μπάρα πλοήγησης</string>
<string name="sk_settings_emoji_reactions">Ενεργοποίηση αντιδράσεων εμότζι</string>
<string name="sk_settings_emoji_reactions_explanation">Εμφανίζει αντιδράσεις εμότζι σε αναρτήσεις και σού επιτρέπει να προσθέσεις τη δική σου. Πολλοί διακομιστές του Fediverse το υποστηρίζουν, αλλά όχι το Mastodon.</string>
<string name="sk_enter_emoji_toast">Παρακαλώ πληκτρολόγησε ένα εμότζι</string>
<string name="sk_enter_emoji_hint">Πληκτρολόγησε ένα εμότζι ή αναζήτησε</string>
<string name="sk_search_suicide_message">Αν αναζητάς κάποιο σημάδι να μην αυτοκτονήσεις, να το. Σε παρακαλώ σκέψου να επικοινωνήσεις με την τοπική γραμμή βοήθειας για αυτοκτονίες αν είσαι σε κρίση.</string>
<plurals name="sk_posts_count_label">
<item quantity="one">ανάρτηση</item>
<item quantity="other">αναρτήσεις</item>
</plurals>
<string name="sk_settings_clear_timeline_cache">Εκκαθάριση κρυφής μνήμης ροής αρχικής</string>
<string name="sk_posted">ο χρήστης %s δημοσίευσε</string>
</resources>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="mo_setting_uniform_summary">Uzi la ikonon de la aplikaĵon por ĉiuj sciigoj</string>
<string name="mo_setting_uniform_summary">Uzi la ikonon de la aplikaĵo por ĉiuj sciigoj</string>
<string name="mo_instance_info_moderated_servers">Moderigitaj serviloj</string>
<string name="mo_confirm_unfollow_title">Malsekvi Konton</string>
<string name="mo_setting_marquee_summary">Malŝaltas la elipsan titolon glitigado</string>
<string name="mo_setting_reduced_motion_summary">Malŝalti animaciojn de interagoj</string>
<string name="mo_unmute_hashtag">Malsilentigi haketetikedo</string>
<string name="mo_unmute_hashtag">Malsilentigi haketetikedon</string>
<string name="mo_sending_error">Eraro dum afiŝado</string>
<string name="mo_haptic_feedback">Haptika reago</string>
<string name="mo_show_media_preview">Montri amaskomunikilaron antaŭrigardon en templinioj</string>
@@ -22,7 +22,7 @@
<string name="mo_unmuted_conversation_successfully">Sukcese malsilentigita konversacio</string>
<string name="mo_no_image_desc">La inkluzivitaj bildoj ne havas priskribon. Bonvolu konsideri aldoni unu, por permesi al vidhandikapitoj partopreni.</string>
<string name="mo_confirm_to_unmute_conversation">Ĉu vi certas, ke vi volas malsilentigi ĉi tiun konversacion\?</string>
<string name="mo_share_open_url">Malfermi en Aplikaĵo</string>
<string name="mo_share_open_url">Malfermi en aplikaĵo</string>
<string name="mo_mute_label">Daŭro:</string>
<string name="mo_update_available">Moshidon %s estas preta por elŝuti.</string>
<string name="mo_confirm_to_unmute_hashtag">Ĉu vi certas, ke vi volas malsilentigi ĉi tiun haketetikedon\?</string>
@@ -32,25 +32,25 @@
<string name="mo_personal_note_confirm">Konfirmi ŝanĝojn por noti</string>
<string name="mo_instance_contact">Kontakto</string>
<string name="mo_composer_behavior">Konduto de verkilo</string>
<string name="mo_setting_interaction_count_summary">Montri kiom da homoj interagis kun afiŝo en la templinio</string>
<string name="mo_setting_interaction_count_summary">Montri, kiom da homoj interagis kun afiŝo en la templinio</string>
<string name="mo_open_camera">Foti</string>
<string name="mo_blocked_accounts">Blokitaj kontoj</string>
<string name="mo_no_image_desc_title">Neniu Bilda priskribo</string>
<string name="mo_update_ready">Moshidon %s estas elŝutita kaj preta por instali.</string>
<string name="mo_notification_action_replied">Sukcese respondis al la afiŝo de %s</string>
<string name="mo_disable_relocate_publish_button_to_enable_customization">Malŝalti Reloki afiŝi butonon por permesi personigon</string>
<string name="mo_disable_relocate_publish_button_to_enable_customization">Malŝaltu la agordon \"Reloki la afîsado-butonon\" por permesi personigon</string>
<string name="mo_setting_default_reply_privacy_summary">Respondoj estos forigitaj el malkovraj funkcioj</string>
<string name="mo_poll_option_add">Aldoni novan balotopcion</string>
<string name="mo_enable_dividers">Montri afiŝo-dividiloj</string>
<string name="mo_enable_dividers">Montri afiŝo-dividilojn</string>
<string name="mo_color_palette_nord">Nord</string>
<string name="mo_color_palette_black_and_white">Nigra kaj Blanka</string>
<string name="mo_add_custom_server_local_timeline">Aldoni lokan templinion de propra servilo</string>
<string name="mo_severity_suspend">Blokita</string>
<string name="mo_setting_true_black_summary">Eble ŝparos potencon sur AMOLED-ekranoj</string>
<string name="mo_setting_true_black_summary">Eble ŝparos energion sur AMOLED-ekranoj</string>
<string name="mo_duration_days_7">7 tagoj</string>
<string name="mo_settings_show_posts_without_alt_summary">Afiŝoj estos kaŝitaj en ĉiuj templinioj, sed povas esti malkaŝitaj en fadenoj kaj sciigoj</string>
<string name="mo_confirm_to_mute_conversation">Ĉu vi certas, ke vi volas silentigi ĉi tiun konversacion\?</string>
<string name="mo_clear_recent_emoji">Forigi lastatempe uzatan emoji</string>
<string name="mo_clear_recent_emoji">Forigi lastatempe uzatan emoĝiojn</string>
<string name="mo_muted_conversation_successfully">Sukcese silentigita konversacio</string>
<string name="mo_swap_bookmark_with_reblog">Interŝanĝi legosignon per reblogi ago</string>
<string name="mo_filtered">Filtritaj: %s</string>
@@ -61,8 +61,8 @@
<string name="mo_mute_conversation">Silentigi konversacion</string>
<string name="mo_settings_contribute">Kontribui al Moshidon</string>
<string name="mo_recent_emoji_cleared">Lastatempa emoĵion forigis</string>
<string name="mo_instance_registration_approval">Aprobo necesa</string>
<string name="mo_setting_play_gif_summary">Aŭtomate ludi GIF-ojn en avataroj kaj emoĵioJ</string>
<string name="mo_instance_registration_approval">Aprobo necesas</string>
<string name="mo_setting_play_gif_summary">Aŭtomate ludi GIF-ojn en avataroj kaj emoĝioj</string>
<string name="mo_mute_hashtag">Silentigi haketetikedon</string>
<string name="mo_duration_hours_6">6 horoj</string>
<string name="mo_duration_hours_1">1 horo</string>
@@ -71,10 +71,10 @@
<string name="mo_setting_disable_swipe_summary">Glitigi por ŝanĝi viditan templinion</string>
<string name="mo_filter_notifications">Filtri sciigojn</string>
<string name="mo_disable_reminder_to_add_alt_text">Malŝalti memorigilon por aldoni alttekston</string>
<string name="mo_muting">Silentigante</string>
<string name="mo_muting">Silentigado</string>
<string name="mo_mention_reblogger_automatically">Aŭtomate mencii konton, kiu reblogis la afiŝon en respondoj</string>
<string name="mo_emoji_recent">Lastatempe uzata</string>
<string name="mo_unmuting">Malsilentigante</string>
<string name="mo_emoji_recent">Lastatempe uzataj</string>
<string name="mo_unmuting">Malsilentigado</string>
<string name="mo_duration_indefinite">Nedifinita</string>
<string name="mo_double_tap_to_search">Duoble premi por malfermi serĉon</string>
<string name="mo_personal_note_update_failed">Malsukcesis konservi noton</string>
@@ -82,14 +82,18 @@
<string name="mo_setting_haptic_feedback_summary">Vibri dum interagado kun afiŝoj</string>
<string name="mo_confirm_to_mute_hashtag">Ĉu vi certas, ke vi volas silentigi ĉi tiun haketetikedon\?</string>
<string name="mo_swap_bookmark_with_reblog_summary">Legosigni aŭ reblogi afiŝojn de la sciigo</string>
<string name="mo_setting_relocate_publish_summary">Movi la afiŝi butonon al la malsupra breto</string>
<string name="mo_setting_relocate_publish_summary">Movi la afiŝado-butonon al la malsupra breto</string>
<string name="mo_confirm_unfollow">Konfirmi malsekvi %s</string>
<string name="mo_instance_admin">Administrita de</string>
<string name="mo_hide_compose_button_while_scrolling_setting">Kaŝi verkan butonon dum glitigado</string>
<string name="mo_instance_registration_open">Malfermi</string>
<string name="mo_fab_compose">Verki</string>
<string name="mo_relocate_publish_button">Reloki aifŝi butonon</string>
<string name="mo_relocate_publish_button">Reloki la afîsado-butonon</string>
<string name="mo_duration_days_3">3 tagoj</string>
<string name="mo_setting_remote_follower_summary">Montri sekvantojn de aliaj instancoj</string>
<string name="mo_notification_filter_reset">Restarigi al defaŭlta</string>
<string name="mo_instance_view_info">Vidi servilan informon</string>
<string name="mo_trending_link_read">Legi</string>
<string name="mo_error_display_copy_error_details">Kopii informojn</string>
<string name="mo_settings_unifiedpush_enable">Ebligi</string>
</resources>

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