Compare commits

..

390 Commits

Author SHA1 Message Date
LucasGGamerM
dae5989d64 docs: add changelog 2025-01-05 19:34:29 -03:00
LucasGGamerM
1d1c4f2666 build: bump version number 2025-01-05 19:33:11 -03:00
LucasGGamerM
0fecdf345a Merge pull request #570
Upgrade build dependencies for the project
2025-01-04 15:15:24 -03:00
Aurimas
82bcfe3fa8 Update gradle.properties
Co-authored-by: Zongle Wang <wangzongler@gmail.com>
2024-11-17 18:46:51 -08:00
Aurimas
203c43343a Update build.gradle
Co-authored-by: Zongle Wang <wangzongler@gmail.com>
2024-11-17 18:46:43 -08:00
Aurimas Liutikas
4c105acc30 Upgrade build dependencies for the project
- Upgrade to Gradle 8.11
- Upgrade to Android Gradle Plugin 8.7.2
- Remove deprecated android.defaults.buildfeatures.buildconfig=true
  gradle property, it is not needed as mastodon/build.gradle already
  sets android { buildFeatures { buildConfig true } }
- Move plugin repository definition to settings.gradle to match latest
  Gradle practices
- Move to using plugin {} mechanism to add Android Gradle Plugin to
  match the latest Gradle practices
- Remove root project clean task as this project does not produce any
  real artifacts, it seems to be leftover from original Android new
  project template
2024-11-15 17:21:37 -08:00
LucasGGamerM
d81eb6ad0a Merge pull request #538 from FineFindus/fix/hashtag-timeline-open-crash
fix(Timeline/Hashtag): check if hashtag is null
2024-09-09 09:03:54 -03:00
FineFindus
08542cd16f fix(Timeline/Hashtag): check if hashtag is null
Fixes a crash when clicking on hashtags in profiles, since the hashtag
is for some reason null.
2024-09-09 12:03:10 +02:00
LucasGGamerM
f30e12f5c6 Merge pull request #526 from FineFindus/fix/empty-hashtag
feat(Error): disable clicking + crash fix
2024-09-02 09:39:41 -03:00
LucasGGamerM
3a14fb5912 Merge pull request #529 from FineFindus/fix/hashtag-timeline-follow-icon
fix(HashtagTimeline): update follow menu icon
2024-09-02 08:55:15 -03:00
FineFindus
cc64a1b6a2 fix(HashtagTimeline): update follow menu icon
Fixes an issue, where the menu follow icon was not correctly updated,
and would always show the follow state.
2024-08-31 17:49:04 +02:00
FineFindus
7fa079e362 fix(HtmlParser): check if hashtag has text
Fixes a crash, where the text of an hashtag was empty, leading to an oob
string access.
2024-08-31 10:44:53 +02:00
FineFindus
c2e6280a18 feat(ErrorStatusDisplayItem): disable clicking on error item
Disable clicking on the ErrorStatusDisplayItem, since there is no valid
content that can be displayed.
2024-08-31 10:42:09 +02:00
LucasGGamerM
01225b05f2 Merge pull request #515 from collingsr/master
Updated README formatting & content
2024-08-29 20:25:16 -03:00
Ruth Collings
89f27984b7 Update FAQ.md
Co-authored-by: FineFindus <63370021+FineFindus@users.noreply.github.com>
2024-08-22 14:22:30 -04:00
Ruth Collings
61b933655c clean up
removing stuff I dropped in there earlier
2024-08-21 14:22:13 -04:00
Ruth Collings
d47e1939d0 formatting 2024-08-21 14:18:42 -04:00
Ruth Collings
00b934dc69 spacing and lines
it was bothering me
2024-08-19 16:34:22 -04:00
Ruth Collings
c86ff1cce4 update
Reformatted header buttons and donate section, rewrote blurb, shortened up features descriptions and moved some to FAQ which I will tidy up later
2024-08-19 16:30:50 -04:00
LucasGGamerM
5427b21365 Merge pull request #508 from FineFindus/feat/duration
feat(MuteSheet): revert to row
2024-08-18 17:35:05 -03:00
Ruth Collings
d875edbc23 Update README.md 2024-08-15 14:57:57 -04:00
LucasGGamerM
4aecb17497 Merge pull request #507 from FineFindus/fix/note-save-crash
fix(Profile) note save crash
2024-08-14 15:56:20 -03:00
LucasGGamerM
806db1d09f Merge pull request #496 from FineFindus/fix/share-theme
fix(ExternalShareActivity): set theme before opening compose
2024-08-14 15:55:16 -03:00
FineFindus
49cf100d37 fix(MuteAccountSheet): set selected mute time 2024-08-14 19:12:35 +02:00
FineFindus
259a0ae140 feat: use mute row
Partially 6c519b3cb9 to use the full row
again, but keep the selection in a dialog. This improves the UI, as the
secondary button is confusing, and feels out-of-place.
2024-08-14 18:50:58 +02:00
FineFindus
420233da14 fix(Profile): remove note text before hiding NoteTextField
Fixes an issue, where if the NoteTextField was focussed and the note was
hidden, the note would saved and reshown once the NoteTextField was
hidden.
2024-08-12 07:16:15 +02:00
FineFindus
78ec24ff0c feat(Profile): only show note saved if note has not been hidden
The note text field being hidden is already enough comfirmation that the
note has been hidden.
2024-08-12 07:15:22 +02:00
FineFindus
a6f1d981db fix(Profile): use global context for note toast
Uses the global context for displaying the note saved toast. When using
the local context, it was somehow, sometimes null, leading to crashes.
2024-08-12 07:09:17 +02:00
LucasGGamerM
b07789b346 Merge pull request #505 from FineFindus/fix/scheduled-status-quote-crash
fix(StatusDisplayItem): check if account is null before adding quote
2024-08-11 19:32:08 -03:00
FineFindus
42c55d5eee fix(StatusDisplayItem): check if account is null before adding quote
This should fix crashes in the ScheduledStatusListFragment,
as ScheduledStatus do not contain an account.
2024-08-11 23:48:31 +02:00
LucasGGamerM
13545fd5ef Merge pull request #503 from FineFindus/feat/profile-note-save-feedback
feat(Profile): display Toast when saving note
2024-08-11 09:38:45 -03:00
FineFindus
134513babd feat(ProfileFragment): display toast when saving note
It can be quite unclear if the note has been saved. This adds a toast,
to indicate that the profile note has been saved.
2024-08-11 11:48:51 +02:00
FineFindus
91cb616164 refactor(ProfileFragment): remove duplicated InputType setting
The NoteEdit InputType is already set in the UI file.
2024-08-11 11:47:34 +02:00
FineFindus
f3d600282e fix(ExternalShareActivity): set theme before opening compose
Fixes https://github.com/sk22/megalodon/issues/926.
2024-08-05 14:28:20 +02:00
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
267 changed files with 6129 additions and 1502 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**

54
FAQ.md
View File

@@ -7,3 +7,57 @@ A: There are many, but the most outstanding differences are: the ability to have
Q: Will there ever be a version of Moshidon for iOS?
A: No. As android and iOS apps do not share code, it is incredibly hard to port.
## Detailed changes
### Features
* [Adding the ability to view other server's local timelines](https://github.com/LucasGGamerM/moshidon/tree/feature/local-timelines)
* [Adding the ability to load followers and following from remote instance](https://github.com/LucasGGamerM/moshidon/tree/feature/remote-followers)
* [Adding the ability to have filtered posts show with a warning](https://github.com/LucasGGamerM/moshidon/tree/feature/filters_again)
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
* Adding a useful private profile note box
* Auto hiding the compose button on scroll
* Adding the ability to remind yourself to add alt text to images
* An indicator for if an image has alt text or not
* Adding the ability to have drafts
* Also adding the ability to view announcements from your instance
* Adding the ability to post for local timeline only (Only on instances that support it!)
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Implement deleting and re-drafting](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
* [Implement a bookmark button and list](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
* [List favorited posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/favs-list)
* [Accept/reject follow requests](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/follow-requests)
* [Display content warning title above text](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
### Behavior
* Ask for confirmation before reblogging
* Adding a bottom option for the publish button, allowing for easier use on larger screens!
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
* [Always preserve content warnings when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
* [Display full image when adding image description](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [Set spoiler height independently to content height](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
### Visual
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)

215
README.md
View File

@@ -1,185 +1,91 @@
![MoshidonLogo](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
# ![MoshidonLogo](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png) Moshidon, the material you mastodon client!
# Moshidon, the material you mastodon client!
> A fork of [megalodon](https://github.com/sk22/megalodon) which is a fork of [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly wont ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer.
> A fast, highly customizable, up-to-date fork of [megalodon](https://github.com/sk22/megalodon) adding important features such as a fully federated timeline, unlisted posting, drafts, scheduled posts, bookmarks, and alt text warnings.
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=282C37&label=Download%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk)
## Download Now
[![Download nightly release](https://img.shields.io/badge/dynamic/json?color=282C37&label=Download%20Nightly%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon-nightly/releases/latest/download/moshidon-nightly.apk)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.moshinda"><img height="35" alt="Get it on Google Play" src="img/google-play-badge.png"></a> <a href="https://f-droid.org/pt_BR/packages/org.joinmastodon.android.moshinda"><img height="35" alt="Get it on F-Droid" src="img/f-droid-badge.png"></a> <a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="35" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
[![GitHub Release Download](https://img.shields.io/badge/dynamic/json?color=282C37&label=Download%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk) [![Translation status](https://translate.codeberg.org/widgets/moshidon/-/svg-badge.svg)](https://translate.codeberg.org/engage/moshidon/) [![GitHub Nightly Download](https://img.shields.io/badge/dynamic/json?color=282C37&label=Download%20Nightly%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon-nightly/releases/latest/download/moshidon-nightly.apk) [![GitHub Nightly Build Download](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml/badge.svg)](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml)
[![Translation status](https://translate.codeberg.org/widgets/moshidon/-/svg-badge.svg)](https://translate.codeberg.org/engage/moshidon/)
&nbsp;
[![Nightly build](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml/badge.svg)](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml)
## Donate
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.moshinda"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
&nbsp;
<a href="https://f-droid.org/pt_BR/packages/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on F-Droid" src="img/f-droid-badge.png"></a>
&nbsp;
<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>
<a href="https://github.com/sponsors/LucasGGamerM">Github Sponsors</a> | <a href="https://liberapay.com/LucasGGamerM/donate">Liberapay</a> | Monero Wallet Key: `4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j`
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate (Currently broken)
## Key Features
### You can also donate some Monero through this wallet address as well:
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j
[ screenshot of full timeline in default colour scheme ]
[ screenshot of full timeline in an alt colour scheme ]
[ screenshot of profile page ]
[ screenshot of compose post window ]
---
### Flexible Timelines
## Key features
[ Home dropdown menu ]
### **The ability to add other server's local timeline to your timelines**
Under the Home menu by default you can see your active account's timeline, your server's local timeline, and your server's federated timeline. You can also pin hashtags, lists, other servers, or make a custom view of just your posts, your bookmarks, or your favourites for quick access. Then sort these timelines to prioritize the ones you visit most often.
It can be accessed in the "Edit timelines" menu, where you can add a new "Community" to see other server's local posts!
### Multiple Accounts & Crossposting
### **View remote profiles**
Sign in to multiple accounts in the same app and easily switch between them. Press and hold on the boost or fave button to boost or fave a post to a different account than the one you are currently browsing with.
You can now see all of a profile follows and followers, by directly loading them from the profile's home instance. In case of a failed lookup, the app will automatically fall back to the older method.
[ boost icon pop up select profile ]
### **Translate posts easily**
### Drafts & Scheduled Posts
Allows you to easily translate posts in another language with a translate button! Your instance must support translation, otherwise it will not work.
Write posts and save them, or schedule them to post later. Edit and delete your drafts.
### **Show posts filtered with a warning**
### Alt Text Tag & Reminder
Allows you to have filtered posts collapsed with a warning! As shown in the screenshots:
An unobtrusive ALT tag appears on images with alt text. Clicking on the icon makes the alt text appear. By default, Moshidon will show a warning to add alt text if your post has any attachments lacking alt text. This is for better accessibility, and it can be disabled in settings. You can also hide from your feed all posts that are lacking in alt text.
Before | After
:-------------------------:|:-------------------------:
![Screenshot_20230205-100200edited](https://user-images.githubusercontent.com/71328265/216820539-20802dc5-e433-4511-b2d9-291d810e4ef2.png) | ![Screenshot_20230205-100203edited](https://user-images.githubusercontent.com/71328265/216820544-231b2966-f38f-4ec6-b555-d39c62433839.png)
[ image with alt text icon higlighted ]
[ alt text expanded ]
### Themes & Customization
### **Color themes**
Moshidon is designed according to Material Design principles. Follow your device's light or dark mode settings or change colour palette - your system's default, purple, black & white, "pitch black" (battery saving) and more. Customize your experience by moving or renaming the publish button, show or hide sensitive media by default, reduce motion, collapse long posts, add haptic feedback, or making the fave button a heart &hearts; or a star &starf;.
Allows you to change theme within the app. Supports Material You, purple, pink, green, blue, red, orange, yellow and Nord!
### Not Just For Mastodon
### **Unlisted posting**
Supports features available on other types of fediverse servers such as admin announcements, showing pronouns in user names, post translation, emoji reactions, local-only posting, and markdown or html in posts.
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Local”, “Community” and “Posts”).**
### Fully Federated Feed & Profiles
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in peoples Home timelines, but only if they follow you or someone they follow reposted/replied to your post.
See all public posts from servers your server federates with and fetch profiles from a user's local server for accurate up to date information.
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
## And more...
- quote-posts - links to fediverse posts in other posts will be loaded inline like quote-tweets
- manage pinned posts and bookmarks
- manage lists, filters, and most privacy settings
- display pronouns in timelines, threads, and user listings
- get only specific types of notifications (no more finished polls!), limit who you get notifications from, or group all notifications into one.
- automatically add "re:" to beginning of replies with content warnings
- ask before boosting or deleting posts
- when replying to a boosted post automatically mention the person who boosted it
- overlay audio from posts, allowing your existing media to keep playing
- auto-reveal CWs that are the same as ones you've already opened, or always reveal content warnings and sensitive media
- hide media previews in timelines (save data)
- show post interaction counts in timeline
- allow custom emoji in display names
- enable scrolling text for long display names
- hide interaction buttons
- show post dividers
### **Federated timeline**
**This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.**
## Installation & Releases
Despite being one of the main features of federated social media, the Federated timeline wasnt included in the official Mastodon app supposedly, because this conflicts with Googles safety requirements for apps on the Play Store.
Moshidon is available on GitHub, Google Play, F-Droid, and the IzzyOnDroid repo. All sources provide the same ` moshidon.apk ` stable release. Older releases are available on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
Thats one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
### How to Install from GitHub
[Download the latest stable release from Github](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk) and open it. You might have to accept installing APK files from your browser. Moshidon will automatically check for new updates available on GitHub and offer to download and install them within the app. You can also manually press “Check for updates” at the bottom of the settings page.
### **Image description viewer**
### Nightly Version
All ` moshidon-night.apk ` nightly builds can be downloaded on the [Nightly Releases](https://github.com/LucasGGamerM/moshidon-nightly/releases) page. This is an unstable version with an integrated updater for development and testing purposes. If you find any bugs with it, please file a bug report on our [Issues](https://github.com/LucasGGamerM/moshidon/issues) page.
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
This is important to **ensure the content youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
### **Reminder to add alt text to attached media**
By default, Moshidon will show a warning to add alt text if your post has any attachments without any alt text. This is for better accessibility, and it can easily be bypassed and disabled in settings.
### **Pinning posts**
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in peoples profiles shows all the posts they pinned.**
On the Fediverse, its quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
### **Bookmarks**
**They allow for quickly saving posts and viewing them through the Bookmarks button on the top right of your profile.**
To bookmark a post, press the button between the Favorite and Share buttons on the bottom of the post. Bookmarks are saved privately, so the post authors wont know you saved their post the list of bookmarked posts is only visible to you.
## Installation
**Press the download button above to download the APK. Open the downloaded file on your Android device to install it. Moshidon will automatically notify you about new updates inside the app.**
To install this app on your Android device, download the [latest release from GitHub](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk) and open it. You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
Moshidon makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)s automatic update checker. Moshidon will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
Moshidon is also available in [IzzyOnDroid repo](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda), compatible with all F-Droid clients. The APK provided here is the same as the one included in the Releases.
## Release variants
### Stable variant
All stable version downloads can be found on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
**`moshidon.apk`**
Variant with an integrated updater. If you download Moshidon from here (and not from an app store), just download the regular `moshidon.apk`.
### Nightly variant
All nightly builds can be downloaded at [Nightly Releases](https://github.com/LucasGGamerM/moshidon-nightly/releases) page.
**`moshidon-nightly.apk`**
Unstable variant with an integrated updater. It's for development and testing purposes. If you find any bugs with it, please file a bug report at our [issues](https://github.com/LucasGGamerM/moshidon/issues) page.
---
## Detailed changes
### Features
* [Adding the ability to view other server's local timelines](https://github.com/LucasGGamerM/moshidon/tree/feature/local-timelines)
* [Adding the ability to load followers and following from remote instance](https://github.com/LucasGGamerM/moshidon/tree/feature/remote-followers)
* [Adding the ability to have filtered posts show with a warning](https://github.com/LucasGGamerM/moshidon/tree/feature/filters_again)
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
* Adding a useful private profile note box
* Auto hiding the compose button on scroll
* Adding the ability to remind yourself to add alt text to images
* An indicator for if an image has alt text or not
* Adding the ability to have drafts
* Also adding the ability to view announcements from your instance
* Adding the ability to post for local timeline only (Only on instances that support it!)
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Implement deleting and re-drafting](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
* [Implement a bookmark button and list](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
* [List favorited posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/favs-list)
* [Accept/reject follow requests](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/follow-requests)
* [Display content warning title above text](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
### Behavior
* Allow for confirmation before reblogging
* Adding a bottom option for the publish button, allowing for easier use on larger screens!
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
* [Always preserve content warnings when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
* [Display full image when adding image description](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [Set spoiler height independently to content height](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
### Visual
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
## Building
## Building & Contributing
As this app is using Java 17 features, you need JDK 17 or newer to build it. Other than that, everything is pretty standard. You can either import the project into Android Studio and build it from there, or run the following command in the project directory:
@@ -191,14 +97,13 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
This project is released under the [GPL-3 License](./LICENSE).
## Links
## Contact & Support
**<a rel="me" href="https://floss.social/@moshidon">@moshidon@floss.social</a>**
[Official Matrix Chatroom](https://matrix.to/#/#moshidon:floss.social)
[F.A.Q](FAQ.md)
[Official matrix chatroom:](https://matrix.to/#/#moshidon:floss.social) https://matrix.to/#/#moshidon:floss.social
[Moshidon Roadmap](https://github.com/users/LucasGGamerM/projects/1)
[Moshidon roadmap](https://github.com/users/LucasGGamerM/projects/1)
<a rel="me" href="https://floss.social/@moshidon">@moshidon<wbr>@floss.social</a>
---

View File

@@ -1,23 +1,3 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
maven {
url "https://www.jitpack.io"
content {
includeModule 'com.github.UnifiedPush', 'android-connector'
}
}
mavenLocal()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
task clean(type: Delete) {
delete rootProject.buildDir
plugins {
id("com.android.application") version "8.7.2" apply false
}

View File

@@ -17,7 +17,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=false
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=true
android.nonFinalResIds=false
org.gradle.configuration-cache=true

View File

@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionSha256Sum=57dafb5c2622c6cc08b993c85b7c06956a2f53536432a30ead46166dbca0f1e9
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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 108
versionName "2.3.0+fork.108.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) -> {
@@ -82,6 +86,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
}
private void openComposeFragment(String accountID){
AccountSession session=AccountSessionManager.get(accountID);
UiUtils.setUserPreferredTheme(this, session);
getWindow().setBackgroundDrawable(null);
Intent intent=getIntent();

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,10 +101,15 @@ 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);
}
private void updateFollowState(boolean following) {
followMenuItem.setTitle(getString(following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
followMenuItem.setIcon(following ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
}
private void showMuteDialog(boolean mute) {
UiUtils.showConfirmationAlert(getContext(),
mute ? R.string.mo_unmute_hashtag : R.string.mo_mute_hashtag,
@@ -148,8 +153,6 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
}).exec(accountID);
}
@Override
protected TimelineDefinition makeTimelineDefinition() {
return TimelineDefinition.ofHashtag(hashtagName);
@@ -232,7 +235,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
@@ -292,6 +295,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
followMenuItem=optionsMenu.findItem(R.id.follow_hashtag);
pinMenuItem=optionsMenu.findItem(R.id.pin);
followMenuItem.setVisible(toolbarContentVisible);
updateFollowState(hashtag!=null && hashtag.following);
// pinMenuItem.setShowAsAction(toolbarContentVisible ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_ALWAYS);
super.updatePinButton(pinMenuItem);
@@ -388,8 +392,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
followButton.setTextVisible(true);
followProgress.setVisibility(View.GONE);
if(followMenuItem!=null){
followMenuItem.setTitle(getString(hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName));
followMenuItem.setIcon(hashtag.following ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
updateFollowState(hashtag.following);
}
if(muteMenuItem!=null){
muteMenuItem.setTitle(getString(filter.isPresent() ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
@@ -429,6 +432,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
return;
hashtag=result;
updateHeader();
updateFollowState(result.following);
followRequestRunning=false;
}

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

@@ -58,6 +58,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
@@ -287,11 +288,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus){
hideFab();
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
}else{
showFab();
savePrivateNote(noteEdit.getText().toString());
return;
}
showFab();
savePrivateNote(noteEdit.getText().toString());
});
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@@ -454,8 +454,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void hidePrivateNote(){
noteWrap.setVisibility(View.GONE);
noteEdit.setText(null);
noteWrap.setVisibility(View.GONE);
}
private void savePrivateNote(String note){
@@ -469,6 +469,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public void onSuccess(Relationship result) {
updateRelationship(result);
invalidateOptionsMenu();
if(!TextUtils.isEmpty(result.note))
Toast.makeText(MastodonApp.context, R.string.mo_personal_note_saved, Toast.LENGTH_SHORT).show();
}
@Override
@@ -817,18 +819,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 +861,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 +958,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 +1589,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,74 @@
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 onClick(){
// explicitly do nothing when clicked
}
@Override
public boolean isEnabled(){
return false;
}
@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() && statusForContent.account!=null){
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,61 @@ 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();
// account may be null for scheduled posts
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 +499,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

@@ -2,14 +2,8 @@ package org.joinmastodon.android.ui.sheets;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Typeface;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
@@ -17,6 +11,7 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.M3Switch;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -44,10 +39,10 @@ public class MuteAccountConfirmationSheet extends AccountRestrictionConfirmation
addRow(R.drawable.ic_fluent_alert_off_24_regular, R.string.mo_mute_notifications, m3Switch);
// add mute duration (Moshidon)
secondaryBtn.setVisibility(View.VISIBLE);
secondaryBtn.setOnClickListener(v->getMuteDurationDialog(context, muteDuration, secondaryBtn).show());
secondaryBtn.setText(R.string.sk_duration_indefinite);
secondaryBtn.setTypeface(null, Typeface.BOLD_ITALIC);
Button muteDurationBtn=new Button(getContext());
muteDurationBtn.setOnClickListener(v->getMuteDurationDialog(context, muteDuration, muteDurationBtn).show());
muteDurationBtn.setText(R.string.sk_duration_indefinite);
addRow(R.drawable.ic_fluent_clock_20_regular, R.string.sk_mute_label, muteDurationBtn);
}
@NonNull
@@ -55,6 +50,15 @@ public class MuteAccountConfirmationSheet extends AccountRestrictionConfirmation
M3AlertDialogBuilder builder=new M3AlertDialogBuilder(context);
builder.setTitle(R.string.sk_mute_label);
builder.setIcon(R.drawable.ic_fluent_clock_20_regular);
List<Duration> durations =List.of(Duration.ZERO,
Duration.ofMinutes(5),
Duration.ofMinutes(30),
Duration.ofHours(1),
Duration.ofHours(6),
Duration.ofDays(1),
Duration.ofDays(3),
Duration.ofDays(7),
Duration.ofDays(7));
String[] choices = {context.getString(R.string.sk_duration_indefinite),
context.getString(R.string.sk_duration_minutes_5),
@@ -65,35 +69,14 @@ public class MuteAccountConfirmationSheet extends AccountRestrictionConfirmation
context.getString(R.string.sk_duration_days_3),
context.getString(R.string.sk_duration_days_7)};
builder.setSingleChoiceItems(choices, 0, (dialog, which) -> {});
builder.setSingleChoiceItems(choices, durations.indexOf(muteDuration.get()), (dialog, which) -> {});
builder.setPositiveButton(R.string.ok, (dialog, which)->{
int selected = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
if(selected==0){
muteDuration.set(Duration.ZERO);
}else if(selected==1){
muteDuration.set(Duration.ofMinutes(5));
}else if(selected==2){
muteDuration.set(Duration.ofMinutes(30));
}else if(selected==3){
muteDuration.set(Duration.ofHours(1));
}else if(selected==4){
muteDuration.set(Duration.ofHours(6));
}else if(selected==5){
muteDuration.set(Duration.ofDays(1));
}else if(selected==6){
muteDuration.set(Duration.ofDays(3));
}else if(selected==7){
muteDuration.set(Duration.ofDays(7));
}
if(selected >= 0 && selected <= 7){
button.setText(choices[selected]);
} else {
Toast.makeText(context, "" + selected, Toast.LENGTH_SHORT).show();
}
muteDuration.set(durations.get(selected));
button.setText(choices[selected]);
});
builder.setNegativeButton(R.string.cancel, ((dialogInterface, i) -> {}));
builder.setNegativeButton(R.string.cancel, null);
return builder;
}

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(!TextUtils.isEmpty(text) && (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>

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