Compare commits
356 Commits
2.3.0+fork
...
merge-upst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26df5762f | ||
|
|
2021c335ac | ||
|
|
d121f14d30 | ||
|
|
d1a2a70cdc | ||
|
|
89ef482e2e | ||
|
|
9918649d7c | ||
|
|
09185faf9a | ||
|
|
bed201a2f7 | ||
|
|
5e7a4c0136 | ||
|
|
bcb8717d5f | ||
|
|
ed1c1bd097 | ||
|
|
f480532fd6 | ||
|
|
cc056cef08 | ||
|
|
9e7445b8d8 | ||
|
|
e2d96d3bc7 | ||
|
|
4f5c99be21 | ||
|
|
0388f9d9be | ||
|
|
c45128ced0 | ||
|
|
f404d2f9cd | ||
|
|
2dada69eb8 | ||
|
|
b7e0596014 | ||
|
|
dbef984908 | ||
|
|
55259f103d | ||
|
|
81519fe906 | ||
|
|
07ab3c394a | ||
|
|
620cc94351 | ||
|
|
2494918171 | ||
|
|
a0bed5e739 | ||
|
|
a42bf86a1e | ||
|
|
9c7ae9653b | ||
|
|
44473705b9 | ||
|
|
f1d40f8963 | ||
|
|
fbae5d8816 | ||
|
|
43afbb7523 | ||
|
|
080815846f | ||
|
|
4b6c6cbcfe | ||
|
|
117037e7e8 | ||
|
|
05972fc702 | ||
|
|
28084b9f9e | ||
|
|
02010df408 | ||
|
|
38f77c69d1 | ||
|
|
d0a8c26b65 | ||
|
|
401602e5bc | ||
|
|
ccd9dbed13 | ||
|
|
736d5d9f3e | ||
|
|
32451c0eea | ||
|
|
e7ed8d5590 | ||
|
|
79d04a949b | ||
|
|
5cd99b9763 | ||
|
|
3f30c2f3be | ||
|
|
db8187bbc9 | ||
|
|
4e1632aa19 | ||
|
|
a813f961af | ||
|
|
f6417662b9 | ||
|
|
2d1bc09616 | ||
|
|
d9e5ea5b80 | ||
|
|
1ab6bc3663 | ||
|
|
effe3a079f | ||
|
|
7d65563096 | ||
|
|
857c5b9a55 | ||
|
|
e49760c5a0 | ||
|
|
93b97e99a8 | ||
|
|
6d148b1f7a | ||
|
|
4d24e4e846 | ||
|
|
9f5c420e66 | ||
|
|
ca07240a70 | ||
|
|
1b6978bb93 | ||
|
|
d4b20fc5f7 | ||
|
|
d3d95c7963 | ||
|
|
98c5baecad | ||
|
|
766b7b8c45 | ||
|
|
896ded9ff3 | ||
|
|
7b31543d7a | ||
|
|
ff61c3c02e | ||
|
|
aa8562dc88 | ||
|
|
ec495750fe | ||
|
|
af33c593b5 | ||
|
|
4586e42459 | ||
|
|
2a45b7d13d | ||
|
|
60d573de58 | ||
|
|
2d7499e8cc | ||
|
|
9ec82ae090 | ||
|
|
da783c3771 | ||
|
|
9869581515 | ||
|
|
f45fb87ea5 | ||
|
|
d80ac7557e | ||
|
|
58403fef59 | ||
|
|
87ca8b1ad7 | ||
|
|
04e1f9e148 | ||
|
|
1e1fe47638 | ||
|
|
c567e264de | ||
|
|
c142f82fd1 | ||
|
|
c0cf5b40fa | ||
|
|
b45e87b271 | ||
|
|
958243e65d | ||
|
|
8cc91b0f02 | ||
|
|
0ac7d3530e | ||
|
|
10d42264c8 | ||
|
|
72fee62472 | ||
|
|
9b4528b69a | ||
|
|
4b0cf4311d | ||
|
|
4ceea9100d | ||
|
|
2522cd26d1 | ||
|
|
294bcef5f6 | ||
|
|
e61618bf2c | ||
|
|
70e5030fe1 | ||
|
|
7c270aadda | ||
|
|
30eaeb006d | ||
|
|
5e11b3fb7a | ||
|
|
d6089d0c1e | ||
|
|
1bb288e565 | ||
|
|
d42eb934d5 | ||
|
|
2fecd6f0a3 | ||
|
|
c3a2b5a6e1 | ||
|
|
ccff874bcf | ||
|
|
9e7f351174 | ||
|
|
a9e7fab029 | ||
|
|
aad8abd3bf | ||
|
|
d938c8c470 | ||
|
|
124ad8cb0e | ||
|
|
a17c3293b5 | ||
|
|
5868da3337 | ||
|
|
731ee17d6d | ||
|
|
edddc297dd | ||
|
|
85152102fd | ||
|
|
fba4c1c6d6 | ||
|
|
593e8d0eb7 | ||
|
|
bafb1ba8f8 | ||
|
|
36124db2aa | ||
|
|
155a093eb7 | ||
|
|
ddee29bf03 | ||
|
|
99e2958649 | ||
|
|
519afb6259 | ||
|
|
6ab8991c45 | ||
|
|
44200a4d56 | ||
|
|
e929478b6a | ||
|
|
cf98aa4939 | ||
|
|
22585a2ec5 | ||
|
|
fa6abd44c3 | ||
|
|
1d7cbcc4e1 | ||
|
|
5edbe9b826 | ||
|
|
b5027ee66f | ||
|
|
499baeb496 | ||
|
|
72d486e992 | ||
|
|
3020c826ed | ||
|
|
34f3e33efc | ||
|
|
5b25168eb7 | ||
|
|
c785bbb2d7 | ||
|
|
45324a5598 | ||
|
|
55ad624209 | ||
|
|
ed0fe1e803 | ||
|
|
18079454a9 | ||
|
|
87cb80867a | ||
|
|
1829dc1d9d | ||
|
|
519cb672d2 | ||
|
|
e0a5e259f7 | ||
|
|
86512e237e | ||
|
|
b9efdbbb40 | ||
|
|
d369129ac7 | ||
|
|
c01135d822 | ||
|
|
653a66bd87 | ||
|
|
ffc2990b32 | ||
|
|
8b26fb3184 | ||
|
|
3fec39835c | ||
|
|
5402e78342 | ||
|
|
8995cfcc9d | ||
|
|
8d3b1f40a3 | ||
|
|
f775bae93e | ||
|
|
ca84bc36e3 | ||
|
|
2a775aba70 | ||
|
|
7cd65dcb32 | ||
|
|
4d694b2725 | ||
|
|
2e39f81c36 | ||
|
|
803e66f999 | ||
|
|
ed22d3b4ed | ||
|
|
ec72653dba | ||
|
|
9b1e79eba8 | ||
|
|
ca4a1d461a | ||
|
|
b90607582a | ||
|
|
0c95f6db1b | ||
|
|
4caa6cf650 | ||
|
|
bc08c149b7 | ||
|
|
4a783957ed | ||
|
|
113b47d9e2 | ||
|
|
96ccb14a59 | ||
|
|
bc8b0e192c | ||
|
|
72400703ab | ||
|
|
91345268e8 | ||
|
|
bff6ac4a14 | ||
|
|
75183f5625 | ||
|
|
7654b869ba | ||
|
|
f176384bcc | ||
|
|
a4f2a733b5 | ||
|
|
9ea48fa0ab | ||
|
|
cc2076ec10 | ||
|
|
b5a0c293c5 | ||
|
|
3265cfe772 | ||
|
|
857d0ce539 | ||
|
|
31a52c2790 | ||
|
|
94ce329f49 | ||
|
|
a67c8b36b1 | ||
|
|
ff90e21e86 | ||
|
|
5fd2e322f6 | ||
|
|
cdd9b0553f | ||
|
|
6157d4942a | ||
|
|
e68e870a7c | ||
|
|
0788b03828 | ||
|
|
b670da04ed | ||
|
|
f70abbbb73 | ||
|
|
a0dd75890c | ||
|
|
38df70cd9e | ||
|
|
e18fa57d73 | ||
|
|
51f6264534 | ||
|
|
feff45721f | ||
|
|
20558f0a19 | ||
|
|
e97a479e65 | ||
|
|
f590fde7a4 | ||
|
|
77c5173014 | ||
|
|
dd4bed0027 | ||
|
|
229c0b359f | ||
|
|
0d4158a612 | ||
|
|
cfde4425b7 | ||
|
|
15f84af757 | ||
|
|
39895ff79a | ||
|
|
3d2b67efc5 | ||
|
|
ebd637546f | ||
|
|
618946a8c6 | ||
|
|
e8ce2a7e35 | ||
|
|
f8dbecc3e1 | ||
|
|
76030c041c | ||
|
|
998e186f8b | ||
|
|
75bc0aa052 | ||
|
|
edb4b7152b | ||
|
|
66c9e0d908 | ||
|
|
0bdb23e462 | ||
|
|
d9ce0e6d31 | ||
|
|
aa3c8b5812 | ||
|
|
4392ce20b6 | ||
|
|
d5085c5899 | ||
|
|
9a1668a29a | ||
|
|
4d598bd2fe | ||
|
|
57911ce070 | ||
|
|
f9f8c4a9ef | ||
|
|
6ad8a85044 | ||
|
|
14e6187efc | ||
|
|
bd88606c48 | ||
|
|
b38c78c50a | ||
|
|
4c9f7fc8be | ||
|
|
4f11a79d2a | ||
|
|
7ab920d943 | ||
|
|
c8f2e7a752 | ||
|
|
cdcc428e86 | ||
|
|
7bb5584dd9 | ||
|
|
0c5c51dc17 | ||
|
|
b17b7afd03 | ||
|
|
e2e8173db6 | ||
|
|
5e7f4bda82 | ||
|
|
38996d8921 | ||
|
|
6cb8961639 | ||
|
|
18ac0423c0 | ||
|
|
d2704c1f0d | ||
|
|
ed23b7cc13 | ||
|
|
47ab6b5a08 | ||
|
|
70686bbbd0 | ||
|
|
b53997261e | ||
|
|
efd9b1e916 | ||
|
|
b51033a421 | ||
|
|
e0a793e176 | ||
|
|
542c24ff75 | ||
|
|
965f7c6d1d | ||
|
|
2df6d9ce60 | ||
|
|
5d3afc1b0e | ||
|
|
0c8f903eb6 | ||
|
|
ef23734b22 | ||
|
|
c0ab3a47ae | ||
|
|
f4a94bc42e | ||
|
|
69b95c27ec | ||
|
|
c64d6db859 | ||
|
|
730adc34dd | ||
|
|
a082a3d325 | ||
|
|
c7820ddac8 | ||
|
|
169fbc2d52 | ||
|
|
44e3e5faaf | ||
|
|
711c70af2f | ||
|
|
1d405d9e48 | ||
|
|
892ce130ca | ||
|
|
fea9d6e761 | ||
|
|
88e11f25a7 | ||
|
|
6faa497569 | ||
|
|
1d45899f8c | ||
|
|
938643f9e2 | ||
|
|
1ccf9bf4b7 | ||
|
|
ad9b5f028d | ||
|
|
e52154fd17 | ||
|
|
54202f3e8d | ||
|
|
d4b8c350dc | ||
|
|
daaf467168 | ||
|
|
eda52d5a55 | ||
|
|
0700274d6b | ||
|
|
faee3e3dd6 | ||
|
|
129ce09c9f | ||
|
|
368e226257 | ||
|
|
93321720e1 | ||
|
|
96c1c036a8 | ||
|
|
edffe0fd42 | ||
|
|
d1d8f2ef45 | ||
|
|
95ba52b761 | ||
|
|
02c8a56c17 | ||
|
|
b34a855150 | ||
|
|
b736cf2925 | ||
|
|
eea78302ab | ||
|
|
09a7da2952 | ||
|
|
ebf3b075b8 | ||
|
|
28c851a630 | ||
|
|
44194e5d43 | ||
|
|
58bb492461 | ||
|
|
00726abec1 | ||
|
|
c9e93bb6a6 | ||
|
|
f980bba7cd | ||
|
|
eea350f84e | ||
|
|
44bec713ae | ||
|
|
2139dbd76b | ||
|
|
ad92a08271 | ||
|
|
b0dc521b90 | ||
|
|
732de52ebb | ||
|
|
34b2a4e2a0 | ||
|
|
2291c2bb28 | ||
|
|
7581a6cf7e | ||
|
|
2c86356389 | ||
|
|
6815cd77e4 | ||
|
|
4f9a1db26b | ||
|
|
d3bcf9d8ee | ||
|
|
35d39b63e2 | ||
|
|
15c77e4220 | ||
|
|
962c094f7e | ||
|
|
c6081fb4d4 | ||
|
|
1832de3aab | ||
|
|
5c15914bab | ||
|
|
7e244d65bf | ||
|
|
9c8e6647bc | ||
|
|
4d128b4408 | ||
|
|
e0098efe32 | ||
|
|
42f5975f6b | ||
|
|
1045593cc9 | ||
|
|
3443b80ff7 | ||
|
|
9fe6b3457a | ||
|
|
0a26380f23 | ||
|
|
ef3605c8e3 | ||
|
|
3df20c4749 | ||
|
|
c63e87de45 | ||
|
|
1151e41846 | ||
|
|
09668d2500 | ||
|
|
773a24af2c | ||
|
|
b1f6409c8d | ||
|
|
ee8e535e58 | ||
|
|
d128f29bbc |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -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']
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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**
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
||||
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate (Currently broken)
|
||||
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate
|
||||
|
||||
### You can also donate some Monero through this wallet address as well:
|
||||
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j
|
||||
|
||||
@@ -15,9 +15,9 @@ android {
|
||||
archivesBaseName = "moshidon"
|
||||
applicationId "org.joinmastodon.android.moshinda"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 105
|
||||
versionName "2.3.0+fork.105.moshinda"
|
||||
targetSdk 34
|
||||
versionCode 107
|
||||
versionName "2.3.0+fork.107.moshinda"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
@@ -102,9 +102,14 @@ android {
|
||||
shrinkResources true
|
||||
versionNameSuffix '-play'
|
||||
}
|
||||
githubRelease { initWith release }
|
||||
githubRelease {
|
||||
initWith release
|
||||
versionNameSuffix '-github'
|
||||
}
|
||||
fdroidRelease {
|
||||
initWith release
|
||||
// The F-droid build system doesn't like this at all for some reason.
|
||||
// versionNameSuffix '-fdroid'
|
||||
// signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) -> {
|
||||
|
||||
@@ -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"><provider></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">
|
||||
*<manifest>
|
||||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <provider
|
||||
* android:name="androidx.core.content.FileProvider"
|
||||
* android:authorities="com.mydomain.fileprovider"
|
||||
* android:exported="false"
|
||||
* android:grantUriPermissions="true">
|
||||
* ...
|
||||
* </provider>
|
||||
* ...
|
||||
* </application>
|
||||
*</manifest></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><provider></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><paths></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">
|
||||
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
* <files-path name="my_images" path="images/"/>
|
||||
* ...
|
||||
*</paths>
|
||||
*</pre>
|
||||
* <p>
|
||||
* The <code><paths></code> element must contain one or more of the following child elements:
|
||||
* </p>
|
||||
* <dl>
|
||||
* <dt>
|
||||
* <pre class="prettyprint">
|
||||
*<files-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</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>
|
||||
*<cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</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">
|
||||
*<external-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</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">
|
||||
*<external-files-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</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">
|
||||
*<external-cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</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">
|
||||
*<external-media-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</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><paths></code> for each directory that contains
|
||||
* files for which you want content URIs. For example, these XML elements specify two directories:
|
||||
* <pre class="prettyprint">
|
||||
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
* <files-path name="my_images" path="images/"/>
|
||||
* <files-path name="my_docs" path="docs/"/>
|
||||
*</paths>
|
||||
*</pre>
|
||||
* <p>
|
||||
* Put the <code><paths></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"><meta-data></a> element
|
||||
* as a child of the <code><provider></code> element that defines the FileProvider. Set the
|
||||
* <code><meta-data></code> element's "android:name" attribute to
|
||||
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
|
||||
* to <code>@xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
|
||||
* extension). For example:
|
||||
* <pre class="prettyprint">
|
||||
*<provider
|
||||
* android:name="androidx.core.content.FileProvider"
|
||||
* android:authorities="com.mydomain.fileprovider"
|
||||
* android:exported="false"
|
||||
* android:grantUriPermissions="true">
|
||||
* <meta-data
|
||||
* android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
* android:resource="@xml/file_paths" />
|
||||
*</provider>
|
||||
*</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><paths></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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()){
|
||||
|
||||
@@ -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++){
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -101,7 +101,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
||||
}
|
||||
|
||||
private void updateMuteState(boolean newMute) {
|
||||
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag));
|
||||
muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtagName));
|
||||
muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtag).build();
|
||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtagName).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -817,18 +817,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.edit_note);
|
||||
}
|
||||
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
||||
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
|
||||
openWithAccounts.setVisible(hasMultipleAccounts);
|
||||
SubMenu accountsMenu=openWithAccounts.getSubMenu();
|
||||
if(hasMultipleAccounts){
|
||||
accountsMenu.clear();
|
||||
UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL(
|
||||
getActivity(), s.getID(), account.url, false
|
||||
));
|
||||
}
|
||||
menu.findItem(R.id.open_with_account).setVisible(hasMultipleAccounts);
|
||||
|
||||
if(isOwnProfile) {
|
||||
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
||||
menu.findItem(R.id.favorites).setIcon(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular);
|
||||
UiUtils.insetPopupMenuIcon(getContext(), menu.findItem(R.id.favorites));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -865,10 +859,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
UiUtils.openSystemShareSheet(getActivity(), account);
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
@@ -965,11 +956,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
.show();
|
||||
}
|
||||
invalidateOptionsMenu();
|
||||
}else if(id==R.id.manage_user_lists){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
|
||||
}else if(id==R.id.open_with_account){
|
||||
UiUtils.pickAccount(getActivity(), accountID, R.string.sk_open_with_account, R.drawable.ic_fluent_person_swap_24_regular, session ->UiUtils.openURL(
|
||||
getActivity(), session.getID(), account.url, false
|
||||
), null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1597,8 +1587,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public void setImage(int index, Drawable image){
|
||||
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
|
||||
span.setDrawable(image);
|
||||
title.invalidate();
|
||||
value.invalidate();
|
||||
title.setText(title.getText());
|
||||
value.setText(value.getText());
|
||||
toolbarTitleView.setText(toolbarTitleView.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class ErrorStatusDisplayItem extends StatusDisplayItem{
|
||||
private final Exception exception;
|
||||
|
||||
public ErrorStatusDisplayItem(String parentID, Status status, BaseStatusListFragment<?> parentFragment, Exception exception) {
|
||||
super(parentID, parentFragment);
|
||||
this.exception=exception;
|
||||
this.status=status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.ERROR_ITEM;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<ErrorStatusDisplayItem> {
|
||||
private final Button openInBrowserButton;
|
||||
|
||||
public Holder(Context context, ViewGroup parent) {
|
||||
super(context, R.layout.display_item_error, parent);
|
||||
openInBrowserButton=findViewById(R.id.button_open_browser);
|
||||
openInBrowserButton.setOnClickListener(v -> UiUtils.launchWebBrowser(v.getContext(), item.status.url));
|
||||
findViewById(R.id.button_copy_error_details).setOnClickListener(this::copyErrorDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ErrorStatusDisplayItem item) {
|
||||
openInBrowserButton.setEnabled(item.status!=null && item.status.url!=null);
|
||||
}
|
||||
|
||||
private void copyErrorDetails(View v) {
|
||||
StringWriter stringWriter=new StringWriter();
|
||||
PrintWriter printWriter=new PrintWriter(stringWriter);
|
||||
item.exception.printStackTrace(printWriter);
|
||||
String stackTrace=stringWriter.toString();
|
||||
|
||||
String errorDetails=String.format(
|
||||
"App Version: %s\nOS Version: %s\nStatus URL: %s\nException: %s",
|
||||
v.getContext().getString(R.string.mo_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
|
||||
"Android " + Build.VERSION.RELEASE,
|
||||
item.status.url,
|
||||
stackTrace
|
||||
);
|
||||
UiUtils.copyText(v, errorDetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,13 +10,17 @@ import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
@@ -28,30 +32,35 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
@@ -79,6 +88,10 @@ public abstract class StatusDisplayItem{
|
||||
public static final int FLAG_IS_FOR_QUOTE=1 << 7;
|
||||
public static final int FLAG_NO_MEDIA_PREVIEW=1 << 8;
|
||||
|
||||
|
||||
private final static Pattern QUOTE_MENTION_PATTERN=Pattern.compile("(?:<p>)?\\s?(?:RE:\\s?(<br\\s?\\/?>)?)?<a href=\"https:\\/\\/[^\"]+\"[^>]*><span class=\"invisible\">https:\\/\\/<\\/span><span class=\"ellipsis\">[^<]+<\\/span><span class=\"invisible\">[^<]+<\\/span><\\/a>(?:<\\/p>)?$");
|
||||
private final static Pattern QUOTE_PATTERN=Pattern.compile("https://[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,8}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$");
|
||||
|
||||
public void setAncestryInfo(
|
||||
boolean hasDescendantNeighbor,
|
||||
boolean hasAncestoringNeighbor,
|
||||
@@ -141,6 +154,7 @@ public abstract class StatusDisplayItem{
|
||||
case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type);
|
||||
case SECTION_HEADER -> null; // new SectionHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case ERROR_ITEM -> new ErrorStatusDisplayItem.Holder(activity, parent);
|
||||
case DUMMY -> new DummyStatusDisplayItem.Holder(activity);
|
||||
};
|
||||
}
|
||||
@@ -166,205 +180,202 @@ public abstract class StatusDisplayItem{
|
||||
Status statusForContent=status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus s ? s : null;
|
||||
try{
|
||||
ScheduledStatus scheduledStatus=parentObject instanceof ScheduledStatus s ? s : null;
|
||||
|
||||
// Hide statuses that have a filter action of hide
|
||||
if(!new StatusFilterPredicate(accountID, filterContext, FilterAction.HIDE).test(status))
|
||||
return new ArrayList<StatusDisplayItem>() ;
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
if((flags&FLAG_NO_HEADER)==0){
|
||||
ReblogOrReplyLineStatusDisplayItem replyLine=null;
|
||||
boolean threadReply=statusForContent.inReplyToAccountId!=null &&
|
||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||
|
||||
if((flags & FLAG_NO_HEADER)==0){
|
||||
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
||||
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
||||
Account account=knownAccounts.get(statusForContent.inReplyToAccountId);
|
||||
replyLine=buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||
}
|
||||
|
||||
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
||||
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
||||
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||
if(status.reblog!=null){
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
|
||||
statusForContent.rebloggedBy=status.account;
|
||||
|
||||
String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName());
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}, null, status, status.account));
|
||||
}else if(!(status.tags.isEmpty() ||
|
||||
fragment instanceof HashtagTimelineFragment ||
|
||||
fragment instanceof ListTimelineFragment
|
||||
) && fragment.getParentFragment() instanceof HomeTabFragment home){
|
||||
home.getHashtags().stream()
|
||||
.filter(followed->status.tags.stream()
|
||||
.anyMatch(hashtag->followed.name.equalsIgnoreCase(hashtag.name)))
|
||||
.findAny()
|
||||
// post contains a hashtag the user is following
|
||||
.ifPresent(hashtag->items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, hashtag.name, List.of(),
|
||||
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||
i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag),
|
||||
status
|
||||
)));
|
||||
}
|
||||
|
||||
if(replyLine!=null){
|
||||
Optional<ReblogOrReplyLineStatusDisplayItem> primaryLine=items.stream()
|
||||
.filter(i->i instanceof ReblogOrReplyLineStatusDisplayItem)
|
||||
.map(ReblogOrReplyLineStatusDisplayItem.class::cast)
|
||||
.findFirst();
|
||||
|
||||
if(primaryLine.isPresent()){
|
||||
primaryLine.get().extra=replyLine;
|
||||
}else{
|
||||
items.add(replyLine);
|
||||
}
|
||||
}
|
||||
|
||||
if((flags&FLAG_CHECKABLE)!=0)
|
||||
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
else
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus));
|
||||
}
|
||||
|
||||
if(status.reblog!=null){
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
LegacyFilter applyingFilter=null;
|
||||
if(status.filtered!=null){
|
||||
ArrayList<FilterResult> filters= new ArrayList<>(status.filtered);
|
||||
|
||||
statusForContent.rebloggedBy = status.account;
|
||||
// Only add client filters if there are no pre-existing status filter
|
||||
if(filters.isEmpty())
|
||||
filters.addAll(AccountSessionManager.get(accountID).getClientSideFilters(status));
|
||||
|
||||
String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName());
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}, null, status, status.account));
|
||||
} else if (!(status.tags.isEmpty() ||
|
||||
fragment instanceof HashtagTimelineFragment ||
|
||||
fragment instanceof ListTimelineFragment
|
||||
) && fragment.getParentFragment() instanceof HomeTabFragment home) {
|
||||
home.getHashtags().stream()
|
||||
.filter(followed -> status.tags.stream()
|
||||
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
|
||||
.findAny()
|
||||
// post contains a hashtag the user is following
|
||||
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, hashtag.name, List.of(),
|
||||
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||
i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag),
|
||||
status
|
||||
)));
|
||||
}
|
||||
|
||||
if (replyLine != null) {
|
||||
Optional<ReblogOrReplyLineStatusDisplayItem> primaryLine = items.stream()
|
||||
.filter(i -> i instanceof ReblogOrReplyLineStatusDisplayItem)
|
||||
.map(ReblogOrReplyLineStatusDisplayItem.class::cast)
|
||||
.findFirst();
|
||||
|
||||
if (primaryLine.isPresent()) {
|
||||
primaryLine.get().extra = replyLine;
|
||||
} else {
|
||||
items.add(replyLine);
|
||||
for(FilterResult filter : filters){
|
||||
LegacyFilter f=filter.filter;
|
||||
if(f.isActive() && filterContext!=null && f.context.contains(filterContext)){
|
||||
applyingFilter=f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if((flags & FLAG_CHECKABLE)!=0)
|
||||
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
else
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus));
|
||||
}
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(statusForContent.hasSpoiler()){
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed=true;
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
|
||||
if((flags&FLAG_IS_FOR_QUOTE)!=0){
|
||||
for(StatusDisplayItem item : spoilerItem.contentItems){
|
||||
item.isForQuote=true;
|
||||
}
|
||||
}
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
LegacyFilter applyingFilter=null;
|
||||
if(status.filtered!=null){
|
||||
for(FilterResult filter:status.filtered){
|
||||
LegacyFilter f=filter.filter;
|
||||
if(f.isActive() && filterContext != null && f.context.contains(filterContext)){
|
||||
applyingFilter=f;
|
||||
break;
|
||||
if(statusForContent.quote!=null){
|
||||
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
|
||||
if(quoteInlineIndex!=-1)
|
||||
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
|
||||
else {
|
||||
// hide non-official quote patters
|
||||
Matcher matcher=QUOTE_MENTION_PATTERN.matcher(status.content);
|
||||
if(matcher.find()){
|
||||
String quoteMention=matcher.group();
|
||||
statusForContent.content=statusForContent.content.replace(quoteMention, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Moshidon
|
||||
if(applyingFilter==null){
|
||||
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, FilterAction.WARN);
|
||||
predicate.test(status);
|
||||
applyingFilter = predicate.getApplyingFilter();
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(statusForContent.hasSpoiler()){
|
||||
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
|
||||
if((flags & FLAG_IS_FOR_QUOTE)!=0){
|
||||
for(StatusDisplayItem item:spoilerItem.contentItems){
|
||||
item.isForQuote=true;
|
||||
}
|
||||
}
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
if(statusForContent.quote!=null) {
|
||||
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
|
||||
if (quoteInlineIndex!=-1)
|
||||
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
|
||||
}
|
||||
|
||||
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
|
||||
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
|
||||
contentItems.add(text);
|
||||
}else if(!hasSpoiler && header!=null){
|
||||
header.needBottomPadding=true;
|
||||
}else if(hasSpoiler){
|
||||
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
if(!imageAttachments.isEmpty() && (flags & FLAG_NO_MEDIA_PREVIEW)==0){
|
||||
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant);
|
||||
for (Attachment att : imageAttachments) {
|
||||
if (att.blurhashPlaceholder == null) {
|
||||
att.blurhashPlaceholder = new ColorDrawable(color);
|
||||
}
|
||||
}
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
statusForContent.sensitiveRevealed=false;
|
||||
statusForContent.sensitive=true;
|
||||
} else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
statusForContent.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
if((flags & FLAG_NO_MEDIA_PREVIEW)!=0){
|
||||
contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent));
|
||||
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
if(att.type==Attachment.Type.UNKNOWN){
|
||||
contentItems.add(new FileStatusDisplayItem(parentID, fragment, att));
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
|
||||
}
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags & FLAG_NO_MEDIA_PREVIEW)==0));
|
||||
}
|
||||
if(statusForContent.quote!=null && !(parentObject instanceof Notification)){
|
||||
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
|
||||
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
|
||||
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
|
||||
if(applyingFilter!=null)
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent, (flags&FLAG_NO_TRANSLATE)!=0);
|
||||
contentItems.add(text);
|
||||
}else if(!hasSpoiler && header!=null){
|
||||
header.needBottomPadding=true;
|
||||
}else if(hasSpoiler){
|
||||
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE));
|
||||
}
|
||||
if(contentItems!=items && statusForContent.spoilerRevealed){
|
||||
items.addAll(contentItems);
|
||||
}
|
||||
AccountLocalPreferences lp=fragment.getLocalPrefs();
|
||||
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
|
||||
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
|
||||
statusForContent.reactions!=null){
|
||||
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
|
||||
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
|
||||
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
|
||||
}
|
||||
FooterStatusDisplayItem footer=null;
|
||||
if((flags & FLAG_NO_FOOTER)==0){
|
||||
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
|
||||
footer.hideCounts=hideCounts;
|
||||
items.add(footer);
|
||||
}
|
||||
boolean inset=(flags & FLAG_INSET)!=0;
|
||||
boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0;
|
||||
// add inset dummy so last content item doesn't clip out of inset bounds
|
||||
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){
|
||||
items.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
// in case we ever need the dummy to display a margin for the media grid again:
|
||||
// (i forgot why we apparently don't need this anymore)
|
||||
// !contentItems.isEmpty() && contentItems
|
||||
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
|
||||
}
|
||||
GapStatusDisplayItem gap=null;
|
||||
if((flags & FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment))
|
||||
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
|
||||
int i=1;
|
||||
for(StatusDisplayItem item:items){
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
item.status=statusForContent;
|
||||
item.isForQuote=true;
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
if(items!=contentItems && !statusForContent.spoilerRevealed){
|
||||
for(StatusDisplayItem item:contentItems){
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
if(!imageAttachments.isEmpty() && (flags&FLAG_NO_MEDIA_PREVIEW)==0){
|
||||
int color=UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant);
|
||||
for(Attachment att : imageAttachments){
|
||||
if(att.blurhashPlaceholder==null){
|
||||
att.blurhashPlaceholder=new ColorDrawable(color);
|
||||
}
|
||||
}
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags&FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
statusForContent.sensitiveRevealed=false;
|
||||
statusForContent.sensitive=true;
|
||||
}else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
statusForContent.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
if((flags&FLAG_NO_MEDIA_PREVIEW)!=0){
|
||||
contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent));
|
||||
|
||||
}
|
||||
for(Attachment att : statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
if(att.type==Attachment.Type.UNKNOWN){
|
||||
contentItems.add(new FileStatusDisplayItem(parentID, fragment, att));
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
|
||||
}
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags&FLAG_NO_MEDIA_PREVIEW)==0));
|
||||
}
|
||||
if(statusForContent.quote!=null && (flags & FLAG_INSET)==0){
|
||||
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
|
||||
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER|FLAG_INSET|FLAG_NO_EMOJI_REACTIONS|FLAG_IS_FOR_QUOTE));
|
||||
} else if((flags & FLAG_INSET)==0 && statusForContent.mediaAttachments.isEmpty()){
|
||||
tryAddNonOfficialQuote(statusForContent, fragment, accountID, filterContext);
|
||||
}
|
||||
if(contentItems!=items && statusForContent.spoilerRevealed){
|
||||
items.addAll(contentItems);
|
||||
}
|
||||
AccountLocalPreferences lp=fragment.getLocalPrefs();
|
||||
if((flags&FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
|
||||
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
|
||||
statusForContent.reactions!=null){
|
||||
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
|
||||
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
|
||||
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
|
||||
}
|
||||
FooterStatusDisplayItem footer=null;
|
||||
if((flags&FLAG_NO_FOOTER)==0){
|
||||
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
|
||||
footer.hideCounts=hideCounts;
|
||||
items.add(footer);
|
||||
}
|
||||
boolean inset=(flags&FLAG_INSET)!=0;
|
||||
boolean isForQuote=(flags&FLAG_IS_FOR_QUOTE)!=0;
|
||||
// add inset dummy so last content item doesn't clip out of inset bounds
|
||||
if((inset || footer==null) && (flags&FLAG_CHECKABLE)==0 && !isForQuote){
|
||||
items.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
// in case we ever need the dummy to display a margin for the media grid again:
|
||||
// (i forgot why we apparently don't need this anymore)
|
||||
// !contentItems.isEmpty() && contentItems
|
||||
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
|
||||
}
|
||||
GapStatusDisplayItem gap=null;
|
||||
if((flags&FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment))
|
||||
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
|
||||
int i=1;
|
||||
for(StatusDisplayItem item : items){
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
@@ -373,15 +384,31 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
}
|
||||
if(items!=contentItems && !statusForContent.spoilerRevealed){
|
||||
for(StatusDisplayItem item : contentItems){
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
item.status=statusForContent;
|
||||
item.isForQuote=true;
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
}
|
||||
|
||||
List<StatusDisplayItem> nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items;
|
||||
WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null :
|
||||
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter);
|
||||
return applyingFilter==null ? items : new ArrayList<>(gap!=null
|
||||
? List.of(warning, gap)
|
||||
: Collections.singletonList(warning)
|
||||
);
|
||||
List<StatusDisplayItem> nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items;
|
||||
WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null :
|
||||
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter);
|
||||
if(warning!=null)
|
||||
warning.inset=inset;
|
||||
return applyingFilter==null ? items : new ArrayList<>(gap!=null
|
||||
? List.of(warning, gap)
|
||||
: Collections.singletonList(warning)
|
||||
);
|
||||
} catch(Exception e) {
|
||||
Log.e("StatusDisplayItem", "buildItems: failed to build StatusDisplayItem " + e);
|
||||
return new ArrayList<>(Collections.singletonList(new ErrorStatusDisplayItem(parentID, statusForContent, fragment, e)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> items){
|
||||
@@ -393,6 +420,60 @@ public abstract class StatusDisplayItem{
|
||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll, status));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to adds a non-official quote to a status.
|
||||
* A non-official quote is a quote on an instance that does not support quotes officially.
|
||||
*/
|
||||
private static void tryAddNonOfficialQuote(Status status, BaseStatusListFragment fragment, String accountID, FilterContext filterContext) {
|
||||
Matcher matcher=QUOTE_PATTERN.matcher(status.getStrippedText());
|
||||
|
||||
if(!matcher.find())
|
||||
return;
|
||||
String quoteURL=matcher.group();
|
||||
|
||||
if (!UiUtils.looksLikeFediverseUrl(quoteURL))
|
||||
return;
|
||||
|
||||
new GetSearchResults(quoteURL, GetSearchResults.Type.STATUSES, true, null, 0, 0).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults results){
|
||||
AccountSessionManager.get(accountID).filterStatuses(results.statuses, filterContext);
|
||||
if (results.statuses == null || results.statuses.isEmpty())
|
||||
return;
|
||||
|
||||
Status quote=results.statuses.get(0);
|
||||
new GetAccountRelationships(Collections.singletonList(quote.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> relationships){
|
||||
if(relationships.isEmpty())
|
||||
return;
|
||||
|
||||
Relationship relationship=relationships.get(0);
|
||||
String selfId=AccountSessionManager.get(accountID).self.id;
|
||||
if(!status.account.id.equals(selfId) && (relationship.domainBlocking || relationship.muting || relationship.blocking)) {
|
||||
// do not show posts that are quoting a muted/blocked user
|
||||
fragment.removeStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
status.quote=results.statuses.get(0);
|
||||
fragment.updateStatusWithQuote(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.w("StatusDisplayItem", "onError: failed to find quote status with URL: " + quoteURL + " " + error);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
public enum Type{
|
||||
HEADER,
|
||||
REBLOG_OR_REPLY_LINE,
|
||||
@@ -417,6 +498,7 @@ public abstract class StatusDisplayItem{
|
||||
SECTION_HEADER,
|
||||
HEADER_CHECKABLE,
|
||||
NOTIFICATION_HEADER,
|
||||
ERROR_ITEM,
|
||||
FILTER_SPOILER,
|
||||
DUMMY
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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(){}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,9 @@ public class HtmlParser{
|
||||
int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success);
|
||||
int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error);
|
||||
|
||||
if(source.endsWith("\n"))
|
||||
source=source.stripTrailing();
|
||||
|
||||
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
|
||||
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
|
||||
|
||||
@@ -137,14 +140,12 @@ public class HtmlParser{
|
||||
String href=el.attr("href");
|
||||
LinkSpan.Type linkType;
|
||||
String text=el.text();
|
||||
if(el.hasClass("hashtag")){
|
||||
if(text.startsWith("#")){
|
||||
linkType=LinkSpan.Type.HASHTAG;
|
||||
href=text.substring(1);
|
||||
linkObject=tagsByTag.get(text.substring(1).toLowerCase());
|
||||
}else{
|
||||
linkType=LinkSpan.Type.URL;
|
||||
}
|
||||
if(el.hasClass("hashtag") || text.startsWith("#")){
|
||||
// MOSHIDON: we have slightly refactored this so that the hashtags properly work in akkoma
|
||||
// TODO: upstream this
|
||||
linkType=LinkSpan.Type.HASHTAG;
|
||||
href=text.substring(1);
|
||||
linkObject=tagsByTag.get(text.substring(1).toLowerCase());
|
||||
}else if(el.hasClass("mention")){
|
||||
String id=idsByUrl.get(href);
|
||||
if(id!=null){
|
||||
@@ -321,12 +322,11 @@ public class HtmlParser{
|
||||
}
|
||||
|
||||
public static void applyFilterHighlights(Context context, SpannableStringBuilder text, List<FilterResult> filters){
|
||||
if (filters == null) return;
|
||||
int fgColor=UiUtils.getThemeColor(context, R.attr.colorM3Error);
|
||||
int bgColor=UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer);
|
||||
for(FilterResult filter:filters){
|
||||
if(!filter.filter.isActive())
|
||||
continue;;
|
||||
continue;
|
||||
for(String word:filter.keywordMatches){
|
||||
Matcher matcher=Pattern.compile("\\b"+Pattern.quote(word)+"\\b", Pattern.CASE_INSENSITIVE).matcher(text);
|
||||
while(matcher.find()){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
3
mastodon/src/main/res/drawable/ic_gnome_logo.xml
Normal file
3
mastodon/src/main/res/drawable/ic_gnome_logo.xml
Normal 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>
|
||||
76
mastodon/src/main/res/layout/display_item_error.xml
Normal file
76
mastodon/src/main/res/layout/display_item_error.xml
Normal 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>
|
||||
@@ -29,8 +29,7 @@
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:visibility="visible"
|
||||
/>
|
||||
android:visibility="visible"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
8
mastodon/src/main/res/menu/trending_links_timeline.xml
Normal file
8
mastodon/src/main/res/menu/trending_links_timeline.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
2
mastodon/src/main/res/values-bn/strings_sk.xml
Normal file
2
mastodon/src/main/res/values-bn/strings_sk.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -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>
|
||||
@@ -100,4 +100,25 @@
|
||||
<string name="mo_confirm_to_mute_hashtag">Sind Sie sicher, dass Sie diesen Hashtag stummschalten möchten\?</string>
|
||||
<string name="mo_filter_notifications">Benachrichtigungen filtern</string>
|
||||
<string name="mo_notification_filter_reset">Auf Standardwerte zurücksetzen</string>
|
||||
<string name="mo_instance_view_info">Serverinformationen anzeigen</string>
|
||||
<string name="mo_mute_notifications">Benachrichtigungen dieses Users verstecken?</string>
|
||||
<string name="mo_settings_remove_tracking_params">Private Links</string>
|
||||
<string name="import_settings_title">Einstellungen importieren</string>
|
||||
<string name="mo_error_display_title">Beitrag kann nicht angezeigt werden</string>
|
||||
<string name="mo_error_display_copy_error_details">Details kopieren</string>
|
||||
<string name="mo_trending_link_read">Lesen</string>
|
||||
<string name="import_settings_summary">Zuvor exportierte Einstellungen und Timelines importieren</string>
|
||||
<string name="mo_error_display_text">Beim Laden dieses Beitrags ist ein Fehler aufgetreten. Wenn das Problem weiterhin besteht, melde es bitte auf unserer Issues-Seite zusammen mit den Fehlerdetails.</string>
|
||||
<string name="mo_settings_remove_tracking_params_summary">Tracking-Informationen aus Links entfernen</string>
|
||||
<string name="export_settings_summary">Alle Einstellungen und Timelines der angemeldeten Konten exportieren</string>
|
||||
<string name="mo_settings_unifiedpush_warning">UnifiedPush nicht aktiviert</string>
|
||||
<string name="mo_settings_unifiedpush_warning_no_distributors">Keine UnifiedPush-Verteiler installiert. Du wirst keine Benachrichtigungen erhalten.</string>
|
||||
<string name="mo_settings_unifiedpush_warning_disabled">UnifiedPush ist nicht aktiviert. Du wirst keine Benachrichtigungen erhalten.</string>
|
||||
<string name="mo_settings_unifiedpush_enable">Aktivieren</string>
|
||||
<string name="import_settings_confirm">Import der Einstellungen bestätigen?</string>
|
||||
<string name="import_settings_confirm_body">Alle aktuellen Einstellungen und Timelines werden überschrieben! Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="import_settings_failed">Importieren der Einstellungen fehlgeschlagen</string>
|
||||
<string name="export_settings_share">Einstellungen exportieren</string>
|
||||
<string name="export_settings_fail">Exportieren der Einstellungen fehlgeschlagen</string>
|
||||
<string name="export_settings_title">Einstellungen exportieren</string>
|
||||
</resources>
|
||||
@@ -92,7 +92,7 @@
|
||||
<string name="sk_bookmark_as">Lesezeichen mit anderem Konto</string>
|
||||
<string name="sk_bookmarked_as">Lesezeichen gesetzt als %s</string>
|
||||
<string name="sk_already_bookmarked">Bereits in den Lesezeichen</string>
|
||||
<string name="sk_favorite_as">Favorit mit anderem Konto</string>
|
||||
<string name="sk_favorite_as">Favorisieren mit anderem Konto</string>
|
||||
<string name="sk_favorited_as">Favorisiert als %s</string>
|
||||
<string name="sk_already_favorited">Bereits favorisiert</string>
|
||||
<string name="sk_reblog_as">Mit einem anderen Konto teilen</string>
|
||||
@@ -183,7 +183,7 @@
|
||||
<string name="sk_icon_bot">Bot</string>
|
||||
<string name="sk_icon_language">Sprache</string>
|
||||
<string name="sk_icon_location">Standort</string>
|
||||
<string name="sk_icon_microphone">Mikrophon</string>
|
||||
<string name="sk_icon_microphone">Mikrofon</string>
|
||||
<string name="sk_icon_microscope">Mikroskop</string>
|
||||
<string name="sk_icon_keyboard">Keyboard</string>
|
||||
<string name="sk_icon_coffee">Kaffee</string>
|
||||
@@ -220,7 +220,7 @@
|
||||
<string name="sk_icon_gauge">Gage</string>
|
||||
<string name="sk_icon_headphones">Kopfhörer</string>
|
||||
<string name="sk_icon_human">Mensch</string>
|
||||
<string name="sk_icon_megaphone">Megaphon</string>
|
||||
<string name="sk_icon_megaphone">Megafon</string>
|
||||
<string name="sk_icon_light_bulb">Glühbirne</string>
|
||||
<string name="sk_icon_globe">Globus</string>
|
||||
<string name="sk_attach_file">Datei anhängen</string>
|
||||
@@ -246,7 +246,7 @@
|
||||
<string name="sk_reported">hat gemeldet</string>
|
||||
<string name="sk_sign_ups">Registrierungen</string>
|
||||
<string name="sk_new_reports">Neue Meldungen</string>
|
||||
<string name="sk_settings_see_new_posts_button">“Neue Beiträge anzeigen”-Button</string>
|
||||
<string name="sk_settings_see_new_posts_button">„Neue Beiträge anzeigen“-Button</string>
|
||||
<string name="sk_settings_server_version">Server-Version: %s</string>
|
||||
<string name="sk_notify_poll_results">Umfrage-Ergebnisse</string>
|
||||
<string name="sk_settings_prefix_reply_cw_with_re">Voranstellen von „re:“ an CWs in Antworten an</string>
|
||||
@@ -254,7 +254,7 @@
|
||||
<string name="sk_expand">Erweitern</string>
|
||||
<string name="sk_collapse">Einklappen</string>
|
||||
<string name="sk_settings_collapse_long_posts">Sehr lange Beiträge einklappen</string>
|
||||
<string name="sk_unfinished_attachments">Anhänge laden hoch</string>
|
||||
<string name="sk_unfinished_attachments">Anhänge werden hochgeladen</string>
|
||||
<string name="sk_settings_hide_interaction">Interaktions-Buttons verstecken</string>
|
||||
<string name="sk_unfinished_attachments_message">Einige Anhänge sind nicht fertig hochgeladen.</string>
|
||||
<string name="sk_followed_as">Mit %s gefolgt</string>
|
||||
@@ -269,7 +269,7 @@
|
||||
<string name="sk_notification_action_replied">Antwort an %s gesendet</string>
|
||||
<string name="sk_show_thread">Thread öffnen</string>
|
||||
<string name="sk_compact_reblog_reply_line">Kompakte Geteilt-/Geantwortet-Zeile</string>
|
||||
<string name="sk_reply_line_above_avatar">“Als Antwort auf”-Zeile über Profilbild</string>
|
||||
<string name="sk_reply_line_above_avatar">„Als Antwort auf“-Zeile über Profilbild</string>
|
||||
<string name="sk_settings_confirm_before_reblog">Vor dem Teilen bestätigen</string>
|
||||
<string name="sk_reacted">%s hat reagiert</string>
|
||||
<string name="sk_reacted_with">%1$s hat mit %2$s reagiert</string>
|
||||
@@ -290,10 +290,10 @@
|
||||
<string name="sk_settings_default_content_type">Standard-Inhaltstyp</string>
|
||||
<string name="sk_instance_info_unavailable">Informationen zur Instanz momentan nicht verfügbar</string>
|
||||
<string name="sk_open_in_app">In App öffnen</string>
|
||||
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string>
|
||||
<string name="sk_settings_content_types_explanation">Dadurch kann beim Erstellen von Beiträgen ein Inhaltstyp wie z.B. Markdown angegeben werden. Nicht alle Instanzen unterstützen das.</string>
|
||||
<string name="sk_settings_allow_remote_loading">Infos von Remote-Instanzen laden</string>
|
||||
<string name="sk_no_remote_info_hint">keine Remote-Infos abrufbar</string>
|
||||
<string name="sk_error_loading_profile">Konnte das Profil via %s nicht laden</string>
|
||||
<string name="sk_error_loading_profile">Konnte das Profil nicht via %s laden</string>
|
||||
<string name="sk_settings_allow_remote_loading_explanation">Für vollständigere Auflistung von Follower_innen, Likes und Boosts können die Informationen von der Ursprungs-Instanz geladen werden.</string>
|
||||
<string name="sk_settings_auto_reveal_equal_spoilers">Automatisches Aufdecken von gleichen CWs in Antworten von</string>
|
||||
<string name="sk_settings_auto_reveal_nobody">Nie</string>
|
||||
@@ -342,7 +342,7 @@
|
||||
<string name="sk_settings_continues_playback_summary">Musik im Hintergrund nicht pausieren, wenn in der App Medien abgespielt werden</string>
|
||||
<string name="sk_settings_unifiedpush_choose">Verteiler auswählen</string>
|
||||
<string name="sk_settings_display_pronouns_in_threads">Pronomen in Threads anzeigen</string>
|
||||
<string name="sk_settings_display_pronouns_in_user_listings">Pronomen in User-Auflistungen anzeigen</string>
|
||||
<string name="sk_settings_display_pronouns_in_user_listings">Pronomen in Nutzer-Auflistungen anzeigen</string>
|
||||
<string name="sk_tab_home">Start</string>
|
||||
<string name="sk_tab_search">Suche</string>
|
||||
<string name="sk_tab_notifications">Benachrichtigungen</string>
|
||||
@@ -423,7 +423,7 @@
|
||||
<string name="sk_icon_sun">Sonne</string>
|
||||
<string name="sk_icon_rain">Regen</string>
|
||||
<string name="sk_icon_thunderstorm">Gewitter</string>
|
||||
<string name="sk_private_note_update_failed">Notiz speichern fehlgeschlagen</string>
|
||||
<string name="sk_private_note_update_failed">Speichern der Notiz fehlgeschlagen</string>
|
||||
<string name="sk_delete_note">Private Notiz löschen</string>
|
||||
<string name="sk_confirm_changes">Änderungen bestätigen</string>
|
||||
<string name="sk_settings_crash_log_unavailable">Keines verfügbar… noch</string>
|
||||
@@ -433,5 +433,8 @@
|
||||
<string name="sk_open_post_preview">Vorschau öffnen</string>
|
||||
<string name="sk_post_preview">Vorschau</string>
|
||||
<string name="sk_private_note_confirm_delete">Private Notiz über %s löschen\?</string>
|
||||
<string name="sk_poll_multiple_choice">Mehrfachauswahl</string>
|
||||
<string name="sk_poll_multiple_choice">Mehrfachauswahl</string>
|
||||
<string name="sk_poll_hide_results">Ergebnisse ausblenden</string>
|
||||
<string name="sk_poll_show_results">Ergebnisse anzeigen</string>
|
||||
<string name="sk_posted">%s gepostet</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
<string name="mo_personal_note_confirm">Επιβεβαίωσε τις αλλαγές για σημείωση</string>
|
||||
<string name="mo_fab_compose">Σύνταξη</string>
|
||||
<string name="mo_instance_admin">Διαχειρίζεται από</string>
|
||||
<string name="mo_instance_contact">Επικοινωνία</string>
|
||||
<string name="mo_personal_note_update_failed">Αποτυχία αποθήκευσης σημείωσης</string>
|
||||
<string name="mo_settings_contribute">Συνείσφερε στο Moshidon</string>
|
||||
<string name="mo_update_available">Το Moshidon %s είναι έτοιμο για λήψη.</string>
|
||||
<string name="mo_update_ready">Το Moshidon %s έχει κατέβει και είναι έτοιμο για εγκατάσταση.</string>
|
||||
<string name="mo_no_image_desc_title">Χωρίς περιγραφή εικόνας</string>
|
||||
<string name="mo_no_image_desc">Οι εικόνες όυ περιλαμβάνονται δεν έχουν περιγραφή. Παρακαλώ σκέψου να προσθέσεις μία ώστε, να επιτρέψεις στα άτομα με προβλήματα όρασης να συμμετάσχουν.</string>
|
||||
<string name="mo_emoji_recent">Χρησιμοποιήθηκε πρόσφατα</string>
|
||||
<string name="mo_clear_recent_emoji">Εκκαθάριση πρόσφατων χρησιμοποιημένων emoji</string>
|
||||
<string name="mo_disable_relocate_publish_button_to_enable_customization">Απενεργοποίησε το Μετακίνηση κουμπιού δημοσίευσης για να επιτρέψεις την παραμετροποίηση</string>
|
||||
<string name="mo_severity_suspend">Αποκλεισμένοι</string>
|
||||
<string name="mo_open_camera">Λήψη φωτογραφίας</string>
|
||||
<string name="mo_confirm_unfollow">Επιβεβαίωση για άρση ακολούθησης %s</string>
|
||||
<string name="mo_haptic_feedback">Απτική ανάδραση</string>
|
||||
<string name="mo_relocate_publish_button">Μετακίνηση κουμπιού δημοσίευσης</string>
|
||||
<string name="mo_hide_compose_button_while_scrolling_setting">Απόκρυψη κουμπιού σύνταξης κατά τη κύλιση</string>
|
||||
<string name="mo_disable_reminder_to_add_alt_text">Απενεργοποίηση υπενθύμισης προσθήκης εναλλακτικού κειμένου</string>
|
||||
<string name="mo_sending_error">Σφάλμα δημοσίευσης</string>
|
||||
<string name="mo_color_palette_black_and_white">Άσπρο και Μαύρο</string>
|
||||
<string name="mo_personal_note">Πρόσθεσε μια σημείωση για αυτό το προφίλ</string>
|
||||
<string name="mo_filtered">Φιλτραρισμένα: %s</string>
|
||||
<string name="mo_welcome_text">Για να ξεκινήσεις, παρακαλώ εισήγαγε το όνομα τομέα της οντότητας σου παρακάτω.</string>
|
||||
<string name="mo_notification_management_settings">Διαχείριση Ειδοποιήσεων</string>
|
||||
<string name="mo_color_palette_nord">Nord</string>
|
||||
<string name="mo_camera_not_available">Δεν υπάρχει διαθέσιμη κάμερα!</string>
|
||||
<string name="mo_add_custom_server_local_timeline">Προσθήκη τοπικής ροής ενός προσαρμοσμένου διακομιστή</string>
|
||||
<string name="mo_muted_conversation_successfully">Έγινε επιτυχής σίγαση της συζήτησης</string>
|
||||
<string name="mo_unmuted_conversation_successfully">Καταργήθηκε επιτυχώς η σίγαση της συζήτησης</string>
|
||||
<string name="mo_confirm_to_unmute_conversation">Σίγουρα θε2 να καταργήσεις τη σίγαση αυτής της συζήτησης;</string>
|
||||
<string name="mo_notification_action_replied">Απάντηση με επιτυχία στην ανάρτηση του χρήστη %s</string>
|
||||
<string name="mo_duration_minutes_5">5 λεπτά</string>
|
||||
<string name="mo_duration_minutes_30">30 λεπτά</string>
|
||||
<string name="mo_duration_hours_1">1 ώρα</string>
|
||||
<string name="mo_duration_days_1">1 ημέρα</string>
|
||||
<string name="mo_duration_days_3">3 ημέρες</string>
|
||||
<string name="mo_change_default_reply_visibility_to_unlisted">Απαντήση ως «Εκτός λίστας» από προεπιλογή</string>
|
||||
<string name="mo_composer_behavior">Συμπεριφορά του Συντάκτη</string>
|
||||
<string name="mo_download_latest_nightly_release">Εγκατάσταση τελευταίας έκδοσης nightly</string>
|
||||
<string name="mo_mute_label">Διάρκεια:</string>
|
||||
<string name="mo_severity_silence">Σε σίγαση</string>
|
||||
<string name="mo_duration_hours_6">6 ώρες</string>
|
||||
<string name="mo_share_open_url">Άνοιγμα στην Εφαρμογή</string>
|
||||
<string name="mo_poll_option_add">Προσθήκη νέας επιλογής δημοσκόπησης</string>
|
||||
<string name="mo_duration_indefinite">Άπειρη</string>
|
||||
<string name="mo_duration_days_7">7 ημέρες</string>
|
||||
<string name="mo_setting_haptic_feedback_summary">Δόνηση κατά την αλληλεπίδραση με αναρτήσεις</string>
|
||||
<string name="mo_setting_true_black_summary">Ίσως εξοικονομήσει ενέργεια σε οθόνες AMOLED</string>
|
||||
<string name="mo_swap_bookmark_with_reblog">Εναλλαγή σελιδοδείκτη με ενέργεια αναδημοσίευσης</string>
|
||||
<string name="mo_show_media_preview">Εμφάνιση προεπισκόπησης πολυμέσων στις ροές</string>
|
||||
<string name="mo_settings_unifiedpush_warning">UnifiedPush μη ενεργό</string>
|
||||
<string name="mo_mention_reblogger_automatically">Αυτόματη επισήμανση λογαριασμού που αναδημοσίευσε την ανάρτηση, στις απαντήσεις</string>
|
||||
<string name="mo_confirm_unfollow_title">Άρση ακολούθησης Λογαριασμού</string>
|
||||
<string name="mo_settings_unifiedpush_warning_no_distributors">Δεν έχουν εγκατασταθεί διανομείς UnifiedPush. Δεν θα λάβεις ειδοποιήσεις.</string>
|
||||
<string name="mo_mute_notifications">Απόκρυψη ειδοποιήσεων από αυτόν τον χρήστη;</string>
|
||||
<string name="import_settings_confirm_body">Όλες οι τρέχουσες ρυθμίσεις και ροές θα επικαλυφθούν! Αυτή η ενέργεια δε μπορεί να αναιρεθεί.</string>
|
||||
<string name="import_settings_summary">Εισαγωγή προηγουμένως εξαγόμενων ρυθμίσεων και ροών</string>
|
||||
<string name="mo_enable_dividers">Εμφάνιση διαχωριστικών ανάρτησης</string>
|
||||
<string name="mo_setting_marquee_summary">Απενεργοποιεί την κύλιση τίτλων</string>
|
||||
<string name="mo_setting_uniform_summary">Χρήση του εικονιδίου εφαρμογής για όλες τις ειδοποιήσεις</string>
|
||||
<string name="mo_setting_reduced_motion_summary">Απενεργοποιεί τα εφέ κινήσεις για τις αλληλεπιδράσεις</string>
|
||||
<string name="mo_setting_play_gif_summary">Αυτόματη αναπαραγωγή GIF σε άβαταρ και emoji</string>
|
||||
<string name="mo_load_remote_followers">Φόρτωση ακόλουθων και ακολουθουμένων απομακρυσμένων προφίλ</string>
|
||||
<string name="mo_muting">Σίγαση…</string>
|
||||
<string name="mo_unmuting">Κατάργηση σίγασης…</string>
|
||||
<string name="mo_mute_conversation">Σίγαση συζήτησης</string>
|
||||
<string name="mo_unmute_conversation">Άρση σίγασης συζήτησης</string>
|
||||
<string name="mo_confirm_to_mute_conversation">Θες να κάνεις σίγαση αυτής της συζήτησης;</string>
|
||||
<string name="mo_swap_bookmark_with_reblog_summary">Σελιδοδείκτης ή αναδημοσίευση αναρτήσεων από τις ειδοποιήσεις</string>
|
||||
<string name="mo_settings_show_posts_without_alt_summary">Οι αναρτήσεις θα κρύβονται σε όλες τις ροές αλλά, μπορεί να αποκαλύπτονται στα νήματα και ειδοποιήσεις</string>
|
||||
<string name="mo_notification_audience_settings">Κοινό Ειδοποιήσεων</string>
|
||||
<string name="mo_mute_hashtag">Σίγαση ετικέτας</string>
|
||||
<string name="mo_unmute_hashtag">Άρση σίγασης ετικέτας</string>
|
||||
<string name="mo_confirm_to_mute_hashtag">Σίγουρα θες να κάνεις σίγαση αυτής της ετικέτας;</string>
|
||||
<string name="mo_confirm_to_unmute_hashtag">Θες σίγουρα να κάνεις άρση σίγασης αυτής της ετικέτας;</string>
|
||||
<string name="mo_instance_registration">Εγγραφή</string>
|
||||
<string name="mo_instance_registration_open">Άνοιγμα</string>
|
||||
<string name="mo_instance_registration_approval">Απαιτείται έγκριση</string>
|
||||
<string name="mo_instance_info_open_timeline">Τοπικό χρονολόγιο</string>
|
||||
<string name="mo_instance_info_moderated_servers">Συντονιζόμενοι διακομιστές</string>
|
||||
<string name="mo_setting_remote_follower_summary">Εμφάνιση ακόλουθων από άλλες οντότητες</string>
|
||||
<string name="mo_setting_relocate_publish_summary">Μετακίνηση του κουμπιού δημοσίευσης στη κάτω μπάρα</string>
|
||||
<string name="mo_setting_default_reply_privacy_summary">Οι απαντήσεις δεν θα περιλαμβάνονται στις δυνατότητες ανακάλυψης</string>
|
||||
<string name="mo_setting_interaction_count_summary">Εμφάνιση πόσα άτομα αλληλεπίδραση με μια ανάρτηση στη ροή</string>
|
||||
<string name="mo_setting_disable_swipe_summary">Σύρσιμο για αλλαγή της προσβαλλόμενης ροής</string>
|
||||
<string name="mo_settings_show_posts_without_alt">Εμφάνιση αναρτήσεων πολυμέσων χωρίς εναλλακτικό κείμενο</string>
|
||||
<string name="mo_double_tap_to_swipe_between_tabs">Διπλό άγγιγμα για κύλιση μεταξύ καρτελών</string>
|
||||
<string name="mo_filter_notifications">Φιλτράρισμα ειδοποιήσεων</string>
|
||||
<string name="mo_blocked_accounts">Αποκλεισμένοι λογαριασμοί</string>
|
||||
<string name="mo_recent_emoji_cleared">Έγινε εκκαθάριση πρόσφατων emoji</string>
|
||||
<string name="mo_notification_filter_reset">Επαναφορά στη προεπιλογή</string>
|
||||
<string name="mo_muted_accounts">Λογαριασμοί σε σίγαση</string>
|
||||
<string name="mo_double_tap_to_search">Διπλό άγγιγμα για άνοιγμα αναζήτησης</string>
|
||||
<string name="mo_instance_view_info">Δες Πληροφορίες Διακομιστή</string>
|
||||
<string name="mo_instance_users">Χρήστες</string>
|
||||
<string name="mo_instance_status">Κατάσταση</string>
|
||||
<string name="mo_settings_unifiedpush_warning_disabled">Το UnifiedPush είναι ενεργό. Δε θα λάβεις ειδοποιήσεις.</string>
|
||||
<string name="mo_settings_unifiedpush_enable">Ενεργοποίηση</string>
|
||||
<string name="import_settings_confirm">Επιβεβαίωση για εισαγωγή ρυθμίσεων;</string>
|
||||
<string name="import_settings_failed">Αδυναμία εισαγωγής αναρτήσεων</string>
|
||||
<string name="export_settings_share">Εξαγωγή Ρυθμίσεων</string>
|
||||
<string name="export_settings_fail">Αποτυχία εξαγωγής ρυθμίσεων</string>
|
||||
<string name="export_settings_title">Εξαγωγή ρυθμίσεων</string>
|
||||
<string name="export_settings_summary">Εξαγωγή όλων των ρυθμίσεων και ροών των συνδεδεμένων λογαριασμών</string>
|
||||
<string name="import_settings_title">Εισαγωγή ρυθμίσεων</string>
|
||||
<string name="mo_error_display_title">Αποτυχία εμφάνισης ανάρτησης</string>
|
||||
<string name="mo_error_display_text">Κάτι πήγε στραβά κατά τη φόρτωση της ανάρτησης. Αν το πρόβλημα παραμένει, παρακαλώ ανάφερε το στην σελίδα Issues μαζί με τις λεπτομέρειες του σφάλματος.</string>
|
||||
<string name="mo_error_display_copy_error_details">Αντιγραφή λεπτομερειών</string>
|
||||
<string name="mo_trending_link_read">Ανάγνωση</string>
|
||||
<string name="mo_settings_remove_tracking_params_summary">Απομάκρυνση πληροφοριών ανίχνευσης από συνδέσμους</string>
|
||||
<string name="mo_settings_remove_tracking_params">Ιδιωτικοί Σύνδεσμοι</string>
|
||||
</resources>
|
||||
@@ -1,3 +1,440 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
<string name="sk_timeline_home">Αρχική</string>
|
||||
<string name="sk_pinned_posts">Καρφιτσωμένα</string>
|
||||
<string name="sk_delete_and_redraft">Διαγραφή και επανασύνταξη</string>
|
||||
<string name="sk_confirm_delete_and_redraft">Θες σίγουρα να διαγράψεις και να επανασύνταξεις αυτή την ανάρτηση;</string>
|
||||
<string name="sk_pin_post">Καρφίτσωμα στο προφίλ</string>
|
||||
<string name="sk_confirm_pin_post">Θες να καρφιτσώσεις αυτήν την ανάρτηση στο προφίλ σου;</string>
|
||||
<string name="sk_pinning">Καρφίτσωμα ανάρτησης…</string>
|
||||
<string name="sk_settings_show_boosts">Εμφάνιση ενισχύσεων</string>
|
||||
<string name="sk_confirm_unpin_post_title">Ξεκαρφίτσωμα ανάρτησης από το προφίλ</string>
|
||||
<string name="sk_unpinning">Ξεκαρφίτσωμα ανάρτησης…</string>
|
||||
<string name="sk_image_description">Περιγραφή εικόνας</string>
|
||||
<string name="sk_visibility_unlisted">Εκτός λίστας</string>
|
||||
<string name="sk_settings_show_replies">Εμφάνιση απαντήσεων</string>
|
||||
<string name="sk_welcome_title">Καλώς ήρθες!</string>
|
||||
<string name="sk_welcome_text">Ο καρχαρίας σε χαιρετάει! Για να ξεκινήσεις, βάλε το όνομα τομέα της οντότητας σου, παρακάτω.</string>
|
||||
<string name="sk_translated_using">Μεταφράστηκε από %s</string>
|
||||
<string name="sk_post_language">Γλώσσα: %s</string>
|
||||
<string name="sk_available_languages">Διαθέσιμες γλώσσες</string>
|
||||
<string name="sk_language_name">%1$s (%2$s)</string>
|
||||
<string name="sk_trending_posts_info_banner">Αυτές οι αναρτήσεις λαμβάνουν προσοχή στο Fediverse.</string>
|
||||
<string name="sk_settings_always_reveal_content_warnings">Πάντα αποκάλυψη των προειδοποιήσεων περιεχομένου</string>
|
||||
<string name="sk_settings_enable_marquee">Ενεργοποίηση κυλιόμενου κειμένου στις γραμμές τίτλου</string>
|
||||
<string name="sk_notification_type_status">Αναρτήσεις</string>
|
||||
<string name="sk_notification_type_posts">Ειδοποιήσεις ανάρτησης</string>
|
||||
<string name="sk_color_palette_brown">Καφέ</string>
|
||||
<string name="sk_translate_show_original">Εμφάνιση αρχικού</string>
|
||||
<string name="sk_list_name_hint">Όνομα λίστας</string>
|
||||
<string name="sk_delete_list">Διαγραφή λίστας</string>
|
||||
<string name="sk_delete_list_confirm">Θες σίγουρα να διαγράψεις τη λίστα «%s»;</string>
|
||||
<string name="sk_edit_list_title">Επεξεργασία λίστας</string>
|
||||
<string name="sk_list_replies_policy_none">Κανένας</string>
|
||||
<string name="sk_timeline_federated">Ομοσπονδία</string>
|
||||
<string name="sk_settings_color_palette">Παλέτα χρωμάτων</string>
|
||||
<string name="sk_color_palette_material3">Σύστημα</string>
|
||||
<string name="sk_color_palette_pink">Ροζ</string>
|
||||
<string name="sk_color_palette_purple">Μοβ</string>
|
||||
<string name="sk_color_palette_green">Πράσινο</string>
|
||||
<string name="sk_delete_notification_confirm_action">Διαγραφή ειδοποίησης</string>
|
||||
<string name="sk_delete_notification">Διαγραφή ειδοποίησης</string>
|
||||
<string name="sk_delete_notification_confirm">Θες σίγουρα να διαγράψεις την ειδοποίηση;</string>
|
||||
<string name="sk_settings_translation_availability_note_available">Το %s υποστηρίζει μετάφραση!</string>
|
||||
<string name="sk_settings_translation_availability_note_unavailable">Το %s δεν φαίνεται να υποστηρίζει μετάφραση.</string>
|
||||
<string name="sk_clear_all_notifications_confirm_action">Διαγραφή όλων</string>
|
||||
<string name="sk_clear_all_notifications_confirm">Θες σίγουρα να διαγράψεις όλες τις ειδοποιήσεις;</string>
|
||||
<string name="sk_loading_fediverse_resource_title">Αναζήτηση για αυτό στο Fediverse</string>
|
||||
<string name="sk_undo_reblog">Αναίρεση ενίσχυσης</string>
|
||||
<string name="sk_reblog_with_visibility">Ενίσχυση με ορατότητα</string>
|
||||
<string name="sk_loading_resource_on_instance_title">Αναζήτηση για αυτό στο %s</string>
|
||||
<string name="sk_open_with_account">Άνοιγμα με άλλο λογαριασμό</string>
|
||||
<string name="sk_forward_report_to">Προώθηση σε %s</string>
|
||||
<string name="sk_confirm_save_draft">Αποθήκευση προχείρου;</string>
|
||||
<string name="sk_color_palette_blue">Μπλε</string>
|
||||
<string name="sk_settings_color_palette_default">Προεπιλογή (%s)</string>
|
||||
<string name="sk_example_domain">example.social</string>
|
||||
<string name="sk_settings_tabs_disable_swipe">Απενεργοποίηση κύλισης μεταξύ καρτελών</string>
|
||||
<string name="sk_settings_rules">Κανόνες</string>
|
||||
<string name="sk_settings_about">Σχετικά με την εφαρμογή</string>
|
||||
<string name="sk_settings_donate">Δωρεά</string>
|
||||
<string name="sk_settings_enable_delete_notifications">Ενεργοποίηση διαγραφής ειδοποιήσεων</string>
|
||||
<string name="sk_settings_publish_button_text">Κείμενο κουμπιού δημοσίευσης</string>
|
||||
<string name="sk_settings_publish_button_text_title">Παραμετροποίηση κειμένου κουμπιού Δημοσίευσης</string>
|
||||
<string name="sk_compose_no_schedule">Μην το προγραμματίσεις</string>
|
||||
<string name="sk_compose_no_draft">Μην το κάνεις πρόχειρο</string>
|
||||
<string name="sk_settings_reduce_motion">Μείωση κίνησης στα εφέ κίνησης</string>
|
||||
<string name="sk_timeline_bubble">Φυσαλίδα</string>
|
||||
<string name="sk_announcements">Ανακοινώσεις</string>
|
||||
<string name="sk_recent_searches_placeholder">Πάτα για να αρχίσεις την αναζήτηση</string>
|
||||
<string name="sk_remove_follower">Αφαίρεση απο ακόλουθο</string>
|
||||
<string name="sk_quote_post">Δημοσίευσε περί αυτού</string>
|
||||
<string name="sk_hashtags_you_follow">Ετικέτες που ακολουθείς</string>
|
||||
<string name="sk_resource_not_found">Η πηγή δε βρέθηκε</string>
|
||||
<string name="sk_bookmark_as">Σελιδοδείκτη με άλλο λογαριασμό</string>
|
||||
<string name="sk_bookmarked_as">Σε σελιδοδείκτη ως %s</string>
|
||||
<string name="sk_already_bookmarked">Ήδη με σελιδοδείκτη</string>
|
||||
<string name="sk_already_favorited">Ήδη αγαπημένο</string>
|
||||
<string name="sk_settings_uniform_icon_for_notifications">Ενιαίο εικονίδιο για όλες τις ειδοποιήσεις</string>
|
||||
<string name="sk_unsent_posts">Άρση αποστολής αναρτήσεων</string>
|
||||
<string name="sk_draft">Πρόχειρο</string>
|
||||
<string name="sk_confirm_delete_draft_title">Διαγραφή προχείρου</string>
|
||||
<string name="sk_confirm_delete_scheduled_post_title">Διαγραφή προγραμματισμένης ανάρτησης</string>
|
||||
<string name="sk_confirm_delete_scheduled_post">Θες σίγουρα να διαγράψεις αυτή την προγραμματισμένη ανάρτηση;</string>
|
||||
<string name="sk_draft_or_schedule">Πρόχειρο ή προγραμματισμός</string>
|
||||
<string name="sk_compose_draft">Η ανάρτηση θα αποθηκευτεί ως πρόχειρη.</string>
|
||||
<string name="sk_compose_scheduled">Προγραμματίστηκε για</string>
|
||||
<string name="sk_draft_saved">Το πρόχειρο αποθηκεύτηκε</string>
|
||||
<string name="sk_post_scheduled">Η ανάρτηση προγραμματίστηκε</string>
|
||||
<string name="sk_scheduled_too_soon_title">Η στιγμή προγραμματισμού είναι πολύ κοντινή</string>
|
||||
<string name="sk_mark_as_read">Επισήμανση ως αναγνωσμένο</string>
|
||||
<string name="sk_settings_single_notification">Εμφάνιση μόνο μια ειδοποίησης</string>
|
||||
<string name="sk_settings_unifiedpush">Χρήση UnifiedPush</string>
|
||||
<string name="sk_settings_unifiedpush_choose">Διάλεξε έναν διανομέα</string>
|
||||
<string name="sk_settings_unifiedpush_no_distributor">Δε βρέθηκε διανομέας</string>
|
||||
<string name="sk_create">Δημιουργία</string>
|
||||
<string name="sk_create_list_title">Δημιουργία λίστας</string>
|
||||
<string name="sk_list_replies_policy_list">Λίστα μελών</string>
|
||||
<string name="sk_list_replies_policy_followed">Χρήστες που ακολουθείς</string>
|
||||
<string name="sk_quoting_user">Παρατίθεται ο χρήστης %s</string>
|
||||
<string name="sk_update_available">Το Megalodon %s είναι έτοιμο για λήψη.</string>
|
||||
<string name="sk_update_ready">Το Megalodon %s έχει ληφθεί και είναι έτοιμο για εγκατάσταση.</string>
|
||||
<string name="sk_check_for_update">Έλεγχος για ενημερώσεις</string>
|
||||
<string name="sk_no_update_available">Καμία ενημέρωση</string>
|
||||
<string name="sk_list_timelines">Λίστες</string>
|
||||
<string name="sk_follow_requests">Αιτήματα ακολούθησης</string>
|
||||
<string name="sk_reject_follow_request">Απόρριψη αιτήματος ακολούθησης</string>
|
||||
<string name="sk_settings_reply_visibility">Ορατότητα απάντησης</string>
|
||||
<string name="sk_settings_reply_visibility_following">Απαντήσεις στους ακόλουθους μου</string>
|
||||
<string name="sk_settings_reply_visibility_self">Απαντήσεις σε μένα</string>
|
||||
<string name="sk_settings_show_interaction_counts">Εμφάνιση αριθμού αλληλεπιδράσεων</string>
|
||||
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
|
||||
<string name="sk_mark_media_as_sensitive">Επισήμανση πολυμέσων ως ευαίσθητα</string>
|
||||
<string name="sk_user_post_notifications_off">Οι ειδοποιήσεις ανάρτησης για τον χρήστη %s είναι άνεργες</string>
|
||||
<string name="sk_federated_timeline">Ομοσπονδία</string>
|
||||
<string name="sk_favorited_as">Ορίστηκε αγαπημένο ως %s</string>
|
||||
<string name="sk_reblogged_as">Ενισχύθηκε ως %s</string>
|
||||
<string name="sk_settings_posting">Προτιμήσεις ανάρτησης</string>
|
||||
<string name="sk_settings_filters">Ρύθμιση φίλτρων</string>
|
||||
<string name="sk_settings_auth">Ρύθμισης ασφαλείας</string>
|
||||
<string name="sk_lists_with_user">Λίστες με τον χρήστη %s</string>
|
||||
<string name="sk_settings_contribute">Συνεισφορά στο Megalodon</string>
|
||||
<string name="sk_color_palette_yellow">Κίτρινο</string>
|
||||
<string name="sk_settings_profile">Ρύθμιση προφίλ</string>
|
||||
<string name="sk_confirm_delete_and_redraft_title">Διαγραφή και επανασύνταξη Ανάρτησης</string>
|
||||
<string name="sk_unpin_post">Ξεκαρφίτσωμα από το προφίλ</string>
|
||||
<string name="sk_confirm_unpin_post">Θες σίγουρα να ξεκαρφιτσώσεις αυτή την ανάρτηση;</string>
|
||||
<string name="sk_confirm_pin_post_title">Καρφίτσωμα ανάρτησης στο προφίλ</string>
|
||||
<string name="sk_federated_timeline_info_banner">Αυτές είναι οι πιο πρόσφατες αναρτήσεις από άτομα σε ομοσπονδία με σένα.</string>
|
||||
<string name="sk_bubble_timeline_info_banner">Αυτές είναι οι πιο πρόσφατες αναρτήσεις από το δίκτυο που επιμελήθηκαν οι διαχειριστές της οντότητας σου.</string>
|
||||
<string name="sk_settings_show_federated_timeline">Εμφάνιση ροής ομοσπονδίας</string>
|
||||
<string name="sk_color_palette_red">Κόκκινο</string>
|
||||
<string name="sk_translate_post">Μετάφραση</string>
|
||||
<string name="sk_confirm_clear_recent_languages">Θες σίγουρα να διαγράψεις τις γλώσσες που χρησιμοποίησες πρόσφατα;</string>
|
||||
<string name="sk_favorite_as">Αγαπημένο με άλλο λογαριασμό</string>
|
||||
<string name="sk_already_reblogged">Ήδη ενισχυμένο</string>
|
||||
<string name="sk_reply_as">Απάντηση με άλλο λογαριασμό</string>
|
||||
<string name="sk_confirm_delete_draft">Θες σίγουρα να διαγράψεις αυτή την πρόχειρη ανάρτηση;</string>
|
||||
<string name="sk_schedule_post">Προγραμματισμός ανάρτησης</string>
|
||||
<string name="sk_settings_continues_playback">Επικάλυψη ήχου</string>
|
||||
<string name="sk_trending_links_info_banner">Αυτές οι ειδήσεις συζητούνται σε όλο το Fediverse.</string>
|
||||
<string name="sk_clear_recent_languages">Εκκαθάριση πρόσφατα χρησιμοποιημένων γλωσσών</string>
|
||||
<string name="sk_schedule">Προγραμματισμός</string>
|
||||
<string name="sk_schedule_or_draft">Προγραμματισμός ή πρόχειρο</string>
|
||||
<string name="sk_settings_unifiedpush_no_distributor_body">Πρέπει να εγκαταστήσεις έναν διανομέα για να δουλέψει το UnifiedPush. Για περισσότερες πληροφορίες, πήγαινε στο https://unifiedpush.org/</string>
|
||||
<string name="sk_settings_continues_playback_summary">Επέτρεψε την αναπαραγωγή των ήδη αναπαραγωμένων πολυμέσων, επικαλύπτοντας τη νέα αναπαραγωγή</string>
|
||||
<string name="sk_settings_reply_visibility_all">Όλες οι απαντήσεις</string>
|
||||
<string name="sk_your_lists">Οι λίστες σου</string>
|
||||
<string name="sk_timeline_local">Τοπική</string>
|
||||
<string name="sk_copy_link_to_post">Αντιγραφή συνδέσμου ανάρτησης</string>
|
||||
<string name="sk_reblog_as">Ενίσχυση με άλλο λογαριασμό</string>
|
||||
<string name="sk_settings_load_new_posts">Αυτόματη φόρτωση νέων αναρτήσεων</string>
|
||||
<string name="sk_user_post_notifications_on">Οι ειδοποιήσεις ανάρτησης για τον χρήστη %s είναι ενεργές</string>
|
||||
<string name="sk_accept_follow_request">Αποδοχή αιτήματος ακολούθησης</string>
|
||||
<string name="sk_clear_all_notifications">Εκκαθάριση όλων των ειδοποιήσεων</string>
|
||||
<string name="sk_settings_translate_only_opened">Μετάφραση μόνο ανοιγμένων αναρτήσεων</string>
|
||||
<string name="sk_scheduled_too_soon">Η ανάρτηση πρέπει να προγραμματίζεται τουλάχιστον 10 λεπτά από τώρα.</string>
|
||||
<string name="sk_confirm_save_changes">Αποθήκευση αλλαγών;</string>
|
||||
<string name="sk_mark_as_draft">Επισήμανση ως πρόχειρο</string>
|
||||
<string name="sk_settings_about_instance">Σχετικά με την οντότητα</string>
|
||||
<string name="sk_list_replies_policy">Εμφάνιση απαντήσεων σε</string>
|
||||
<string name="sk_app_name">Megalodon</string>
|
||||
<string name="sk_settings_auto_reveal_nobody">Ποτέ</string>
|
||||
<string name="sk_settings_auto_reveal_author">Απαντήσεις από τον ίδιο συντάκτη</string>
|
||||
<string name="sk_alt_text_missing_title">Λείπει εναλλακτικό κείμενο</string>
|
||||
<string name="sk_icon_microphone">Μικρόφωνο</string>
|
||||
<string name="sk_icon_keyboard">Αρμόνιο</string>
|
||||
<string name="sk_icon_coffee">Καφές</string>
|
||||
<string name="sk_icon_microscope">Μικροσκόπιο</string>
|
||||
<string name="sk_icon_clapper_board">Κλακέτα</string>
|
||||
<string name="sk_icon_sport">Άθλημα</string>
|
||||
<string name="sk_icon_aperture">Διάφραγμα</string>
|
||||
<string name="sk_icon_train">Τρένο</string>
|
||||
<string name="sk_icon_leaves">Φύλλα</string>
|
||||
<string name="sk_icon_gavel">Προεδρική σφύρα</string>
|
||||
<string name="sk_icon_human">Άνθρωπος</string>
|
||||
<string name="sk_icon_globe">Υδρόγειος</string>
|
||||
<string name="sk_icon_headphones">Ακουστικά</string>
|
||||
<string name="sk_post_edited">επεξεργάστηκε</string>
|
||||
<string name="sk_attach_file">Επισύναψη αρχείου</string>
|
||||
<string name="sk_searching">Αναζητείται…</string>
|
||||
<string name="sk_settings_glitch_mode_explanation">Ενεργοποίησέ το αν η οικιακή σου οντότητα τρέχει Glitch. Δεν απαιτείται για Hometown ή Akkoma.</string>
|
||||
<string name="sk_content_type_plain">Σκέτο κείμενο</string>
|
||||
<string name="sk_no_remote_info_hint">Απομακρυσμένες πληροφορίες μη διαθέσιμες</string>
|
||||
<string name="sk_error_loading_profile">Αποτυχία φόρτωσης προφίλ μέσω %s</string>
|
||||
<string name="sk_timelines">Ροές</string>
|
||||
<string name="sk_icon_bug">Έντομο</string>
|
||||
<string name="sk_icon_pizza">Πίτσα</string>
|
||||
<string name="sk_edit_timelines">Επεξεργασία ροών</string>
|
||||
<string name="sk_filtered">Φιλτραρισμένο: %s</string>
|
||||
<string name="sk_external_share_or_open_title">Κοινοποίηση ή άνοιγμα με λογαριασμό</string>
|
||||
<string name="sk_icon_beaker">Ποτήρι ζέσης</string>
|
||||
<string name="sk_icon_bed">Κρεβάτι</string>
|
||||
<string name="sk_notification_type_update">Επεξεργασμένες αναρτήσεις</string>
|
||||
<string name="sk_external_share_title">Κοινοποίηση με λογαριασμό</string>
|
||||
<string name="sk_content_type">Τύπος περιεχομένου</string>
|
||||
<string name="sk_in_reply">Σε απάντηση</string>
|
||||
<string name="sk_remove_follower_success">Επιτυχής αφαίρεση ακολούθου</string>
|
||||
<string name="sk_changelog">Λίστα αλλαγών</string>
|
||||
<string name="sk_remove_follower_confirm">Να καταργηθεί ο χρήστης %s ως ακόλουθος αποκλείοντάς τον και κάνοντας άρση αμέσως;</string>
|
||||
<string name="sk_do_remove_follower">Αφαίρεση</string>
|
||||
<string name="sk_icon_city">Πόλη</string>
|
||||
<string name="sk_icon_cat">Γάτα</string>
|
||||
<string name="sk_icon_pin">Καρφίτσωμα</string>
|
||||
<string name="sk_alt_text_missing">Τουλάχιστον ένα συνημμένο δεν έχει περιγραφή.</string>
|
||||
<string name="sk_publish_anyway">Δημοσίευση ούτως ή άλλως</string>
|
||||
<string name="sk_timeline_posts">Αναρτήσεις</string>
|
||||
<string name="sk_timelines_add">Προσθήκη</string>
|
||||
<string name="sk_timeline">Ροή</string>
|
||||
<string name="sk_list">Λίστα</string>
|
||||
<string name="sk_hashtag">Ετικέτα</string>
|
||||
<string name="sk_pin_timeline">Καρφίτσωμα ροής</string>
|
||||
<string name="sk_pinned_timeline">Καρφιτσώθηκε στην αρχική</string>
|
||||
<string name="sk_unpin_timeline">Ξεκαρφίτσωμα ροής</string>
|
||||
<string name="sk_unpinned_timeline">Ξεκαρφιτσώθηκε απ\' την αρχική</string>
|
||||
<string name="sk_icon_dog">Σκύλος</string>
|
||||
<string name="sk_icon_turtle">Χελώνα</string>
|
||||
<string name="sk_icon_balloon">Μπαλόνι</string>
|
||||
<string name="sk_icon_image">Εικόνα</string>
|
||||
<string name="sk_icon_bot">Ρομπότ</string>
|
||||
<string name="sk_icon_language">Γλώσσα</string>
|
||||
<string name="sk_icon_location">Τοποθεσία</string>
|
||||
<string name="sk_icon_megaphone">Μεγάφωνο</string>
|
||||
<string name="sk_icon_laugh">Γέλιο</string>
|
||||
<string name="sk_icon_pi">Πι</string>
|
||||
<string name="sk_icon_color_palette">Παλέτα χρωμάτων</string>
|
||||
<string name="sk_icon_academic_cap">Ακαδημαϊκό καπέλο</string>
|
||||
<string name="sk_icon_tag">Ετικέτα</string>
|
||||
<string name="sk_icon_stethoscope">Στηθοσκόπιο</string>
|
||||
<string name="sk_icon_weather">Καιρός</string>
|
||||
<string name="sk_icon_games">Παιχνίδια</string>
|
||||
<string name="sk_icon_code">Κώδικας</string>
|
||||
<string name="sk_icon_light_bulb">Λάμπα</string>
|
||||
<string name="sk_icon_music">Μουσική</string>
|
||||
<string name="sk_icon_people">Άνθρωποι</string>
|
||||
<string name="sk_icon_health">Υγεία</string>
|
||||
<string name="sk_icon_chat">Συνομιλία</string>
|
||||
<string name="sk_icon_shield">Ασπίδα</string>
|
||||
<string name="sk_icon_book">Βιβλίο</string>
|
||||
<string name="sk_icon_bicycle">Ποδήλατο</string>
|
||||
<string name="sk_icon_map">Χάρτης</string>
|
||||
<string name="sk_icon_backpack">Σακίδιο</string>
|
||||
<string name="sk_icon_briefcase">Χαρτοφύλακας</string>
|
||||
<string name="sk_icon_fire">Φωτιά</string>
|
||||
<string name="sk_icon_feed">Ροή</string>
|
||||
<string name="sk_icon_recycle_bin">Κάδος ανακύκλωσης</string>
|
||||
<string name="sk_settings_server_version">Έκδοση διακομιστή: %s</string>
|
||||
<string name="sk_icon_diamond">Διαμάντι</string>
|
||||
<string name="sk_icon_umbrella">Ομπρέλα</string>
|
||||
<string name="sk_add_timeline_tag_error_empty">Η ετικέτα δε μπορεί να είναι κενή</string>
|
||||
<string name="sk_updater_enable_pre_releases">Ενεργοποίηση προ-κυκολοφορίας</string>
|
||||
<string name="sk_inline_local_only">Μόνο τοπικά</string>
|
||||
<string name="sk_inline_direct">Μόνο με επισήμανση</string>
|
||||
<string name="sk_separator">·</string>
|
||||
<string name="sk_local_only">Μόνο τοπική οντότητα</string>
|
||||
<string name="sk_instance_features">Δυνατότητες οντότητας</string>
|
||||
<string name="sk_settings_glitch_instance">Λειτουργία μόνο-τοπική του Glitch</string>
|
||||
<string name="sk_signed_up">έγινε εγγραφή</string>
|
||||
<string name="sk_reported">αναφέρθηκε</string>
|
||||
<string name="sk_sign_ups">Χρήστες που εγγράφονται</string>
|
||||
<string name="sk_new_reports">Νέες αναφορές</string>
|
||||
<string name="sk_settings_prefix_reply_cw_with_re">Πρόθεμα ΠΠ με \"re:\" κατά την απάντηση</string>
|
||||
<string name="sk_settings_hide_fab">Αυτόματη απόκρυψη κουμπιού Σύνταξης</string>
|
||||
<string name="sk_notification_action_replied">Εστάλη απάντηση στον χρήστη %s</string>
|
||||
<string name="sk_reply_line_above_avatar">Η γραμμή \"σε απάντηση σε\" πάνω από το άβαταρ</string>
|
||||
<string name="sk_show_thread">Εμφάνιση νήματος</string>
|
||||
<string name="sk_content_type_unspecified">Απροσδιόριστος</string>
|
||||
<string name="sk_content_type_html">HTML</string>
|
||||
<string name="sk_content_type_markdown">Markdown</string>
|
||||
<string name="sk_content_type_bbcode">BBCode</string>
|
||||
<string name="sk_content_type_mfm">MFM</string>
|
||||
<string name="sk_settings_content_types">Ενεργοποίηση μορφοποίησης ανάρτησης</string>
|
||||
<string name="sk_settings_default_content_type">Προεπιλεγμένος τύπος περιεχομένου</string>
|
||||
<string name="sk_instance_info_unavailable">Πληροφορίες οντότητας προσωρινά μη διαθέσιμες</string>
|
||||
<string name="sk_open_in_app">Άνοιγμα στην εφαρμογή</string>
|
||||
<string name="sk_open_in_app_failed">Αδυναμία ανοίγματος στην εφαρμογή</string>
|
||||
<string name="sk_settings_allow_remote_loading">Φόρτωση πληροφοριών απομακρυσμένων οντοτήτων</string>
|
||||
<string name="sk_settings_auto_reveal_anyone">Απαντήσεις απ\' όλους</string>
|
||||
<string name="sk_settings_prefix_replies_always">Σε απάντηση προς όλους</string>
|
||||
<string name="sk_settings_prefix_replies_never">Ποτέ</string>
|
||||
<string name="sk_exclusive_list">Αποκλειστική λίστα</string>
|
||||
<string name="sk_list_exclusive_switch">Κάνε την λίστα αποκλειστική</string>
|
||||
<string name="sk_advanced_options_show">Εμφάνιση προχωρημένων επιλογών</string>
|
||||
<string name="sk_advanced_options_hide">Απόκρυψη προχωρημένων επιλογών</string>
|
||||
<string name="sk_spoiler_show">Εμφάνιση περιεχομένου</string>
|
||||
<string name="sk_pronouns_label">Αντωνυμίες</string>
|
||||
<string name="sk_switch_timeline">Εναλλαγή ροής</string>
|
||||
<string name="sk_settings_instance">Οντότητα</string>
|
||||
<string name="sk_settings_true_black">Απόλυτα μαύρη λειτουργία</string>
|
||||
<string name="sk_settings_display_pronouns_in_timelines">Εμφάνιση αντωνυμιών στις ροές</string>
|
||||
<string name="sk_settings_display_pronouns_in_threads">Εμφάνιση αντωνυμιών στα νήματα</string>
|
||||
<string name="sk_settings_hide_interaction">Απόκρυψη κουμπιών αλληλεπίδρασης</string>
|
||||
<string name="sk_followed_as">Ακολουθείται από %s</string>
|
||||
<string name="sk_timeline_icon">Εικονίδιο</string>
|
||||
<string name="sk_icon_heart">Καρδιά</string>
|
||||
<string name="sk_icon_star">Αστέρι</string>
|
||||
<string name="sk_remove">Αφαίρεση</string>
|
||||
<string name="sk_icon_rabbit">Κουνέλι</string>
|
||||
<string name="sk_reacted_with">Ο χρήστης %1$s αντέδρασε με %2$s</string>
|
||||
<string name="sk_reacted">Ο χρήστης %s αντέδρασε</string>
|
||||
<string name="sk_expand">Επέκταση</string>
|
||||
<string name="sk_collapse">Σύμπτυξη</string>
|
||||
<string name="sk_settings_collapse_long_posts">Σύμπτυξη πολύ μεγάλων αναρτήσεων</string>
|
||||
<string name="sk_unfinished_attachments">Μεταμορφώνονται συνημμένα</string>
|
||||
<string name="sk_icon_sun">Ήλιος</string>
|
||||
<string name="sk_icon_sunset">Ηλιοβασίλεμα</string>
|
||||
<string name="sk_icon_thunderstorm">Καταιγίδα</string>
|
||||
<string name="sk_icon_water">Νερό</string>
|
||||
<string name="sk_edit_timeline">Επεξεργασία ροής</string>
|
||||
<string name="sk_icon_cloud">Σύννεφο</string>
|
||||
<string name="sk_add_timeline">Προσθήκη ροής</string>
|
||||
<string name="sk_edit_timeline_tag_main">Αναρτήσεις που περιέχουν την ετικέτα…</string>
|
||||
<string name="sk_edit_timeline_tag_any">...ή οποιοδήποτε απ\' αυτές</string>
|
||||
<string name="sk_edit_timeline_tag_all">...και όλες αυτές</string>
|
||||
<string name="sk_edit_timeline_tag_none">... αλλά καμία απ\' αυτές</string>
|
||||
<string name="sk_edit_timeline_tag_hint">Εισήγαγε ετικέτα…</string>
|
||||
<string name="sk_edit_timeline_tags_hint">Εισήγαγε ετικέτες…</string>
|
||||
<string name="sk_no_results">Χωρίς αποτέλεσμα</string>
|
||||
<string name="sk_save_draft">Αποθήκευση προχείρου;</string>
|
||||
<string name="sk_save_draft_message">Θες να αποθηκεύσεις τις αλλαγές σ\' αυτό το πρόχειρο ή θα δημοσιεύσεις τώρα;</string>
|
||||
<string name="sk_settings_show_alt_indicator">Ένδειξη για εναλλακτικά κείμενα</string>
|
||||
<string name="sk_settings_show_no_alt_indicator">Ένδειξη για έλλειψη εναλλακτικών κειμένων</string>
|
||||
<string name="sk_alt_button">ALT</string>
|
||||
<string name="sk_gif_badge">GIF</string>
|
||||
<string name="sk_search_fediverse">Αναζήτηση στο Fediverse</string>
|
||||
<string name="sk_icon_math_formula">Μαθηματική φόρμουλα</string>
|
||||
<string name="sk_icon_snowflake">Χιονονιφάδα</string>
|
||||
<string name="sk_no_alt_text">Χωρίς διαθέσιμο εναλλακτικό κείμενο</string>
|
||||
<string name="sk_settings_support_local_only">Ο διακομιστής υποστηρίζει μόνο-τοπικές αναρτήσεις</string>
|
||||
<string name="sk_settings_disable_alt_text_reminder">Απενεργοποίηση υπενθύμισης για εναλλακτικό κείμενο</string>
|
||||
<string name="sk_notify_posts_info_banner">Αν ενεργοποιήσεις τις ειδοποιήσεις ανάρτησης για κάποια άτομα, οι νέες αναρτήσεις τους θα εμφανιστούν εδώ.</string>
|
||||
<string name="sk_icon_news">Ειδήσεις</string>
|
||||
<string name="sk_edit_timeline_tags_explanation">Λάβε υπόψη ότι ο διακομιστής χειρίζεται αυτές τις λειτουργίες. Ο συνδυασμός τους ενδέχεται να μην υποστηρίζεται.</string>
|
||||
<string name="sk_icon_important">Σημαντικό</string>
|
||||
<string name="sk_icon_gauge">Μετρητής</string>
|
||||
<string name="sk_icon_verified">Επαληθεύτηκε</string>
|
||||
<string name="sk_icon_doctor">Γιατρός</string>
|
||||
<string name="sk_hashtag_timeline_local_only_switch">Εμφάνιση μόνο τοπικών αναρτήσεων;</string>
|
||||
<string name="sk_settings_see_new_posts_button">Κουμπί \"Δες νέες αναρτήσεις\"</string>
|
||||
<string name="sk_settings_local_only_explanation">Η οικιακή σου οντότητα πρέπει να υποστηρίζει μόνο-τοπική ανάρτηση για να λειτουργήσει. Οι περισσότερες τροποποιημένες εκδόσεις του Mastodon το κάνουν, αλλά το Mastodon όχι.</string>
|
||||
<string name="sk_notify_poll_results">Αποτελέσματα δημοσκόπησης</string>
|
||||
<string name="sk_icon_rain">Βροχή</string>
|
||||
<string name="sk_notify_update">Επεξεργάζεται μια ενισχυμένη ανάρτηση</string>
|
||||
<string name="sk_follow_as">Ακολούθησε από άλλον λογαριασμό</string>
|
||||
<string name="sk_compact_reblog_reply_line">Συμπαγής γραμμή ενίσχυσης/απάντησης</string>
|
||||
<string name="sk_settings_confirm_before_reblog">Επιβεβαίωση πριν την ενίσχυση</string>
|
||||
<string name="sk_list_exclusive_switch_explanation">Μέλη της αποκλειστικής λίστας δε θα εμφανίζονται στην αρχική σου ροή - αν το υποστηρίζει η οντότητά σου.</string>
|
||||
<string name="sk_unfinished_attachments_message">Κάποια απ\' τα συνημμένα δεν έχουν μεταμορφωθεί πλήρως.</string>
|
||||
<string name="sk_settings_allow_remote_loading_explanation">Προσπάθεια ανάκτησης ακριβέστερων καταχωρίσεων για ακόλουθους, \"μού αρέσει\" και ενισχύσεις φορτώνοντας τις πληροφορίες από την οντότητα προέλευσης.</string>
|
||||
<string name="sk_disable_pill_shaped_active_indicator">Απενεργοποίηση ένδειξης μορφής-χαπιού ενεργής καρτέλας</string>
|
||||
<string name="sk_settings_default_content_type_explanation">Αυτό σου επιτρέπει να προεπιλέξεις έναν τύπο περιεχομένου κατά τη δημιουργία νέων αναρτήσεων, παρακάμπτοντας την τιμή που έχει οριστεί στις \"Προτιμήσεις ανάρτηση ς\".</string>
|
||||
<string name="sk_settings_content_types_explanation">Επιτρέπει τη ρύθμιση ενός τύπου περιεχομένου όπως το Markdown κατά τη δημιουργία μιας ανάρτησης. Λάβε υπόψη ότι δεν το υποστηρίζουν όλες οι οντότητες.</string>
|
||||
<string name="sk_settings_like_icon">Χρήση καρδιάς ως εικονίδιο για αγαπημένα</string>
|
||||
<string name="sk_recently_used">Χρησιμοποιήθηκε πρόσφατα</string>
|
||||
<string name="sk_load_missing_posts_above">Φόρτωση νεότερων αναρτήσεων</string>
|
||||
<string name="sk_load_missing_posts_below">Φόρτωση παλαιότερων αναρτήσεων</string>
|
||||
<string name="sk_settings_auto_reveal_equal_spoilers">Αυτόματη εμφάνιση ίσων ΠΠ στις απαντήσεις</string>
|
||||
<string name="sk_mute_label">Διάρκεια</string>
|
||||
<string name="sk_duration_days_7">7 ημέρες</string>
|
||||
<string name="sk_settings_prefix_replies_to_others">Μόνο προς απάντηση σε άλλους</string>
|
||||
<string name="sk_settings_forward_report_default">Προεπιογή διακόπτη \"Προώθηση αναφοράς\"</string>
|
||||
<string name="sk_tab_home">Αρχική</string>
|
||||
<string name="sk_tab_search">Αναζήτηση</string>
|
||||
<string name="sk_tab_profile">Προφίλ</string>
|
||||
<string name="sk_settings_show_emoji_reactions_always">Εμφάνιση του κουμπιού προσθήκη πάντα</string>
|
||||
<plurals name="sk_users_reacted_with">
|
||||
<item quantity="one">Ένας χρήστης αντέδρασε με %2$s</item>
|
||||
<item quantity="other">%1$,d χρήστες αντέδρασαν με %2$s</item>
|
||||
</plurals>
|
||||
<string name="sk_button_react">Αντέδρασε με εμότζι</string>
|
||||
<string name="sk_duration_indefinite">Άπειρη</string>
|
||||
<string name="sk_duration_minutes_5">5 λεπτά</string>
|
||||
<string name="sk_duration_minutes_30">30 λεπτά</string>
|
||||
<string name="sk_duration_hours_1">1 ώρα</string>
|
||||
<string name="sk_duration_hours_6">6 ώρες</string>
|
||||
<string name="sk_duration_days_1">1 ημέρα</string>
|
||||
<string name="sk_duration_days_3">3 ημέρες</string>
|
||||
<string name="sk_notification_mention">Επισημάνθηκες από τον χρήστη %s</string>
|
||||
<string name="sk_suicide_search_terms">Αυτοκτονία</string>
|
||||
<string name="sk_search_suicide_title">Σε περίπτωση που είσαι σε κρίση…</string>
|
||||
<string name="sk_search_suicide_hotlines">Βρες γραμμή βοήθειας</string>
|
||||
<string name="sk_do_not_show_again">Μην εμφανιστεί ξανά</string>
|
||||
<string name="sk_suicide_helplines_url">http://suicide-help.gr/</string>
|
||||
<string name="sk_post_contains_media">Η ανάρτηση περιέχει πολυμέσα</string>
|
||||
<plurals name="sk_time_seconds">
|
||||
<item quantity="one">%d δευτερόλεπτο</item>
|
||||
<item quantity="other">%d δευτερόλεπτα</item>
|
||||
</plurals>
|
||||
<plurals name="sk_time_minutes">
|
||||
<item quantity="one">%d λεπτό</item>
|
||||
<item quantity="other">%d λεπτά</item>
|
||||
</plurals>
|
||||
<plurals name="sk_time_hours">
|
||||
<item quantity="one">%d ώρα</item>
|
||||
<item quantity="other">%d ώρες</item>
|
||||
</plurals>
|
||||
<plurals name="sk_time_days">
|
||||
<item quantity="one">%d ημέρα</item>
|
||||
<item quantity="other">%d ημέρες</item>
|
||||
</plurals>
|
||||
<string name="sk_settings_underlined_links">Υπογραμμισμένοι σύνδεσμοι</string>
|
||||
<string name="sk_set_as_default">Ορισμός ως προεπιλογή</string>
|
||||
<string name="sk_private_note_hint">Προσθήκη προσωπικής σημείωσης γι\' αυτό το προφίλ</string>
|
||||
<string name="sk_private_note_update_failed">Αποτυχία αποθήκευσης σημείωσης</string>
|
||||
<string name="sk_private_note_confirm_delete">Διγραφή προσωπικής σημείωσης για %s;</string>
|
||||
<string name="sk_delete_note">Διαγραφή προσωπικής σημείωσης</string>
|
||||
<string name="sk_add_note">Προσθήκη προσωπικής σημείωσης</string>
|
||||
<string name="sk_poll_multiple_choice">Πολλαπλές επιλογές</string>
|
||||
<string name="sk_poll_show_results">Εμφάνιση αποτελεσμάτων</string>
|
||||
<string name="sk_poll_hide_results">Απόκρυψη αποτελεσμάτων</string>
|
||||
<string name="sk_edit_alt_text">Επεξεργασία εναλλακτικού κειμένου</string>
|
||||
<string name="sk_settings_default_visibility">Προεπιλεγμένη ορατότητα ανάρτησης</string>
|
||||
<string name="sk_settings_lock_account">Χειροκίνητη αποδοχή νέων ακολούθων</string>
|
||||
<string name="sk_timeline_cache_cleared">Εκκαθαρίστηκε η κρυφή μνήμη ροής αρχικής</string>
|
||||
<string name="sk_settings_show_emoji_reactions">Εμφάνιση αντιδράσεων εμότζι στις ροές</string>
|
||||
<string name="sk_settings_show_emoji_reactions_hide_empty">Απόκρυψη κενών αντιδράσεων εμότζι</string>
|
||||
<string name="sk_settings_show_emoji_reactions_only_opened">Μόνο όταν ανοιχθεί η ανάρτηση</string>
|
||||
<string name="sk_confirm_changes">Επιβεβαίωση αλλαγών</string>
|
||||
<string name="sk_settings_crash_log_unavailable">Καμία διαθέσιμη... ακόμα</string>
|
||||
<string name="sk_crash_log_copied">Καταγραφή σφαλμάτων αντιγράφηκε</string>
|
||||
<string name="sk_open_post_preview">Προεπισκόπηση ανάρτησης</string>
|
||||
<string name="sk_post_preview">Προεπισκόπηση</string>
|
||||
<string name="sk_settings_copy_crash_log">Αντιγραφή τελευταίων καταγραφών σφαλμάτων</string>
|
||||
<string name="sk_button_mutuals">Κοινοί</string>
|
||||
<string name="sk_muted_accounts">Λογαριασμοί σε σίγαση</string>
|
||||
<string name="sk_blocked_accounts">Αποκλεισμένοι λογαριασμοί</string>
|
||||
<string name="sk_settings_display_pronouns_in_user_listings">Εμφάνιση αντωνυμιών στις λίστες χρήστη</string>
|
||||
<string name="sk_tab_notifications">Ειδοποιήσεις</string>
|
||||
<string name="sk_settings_show_labels_in_navigation_bar">Εμφάνιση ετικετών καρτέλας στη μπάρα πλοήγησης</string>
|
||||
<string name="sk_settings_emoji_reactions">Ενεργοποίηση αντιδράσεων εμότζι</string>
|
||||
<string name="sk_settings_emoji_reactions_explanation">Εμφανίζει αντιδράσεις εμότζι σε αναρτήσεις και σού επιτρέπει να προσθέσεις τη δική σου. Πολλοί διακομιστές του Fediverse το υποστηρίζουν, αλλά όχι το Mastodon.</string>
|
||||
<string name="sk_enter_emoji_toast">Παρακαλώ πληκτρολόγησε ένα εμότζι</string>
|
||||
<string name="sk_enter_emoji_hint">Πληκτρολόγησε ένα εμότζι ή αναζήτησε</string>
|
||||
<string name="sk_search_suicide_message">Αν αναζητάς κάποιο σημάδι να μην αυτοκτονήσεις, να το. Σε παρακαλώ σκέψου να επικοινωνήσεις με την τοπική γραμμή βοήθειας για αυτοκτονίες αν είσαι σε κρίση.</string>
|
||||
<plurals name="sk_posts_count_label">
|
||||
<item quantity="one">ανάρτηση</item>
|
||||
<item quantity="other">αναρτήσεις</item>
|
||||
</plurals>
|
||||
<string name="sk_settings_clear_timeline_cache">Εκκαθάριση κρυφής μνήμης ροής αρχικής</string>
|
||||
<string name="sk_posted">ο χρήστης %s δημοσίευσε</string>
|
||||
</resources>
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="mo_setting_uniform_summary">Uzi la ikonon de la aplikaĵon por ĉiuj sciigoj</string>
|
||||
<string name="mo_setting_uniform_summary">Uzi la ikonon de la aplikaĵo por ĉiuj sciigoj</string>
|
||||
<string name="mo_instance_info_moderated_servers">Moderigitaj serviloj</string>
|
||||
<string name="mo_confirm_unfollow_title">Malsekvi Konton</string>
|
||||
<string name="mo_setting_marquee_summary">Malŝaltas la elipsan titolon glitigado</string>
|
||||
<string name="mo_setting_reduced_motion_summary">Malŝalti animaciojn de interagoj</string>
|
||||
<string name="mo_unmute_hashtag">Malsilentigi haketetikedo</string>
|
||||
<string name="mo_unmute_hashtag">Malsilentigi haketetikedon</string>
|
||||
<string name="mo_sending_error">Eraro dum afiŝado</string>
|
||||
<string name="mo_haptic_feedback">Haptika reago</string>
|
||||
<string name="mo_show_media_preview">Montri amaskomunikilaron antaŭrigardon en templinioj</string>
|
||||
@@ -22,7 +22,7 @@
|
||||
<string name="mo_unmuted_conversation_successfully">Sukcese malsilentigita konversacio</string>
|
||||
<string name="mo_no_image_desc">La inkluzivitaj bildoj ne havas priskribon. Bonvolu konsideri aldoni unu, por permesi al vidhandikapitoj partopreni.</string>
|
||||
<string name="mo_confirm_to_unmute_conversation">Ĉu vi certas, ke vi volas malsilentigi ĉi tiun konversacion\?</string>
|
||||
<string name="mo_share_open_url">Malfermi en Aplikaĵo</string>
|
||||
<string name="mo_share_open_url">Malfermi en aplikaĵo</string>
|
||||
<string name="mo_mute_label">Daŭro:</string>
|
||||
<string name="mo_update_available">Moshidon %s estas preta por elŝuti.</string>
|
||||
<string name="mo_confirm_to_unmute_hashtag">Ĉu vi certas, ke vi volas malsilentigi ĉi tiun haketetikedon\?</string>
|
||||
@@ -32,25 +32,25 @@
|
||||
<string name="mo_personal_note_confirm">Konfirmi ŝanĝojn por noti</string>
|
||||
<string name="mo_instance_contact">Kontakto</string>
|
||||
<string name="mo_composer_behavior">Konduto de verkilo</string>
|
||||
<string name="mo_setting_interaction_count_summary">Montri kiom da homoj interagis kun afiŝo en la templinio</string>
|
||||
<string name="mo_setting_interaction_count_summary">Montri, kiom da homoj interagis kun afiŝo en la templinio</string>
|
||||
<string name="mo_open_camera">Foti</string>
|
||||
<string name="mo_blocked_accounts">Blokitaj kontoj</string>
|
||||
<string name="mo_no_image_desc_title">Neniu Bilda priskribo</string>
|
||||
<string name="mo_update_ready">Moshidon %s estas elŝutita kaj preta por instali.</string>
|
||||
<string name="mo_notification_action_replied">Sukcese respondis al la afiŝo de %s</string>
|
||||
<string name="mo_disable_relocate_publish_button_to_enable_customization">Malŝalti Reloki afiŝi butonon por permesi personigon</string>
|
||||
<string name="mo_disable_relocate_publish_button_to_enable_customization">Malŝaltu la agordon \"Reloki la afîsado-butonon\" por permesi personigon</string>
|
||||
<string name="mo_setting_default_reply_privacy_summary">Respondoj estos forigitaj el malkovraj funkcioj</string>
|
||||
<string name="mo_poll_option_add">Aldoni novan balotopcion</string>
|
||||
<string name="mo_enable_dividers">Montri afiŝo-dividiloj</string>
|
||||
<string name="mo_enable_dividers">Montri afiŝo-dividilojn</string>
|
||||
<string name="mo_color_palette_nord">Nord</string>
|
||||
<string name="mo_color_palette_black_and_white">Nigra kaj Blanka</string>
|
||||
<string name="mo_add_custom_server_local_timeline">Aldoni lokan templinion de propra servilo</string>
|
||||
<string name="mo_severity_suspend">Blokita</string>
|
||||
<string name="mo_setting_true_black_summary">Eble ŝparos potencon sur AMOLED-ekranoj</string>
|
||||
<string name="mo_setting_true_black_summary">Eble ŝparos energion sur AMOLED-ekranoj</string>
|
||||
<string name="mo_duration_days_7">7 tagoj</string>
|
||||
<string name="mo_settings_show_posts_without_alt_summary">Afiŝoj estos kaŝitaj en ĉiuj templinioj, sed povas esti malkaŝitaj en fadenoj kaj sciigoj</string>
|
||||
<string name="mo_confirm_to_mute_conversation">Ĉu vi certas, ke vi volas silentigi ĉi tiun konversacion\?</string>
|
||||
<string name="mo_clear_recent_emoji">Forigi lastatempe uzatan emoji</string>
|
||||
<string name="mo_clear_recent_emoji">Forigi lastatempe uzatan emoĝiojn</string>
|
||||
<string name="mo_muted_conversation_successfully">Sukcese silentigita konversacio</string>
|
||||
<string name="mo_swap_bookmark_with_reblog">Interŝanĝi legosignon per reblogi ago</string>
|
||||
<string name="mo_filtered">Filtritaj: %s</string>
|
||||
@@ -61,8 +61,8 @@
|
||||
<string name="mo_mute_conversation">Silentigi konversacion</string>
|
||||
<string name="mo_settings_contribute">Kontribui al Moshidon</string>
|
||||
<string name="mo_recent_emoji_cleared">Lastatempa emoĵion forigis</string>
|
||||
<string name="mo_instance_registration_approval">Aprobo necesa</string>
|
||||
<string name="mo_setting_play_gif_summary">Aŭtomate ludi GIF-ojn en avataroj kaj emoĵioJ</string>
|
||||
<string name="mo_instance_registration_approval">Aprobo necesas</string>
|
||||
<string name="mo_setting_play_gif_summary">Aŭtomate ludi GIF-ojn en avataroj kaj emoĝioj</string>
|
||||
<string name="mo_mute_hashtag">Silentigi haketetikedon</string>
|
||||
<string name="mo_duration_hours_6">6 horoj</string>
|
||||
<string name="mo_duration_hours_1">1 horo</string>
|
||||
@@ -71,10 +71,10 @@
|
||||
<string name="mo_setting_disable_swipe_summary">Glitigi por ŝanĝi viditan templinion</string>
|
||||
<string name="mo_filter_notifications">Filtri sciigojn</string>
|
||||
<string name="mo_disable_reminder_to_add_alt_text">Malŝalti memorigilon por aldoni alttekston</string>
|
||||
<string name="mo_muting">Silentigante…</string>
|
||||
<string name="mo_muting">Silentigado…</string>
|
||||
<string name="mo_mention_reblogger_automatically">Aŭtomate mencii konton, kiu reblogis la afiŝon en respondoj</string>
|
||||
<string name="mo_emoji_recent">Lastatempe uzata</string>
|
||||
<string name="mo_unmuting">Malsilentigante…</string>
|
||||
<string name="mo_emoji_recent">Lastatempe uzataj</string>
|
||||
<string name="mo_unmuting">Malsilentigado…</string>
|
||||
<string name="mo_duration_indefinite">Nedifinita</string>
|
||||
<string name="mo_double_tap_to_search">Duoble premi por malfermi serĉon</string>
|
||||
<string name="mo_personal_note_update_failed">Malsukcesis konservi noton</string>
|
||||
@@ -82,14 +82,18 @@
|
||||
<string name="mo_setting_haptic_feedback_summary">Vibri dum interagado kun afiŝoj</string>
|
||||
<string name="mo_confirm_to_mute_hashtag">Ĉu vi certas, ke vi volas silentigi ĉi tiun haketetikedon\?</string>
|
||||
<string name="mo_swap_bookmark_with_reblog_summary">Legosigni aŭ reblogi afiŝojn de la sciigo</string>
|
||||
<string name="mo_setting_relocate_publish_summary">Movi la afiŝi butonon al la malsupra breto</string>
|
||||
<string name="mo_setting_relocate_publish_summary">Movi la afiŝado-butonon al la malsupra breto</string>
|
||||
<string name="mo_confirm_unfollow">Konfirmi malsekvi %s</string>
|
||||
<string name="mo_instance_admin">Administrita de</string>
|
||||
<string name="mo_hide_compose_button_while_scrolling_setting">Kaŝi verkan butonon dum glitigado</string>
|
||||
<string name="mo_instance_registration_open">Malfermi</string>
|
||||
<string name="mo_fab_compose">Verki</string>
|
||||
<string name="mo_relocate_publish_button">Reloki aifŝi butonon</string>
|
||||
<string name="mo_relocate_publish_button">Reloki la afîsado-butonon</string>
|
||||
<string name="mo_duration_days_3">3 tagoj</string>
|
||||
<string name="mo_setting_remote_follower_summary">Montri sekvantojn de aliaj instancoj</string>
|
||||
<string name="mo_notification_filter_reset">Restarigi al defaŭlta</string>
|
||||
<string name="mo_instance_view_info">Vidi servilan informon</string>
|
||||
<string name="mo_trending_link_read">Legi</string>
|
||||
<string name="mo_error_display_copy_error_details">Kopii informojn</string>
|
||||
<string name="mo_settings_unifiedpush_enable">Ebligi</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user