Compare commits
269 Commits
feature/ca
...
v1.2.3+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a354ea80ab | ||
|
|
9f65b8112a | ||
|
|
6ac5d957fe | ||
|
|
4258c55b88 | ||
|
|
969f29e2e9 | ||
|
|
68921d0f0b | ||
|
|
c4ac4ee173 | ||
|
|
659b4e2fcd | ||
|
|
24e5bda8d3 | ||
|
|
02b1ad8d7a | ||
|
|
47eeb01b75 | ||
|
|
4288814138 | ||
|
|
ac4458e106 | ||
|
|
3d24b2de10 | ||
|
|
ed994b23e9 | ||
|
|
8c4678aba5 | ||
|
|
3d5fb2dfea | ||
|
|
ef6238b593 | ||
|
|
bc9bec3d66 | ||
|
|
d16e199dd1 | ||
|
|
a9c2df2e83 | ||
|
|
4673a4b9f7 | ||
|
|
d4a5286895 | ||
|
|
1b4579346b | ||
|
|
0665b8dd3b | ||
|
|
853124e2ce | ||
|
|
5dcd6e5a0d | ||
|
|
6f25c8be0f | ||
|
|
1db4b1319e | ||
|
|
76a97fcb47 | ||
|
|
4baaa39f35 | ||
|
|
52f025ae5a | ||
|
|
14b805e883 | ||
|
|
433a7b15fe | ||
|
|
6c8cbbc34a | ||
|
|
d4fbb298c1 | ||
|
|
2aeb5f03d6 | ||
|
|
6522403c37 | ||
|
|
f090ca7f75 | ||
|
|
2f02a238df | ||
|
|
0d5fa97800 | ||
|
|
b102deaee1 | ||
|
|
968b2ee460 | ||
|
|
890340de94 | ||
|
|
4ca1a7b29e | ||
|
|
5432f2590c | ||
|
|
60ccf5cf0a | ||
|
|
bc717f5b10 | ||
|
|
486eef21dd | ||
|
|
44a4d02815 | ||
|
|
336a8194bd | ||
|
|
7859f4cd05 | ||
|
|
37622ba9ce | ||
|
|
7a6af89375 | ||
|
|
056bfaacfe | ||
|
|
6684311ec5 | ||
|
|
11943571ad | ||
|
|
f696fcd412 | ||
|
|
2919e109ca | ||
|
|
995f478708 | ||
|
|
fb8764bcd7 | ||
|
|
d7f73e02c5 | ||
|
|
e897b3af57 | ||
|
|
e04fd8a004 | ||
|
|
ada70ae1b5 | ||
|
|
5fdec0900e | ||
|
|
56a93288c4 | ||
|
|
02e3421f98 | ||
|
|
fdbf331432 | ||
|
|
aed86ac6f0 | ||
|
|
3a13d4d6c0 | ||
|
|
f5336564d0 | ||
|
|
1ce49c68fe | ||
|
|
d37e880993 | ||
|
|
6fdb81a01f | ||
|
|
f9d6827572 | ||
|
|
10bf72b9ff | ||
|
|
800f929a15 | ||
|
|
bfcff1e19f | ||
|
|
f373e7df3e | ||
|
|
3985de5b14 | ||
|
|
e175a721d4 | ||
|
|
d9784ebc31 | ||
|
|
f241092277 | ||
|
|
0702703d78 | ||
|
|
2c4504bad3 | ||
|
|
07ca5a8b77 | ||
|
|
798a43906f | ||
|
|
41cb0f2e09 | ||
|
|
e12c0fb81f | ||
|
|
ac39f119e2 | ||
|
|
016faf3df0 | ||
|
|
b2d6879282 | ||
|
|
6926a212f4 | ||
|
|
89afc05d5c | ||
|
|
936f39161b | ||
|
|
ee20ee0722 | ||
|
|
02f9f8c8ea | ||
|
|
de3a252884 | ||
|
|
5e7a00de3e | ||
|
|
2858aeb55e | ||
|
|
357104efa9 | ||
|
|
bb8027c7ef | ||
|
|
f9dd787009 | ||
|
|
e005731ba6 | ||
|
|
18ae3f4f61 | ||
|
|
10dfe0327e | ||
|
|
1d1e921137 | ||
|
|
0985a4c968 | ||
|
|
8df589c103 | ||
|
|
71b6b2f451 | ||
|
|
d85940ded8 | ||
|
|
e9e491c0b0 | ||
|
|
c73562fb75 | ||
|
|
3feacb59c8 | ||
|
|
a033d711c1 | ||
|
|
32081b71f5 | ||
|
|
7849c34d1f | ||
|
|
24977ec613 | ||
|
|
786bbab0d5 | ||
|
|
1facb07c28 | ||
|
|
bba5aba22d | ||
|
|
d7b85d6eba | ||
|
|
6832bfb95c | ||
|
|
4c379b67a3 | ||
|
|
3a2ae1ce71 | ||
|
|
c80afaf9c0 | ||
|
|
31d22bac47 | ||
|
|
b5f6687925 | ||
|
|
b3f25af923 | ||
|
|
78c141e946 | ||
|
|
83d36ce736 | ||
|
|
b928357ff1 | ||
|
|
c074bc57bc | ||
|
|
0e80c88b7d | ||
|
|
5ffa5b01fc | ||
|
|
61d9929485 | ||
|
|
231f19d113 | ||
|
|
bb41f62db5 | ||
|
|
47edc3180b | ||
|
|
9939d99c4b | ||
|
|
8053e8bb05 | ||
|
|
b7e9380bc4 | ||
|
|
83600087e1 | ||
|
|
fe84dc4823 | ||
|
|
c38eb545b1 | ||
|
|
1fc2f81dab | ||
|
|
69ddc95c2c | ||
|
|
a6ac68499c | ||
|
|
c10d7cfee4 | ||
|
|
f933bdbc53 | ||
|
|
274bca84d9 | ||
|
|
6abfe6ddd7 | ||
|
|
ab7489a049 | ||
|
|
a6fd6ae135 | ||
|
|
b30d4a025f | ||
|
|
5b747bfc74 | ||
|
|
a410d19114 | ||
|
|
a8589cc5b0 | ||
|
|
b057c9f7a8 | ||
|
|
96e4a4933c | ||
|
|
630064500d | ||
|
|
9543294996 | ||
|
|
56e9cc3406 | ||
|
|
be569cbe72 | ||
|
|
99f0817bdb | ||
|
|
220cd35d82 | ||
|
|
07f4ef1697 | ||
|
|
f20732ddc2 | ||
|
|
b1e0dc5843 | ||
|
|
285eb25706 | ||
|
|
ec556511e6 | ||
|
|
85c3d9f65f | ||
|
|
a7ebadf269 | ||
|
|
94c09d46c2 | ||
|
|
f6f08d176c | ||
|
|
66cdd63496 | ||
|
|
8b502b605c | ||
|
|
2c0ec28803 | ||
|
|
2e1795dc6f | ||
|
|
cd1be782fa | ||
|
|
67059f3d71 | ||
|
|
15f4d3326b | ||
|
|
a9ab9cb249 | ||
|
|
e65404a466 | ||
|
|
3d47d1b4db | ||
|
|
d1749ab610 | ||
|
|
806c264686 | ||
|
|
34a9cb5a74 | ||
|
|
64fad2e871 | ||
|
|
961c69b525 | ||
|
|
c70f393559 | ||
|
|
9abdc174f4 | ||
|
|
2e5bfa1d9c | ||
|
|
9c89c26097 | ||
|
|
e3b6a5d389 | ||
|
|
0fb54efde5 | ||
|
|
a4a3f32dba | ||
|
|
03a1e29e0c | ||
|
|
eda9ff272b | ||
|
|
b3728e06ac | ||
|
|
33cbd85e19 | ||
|
|
8cb1f3f387 | ||
|
|
3f0c6fcec5 | ||
|
|
797cf893da | ||
|
|
a3564b70e1 | ||
|
|
43004307b8 | ||
|
|
acd1e4ced3 | ||
|
|
6717070f93 | ||
|
|
387499ae49 | ||
|
|
8ab140c55d | ||
|
|
914abb95dd | ||
|
|
5360c0f0f7 | ||
|
|
243d803b51 | ||
|
|
b343fe3835 | ||
|
|
3c42c1120f | ||
|
|
ad840dcef6 | ||
|
|
f73072d95e | ||
|
|
95cb9b5079 | ||
|
|
c6684d3c9b | ||
|
|
5c5989d8c0 | ||
|
|
60e92d30b0 | ||
|
|
8bf8e3f86b | ||
|
|
891ee2d06b | ||
|
|
b450bc7ae8 | ||
|
|
4ca1e0d5db | ||
|
|
859213dd9e | ||
|
|
ad2857791d | ||
|
|
497827f2e2 | ||
|
|
967e333022 | ||
|
|
8df1406006 | ||
|
|
4af42fafdc | ||
|
|
a9e6a452c1 | ||
|
|
a4a4632397 | ||
|
|
421f39e414 | ||
|
|
f8121e2dc4 | ||
|
|
b1784fc51c | ||
|
|
96db0d7de7 | ||
|
|
3837ed9cb1 | ||
|
|
2be789a43c | ||
|
|
fd8d96169a | ||
|
|
1562dc32c1 | ||
|
|
38f377ca09 | ||
|
|
cc28bba884 | ||
|
|
beb3081918 | ||
|
|
1b3c9106b5 | ||
|
|
385b91761b | ||
|
|
43600756c0 | ||
|
|
3c3e0633ad | ||
|
|
f819ad6917 | ||
|
|
e7ad396fc6 | ||
|
|
b1cb4d4257 | ||
|
|
100bd4b062 | ||
|
|
7da09d9b37 | ||
|
|
f46eb07228 | ||
|
|
7627b5eb25 | ||
|
|
c710448c6b | ||
|
|
1ad270b1d6 | ||
|
|
099e253b2b | ||
|
|
66de4a5b91 | ||
|
|
41437d91d5 | ||
|
|
d33d5a6efa | ||
|
|
4f9248d040 | ||
|
|
f40c0e41f3 | ||
|
|
deeb03ff2b | ||
|
|
5c2a09e243 | ||
|
|
2473c999db | ||
|
|
ea2cc265e3 | ||
|
|
a0cd2d42cf |
@@ -15,8 +15,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 83
|
||||
versionName "1.2.2+fork.83"
|
||||
versionCode 90
|
||||
versionName "1.2.3+fork.90"
|
||||
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']
|
||||
}
|
||||
@@ -75,7 +75,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.7'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.8'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public class ThreadFragmentTest {
|
||||
|
||||
private Status fakeStatus(String id, String inReplyTo) {
|
||||
Status status = Status.ofFake(id, null, null);
|
||||
status.inReplyToId = inReplyTo;
|
||||
return status;
|
||||
}
|
||||
|
||||
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
|
||||
return new ThreadFragment.NeighborAncestryInfo(s, d, a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapNeighborhoodAncestry() {
|
||||
StatusContext context = new StatusContext();
|
||||
context.ancestors = List.of(
|
||||
fakeStatus("oldest ancestor", null),
|
||||
fakeStatus("younger ancestor", "oldest ancestor")
|
||||
);
|
||||
Status mainStatus = fakeStatus("main status", "younger ancestor");
|
||||
context.descendants = List.of(
|
||||
fakeStatus("first reply", "main status"),
|
||||
fakeStatus("reply to first reply", "first reply"),
|
||||
fakeStatus("third level reply", "reply to first reply"),
|
||||
fakeStatus("another reply", "main status")
|
||||
);
|
||||
|
||||
List<ThreadFragment.NeighborAncestryInfo> neighbors =
|
||||
ThreadFragment.mapNeighborhoodAncestry(mainStatus, context);
|
||||
|
||||
assertEquals(List.of(
|
||||
fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null),
|
||||
fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)),
|
||||
fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)),
|
||||
fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus),
|
||||
fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)),
|
||||
fakeInfo(context.descendants.get(2), null, context.descendants.get(1)),
|
||||
fakeInfo(context.descendants.get(3), null, null)
|
||||
), neighbors);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maybeApplyMainStatus() {
|
||||
ThreadFragment fragment = new ThreadFragment();
|
||||
fragment.contextInitiallyRendered = true;
|
||||
fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH);
|
||||
|
||||
Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH);
|
||||
update1.editedAt = Instant.ofEpochSecond(1);
|
||||
fragment.updatedStatus = update1;
|
||||
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.maybeApplyMainStatus();
|
||||
assertEquals("fired update event", update1, event1.status);
|
||||
assertEquals("updated main status", update1, fragment.mainStatus);
|
||||
|
||||
Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH);
|
||||
update2.favouritesCount = 123;
|
||||
fragment.updatedStatus = update2;
|
||||
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.maybeApplyMainStatus();
|
||||
assertEquals("only fired counter update event", update2.id, event2.id);
|
||||
assertEquals("updated counter is correct", 123, event2.favorites);
|
||||
assertEquals("updated main status", update2, fragment.mainStatus);
|
||||
|
||||
Status update3 = Status.ofFake("123456", "whatever", Instant.EPOCH);
|
||||
fragment.contextInitiallyRendered = false;
|
||||
fragment.updatedStatus = update3;
|
||||
assertNull("no update when context hasn't been rendered", fragment.maybeApplyMainStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortStatusContext() {
|
||||
StatusContext context = new StatusContext();
|
||||
context.ancestors = List.of(
|
||||
fakeStatus("younger ancestor", "oldest ancestor"),
|
||||
fakeStatus("oldest ancestor", null)
|
||||
);
|
||||
context.descendants = List.of(
|
||||
fakeStatus("reply to first reply", "first reply"),
|
||||
fakeStatus("third level reply", "reply to first reply"),
|
||||
fakeStatus("first reply", "main status"),
|
||||
fakeStatus("another reply", "main status")
|
||||
);
|
||||
|
||||
ThreadFragment.sortStatusContext(
|
||||
fakeStatus("main status", "younger ancestor"),
|
||||
context
|
||||
);
|
||||
List<Status> expectedAncestors = List.of(
|
||||
fakeStatus("oldest ancestor", null),
|
||||
fakeStatus("younger ancestor", "oldest ancestor")
|
||||
);
|
||||
List<Status> expectedDescendants = List.of(
|
||||
fakeStatus("first reply", "main status"),
|
||||
fakeStatus("reply to first reply", "first reply"),
|
||||
fakeStatus("third level reply", "reply to first reply"),
|
||||
fakeStatus("another reply", "main status")
|
||||
);
|
||||
|
||||
// TODO: ??? i have no idea how this code works. it certainly doesn't return what i'd expect
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class UiUtilsTest {
|
||||
@BeforeClass
|
||||
public static void createDummySession() {
|
||||
Instance dummyInstance = new Instance();
|
||||
dummyInstance.uri = "test.tld";
|
||||
Account dummyAccount = new Account();
|
||||
dummyAccount.id = "123456";
|
||||
AccountSessionManager.getInstance().addAccount(dummyInstance, null, dummyAccount, null, null);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanUp() {
|
||||
AccountSessionManager.getInstance().removeAccount("test.tld_123456");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseFediverseHandle() {
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||
UiUtils.parseFediverseHandle("megalodon@floss.social")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||
UiUtils.parseFediverseHandle("@megalodon@floss.social")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.empty())),
|
||||
UiUtils.parseFediverseHandle("@megalodon")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||
UiUtils.parseFediverseHandle("mailto:megalodon@floss.social")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.empty(),
|
||||
UiUtils.parseFediverseHandle("megalodon")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.empty(),
|
||||
UiUtils.parseFediverseHandle("this is not a fedi handle")
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Optional.empty(),
|
||||
UiUtils.parseFediverseHandle("not@a-domain")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acctMatches() {
|
||||
assertTrue("local account, domain not specified", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone",
|
||||
"someone",
|
||||
null
|
||||
));
|
||||
|
||||
assertTrue("domain not specified", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone@somewhere.social",
|
||||
"someone",
|
||||
null
|
||||
));
|
||||
|
||||
assertTrue("local account, domain specified, different casing", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"SomeOne",
|
||||
"someone",
|
||||
"Test.TLD"
|
||||
));
|
||||
|
||||
assertFalse("username doesn't match", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone-else@somewhere.social",
|
||||
"someone",
|
||||
"somewhere.social"
|
||||
));
|
||||
|
||||
assertFalse("domain doesn't match", UiUtils.acctMatches(
|
||||
"test.tld_123456",
|
||||
"someone@somewhere.social",
|
||||
"someone",
|
||||
"somewhere.else"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import static org.joinmastodon.android.model.Filter.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.Filter.FilterContext.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class StatusFilterPredicateTest {
|
||||
|
||||
private static final Filter hideMeFilter = new Filter(), warnMeFilter = new Filter();
|
||||
private static final List<Filter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||
|
||||
private static final Status
|
||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
|
||||
|
||||
static {
|
||||
hideMeFilter.phrase = "hide me";
|
||||
hideMeFilter.filterAction = HIDE;
|
||||
hideMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||
|
||||
warnMeFilter.phrase = "warning";
|
||||
warnMeFilter.filterAction = WARN;
|
||||
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHide() {
|
||||
assertFalse("should not pass because matching filter applies to given context",
|
||||
new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideRegardlessOfContext() {
|
||||
assertTrue("filters without context should always pass",
|
||||
new StatusFilterPredicate(allFilters, null).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideInDifferentContext() {
|
||||
assertTrue("should pass because matching filter does not apply to given context",
|
||||
new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideWithWarningText() {
|
||||
assertTrue("should pass because matching filter is for warnings",
|
||||
new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarn() {
|
||||
assertFalse("should not pass because filter applies to given context",
|
||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnRegardlessOfContext() {
|
||||
assertTrue("filters without context should always pass",
|
||||
new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnInDifferentContext() {
|
||||
assertTrue("should pass because filter does not apply to given context",
|
||||
new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnWithHideText() {
|
||||
assertTrue("should pass because matching filter is for hiding",
|
||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,22 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -46,7 +62,8 @@
|
||||
<data android:scheme="megalodon-android-auth" android:host="callback"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
||||
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/TransparentDialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
|
||||
# https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
|
||||
# This list contains domains of toxic mastodon instances
|
||||
# Last-Modified: 1672044500
|
||||
|
||||
# gab - a neonazi social network
|
||||
gab.ai
|
||||
gab.com
|
||||
gab.protohype.net
|
||||
|
||||
# consequence-free speech
|
||||
social.unzensiert.to
|
||||
freeatlantis.com
|
||||
|
||||
# reactionary bigotry and hatespeech against marginalized groups
|
||||
poa.st
|
||||
freespeechextremist.com
|
||||
rdrama.cc
|
||||
outpoa.st
|
||||
anime.website
|
||||
gameliberty.club
|
||||
social.byoblu.com
|
||||
yggdrasil.social
|
||||
smuglo.li
|
||||
dogeposting.social
|
||||
unsafe.space
|
||||
freezepeach.xyz
|
||||
|
||||
# + CSAM
|
||||
rojogato.com
|
||||
|
||||
# antivaxxer shitposting & fearmongering
|
||||
shadowsocial.org
|
||||
|
||||
# Kiwifarms
|
||||
kiwifarms.net
|
||||
kiwifarms.cc
|
||||
kiwifarms.is
|
||||
kiwifarms.pleroma.net
|
||||
|
||||
|
||||
# https://mastodon.art/@Curator/109649354849593592
|
||||
|
||||
poa.st antisemitic racist homophobic
|
||||
nicecrew.digital antisemitic
|
||||
beefyboys.win antisemitic racist homophobic harassment
|
||||
cawfee.club antisemitic racist homophobic
|
||||
comfyboy.club antisemitic racist homophobic
|
||||
freespeechextremist.com racist homophobic
|
||||
cum.salon racist misogynist
|
||||
bae.st racist
|
||||
natehiggers.online racist
|
||||
rapemeat.solutions misogynist
|
||||
rapist.town misogynist
|
||||
rapefeminists.network misogynist
|
||||
kiwifarms.cc harassment
|
||||
noagendasocial.com noagenda
|
||||
posting.lolicon.rocks underage
|
||||
urchan.org harassment homophobic racist
|
||||
ryona.agency harassment
|
||||
yggdrasil.social antisemitic homophobic racist
|
||||
genderheretics.xyz transphobic
|
||||
baraag.net underage
|
||||
lolison.top underage
|
||||
shota.house underage
|
||||
shota.social underage
|
||||
aethy.com underage
|
||||
taullo.social underage
|
||||
childpawn.shop underage
|
||||
posting.lolicon.rocks underage
|
||||
loli.best underage
|
||||
gothloli.club underage
|
||||
smuglo.li underage
|
||||
youjo.love underage
|
||||
pedo.school underage
|
||||
lolison.network underage
|
||||
freak.university underage
|
||||
mirr0r.city underage
|
||||
xhais.love underage
|
||||
refusal.biz underage
|
||||
refusal.llc underage
|
||||
mirr0r.city underage
|
||||
nnia.space underage
|
||||
ignorelist.com malicious
|
||||
repl.co malicious
|
||||
|
||||
# custom
|
||||
|
||||
pawoo.net csam
|
||||
|
171
mastodon/src/main/assets/blocks.txt
Normal file
171
mastodon/src/main/assets/blocks.txt
Normal file
@@ -0,0 +1,171 @@
|
||||
13bells.com
|
||||
4aem.com
|
||||
aethy.com
|
||||
anime.website
|
||||
annihilation.social
|
||||
anon-kenkai.com
|
||||
asbestos.cafe
|
||||
bae.st
|
||||
bajax.us
|
||||
banepo.st
|
||||
baraag.net
|
||||
beefyboys.win
|
||||
beepboop.ga
|
||||
berserker.town
|
||||
bikeshed.party
|
||||
boks.moe
|
||||
brainsoap.net
|
||||
breastmilk.club
|
||||
brighteon.social
|
||||
cawfee.club
|
||||
clew.lol
|
||||
clubcyberia.co
|
||||
collapsitarian.io
|
||||
comfyboy.club
|
||||
contrapointsfan.club
|
||||
cum.camp
|
||||
cum.salon
|
||||
cybercriminal.eu
|
||||
darknight-coffee.org
|
||||
dembased.xyz
|
||||
desupost.soy
|
||||
detroitriotcity.com
|
||||
eatthebugs.social
|
||||
eientei.org
|
||||
elementality.org
|
||||
eveningzoo.club
|
||||
firedragonstudios.com
|
||||
firefaithfellowship.com
|
||||
fluf.club
|
||||
foxfam.club
|
||||
freak.university
|
||||
freeatlantis.com
|
||||
freecumextremist.com
|
||||
freedomstrike.org
|
||||
freesoftwareextremist.com
|
||||
freespeech.group
|
||||
freespeechextremist.com
|
||||
freetalklive.com
|
||||
froth.zone
|
||||
fulltermprivacy.com
|
||||
gameliberty.club
|
||||
gearlandia.haus
|
||||
genderheretics.xyz
|
||||
geofront.rocks
|
||||
gleasonator.com
|
||||
glee.li
|
||||
glindr.org
|
||||
goyim.app
|
||||
goyslop.cafe
|
||||
haeder.net
|
||||
handholding.io
|
||||
hidamari.apartments
|
||||
hitchhiker.social
|
||||
hunk.city
|
||||
iddqd.social
|
||||
intkos.link
|
||||
justicewarrior.social
|
||||
kawa-kun.com
|
||||
kitsunemimi.club
|
||||
kiwifarms.cc
|
||||
kompost.cz
|
||||
kurosawa.moe
|
||||
leafposter.club
|
||||
leftychan.net
|
||||
lewdieheaven.com
|
||||
liberdon.com
|
||||
ligma.pro
|
||||
lizards.live
|
||||
lolicon.rocks
|
||||
lolison.top
|
||||
lovingexpressions.net
|
||||
lucasvl.nl
|
||||
mahodou.moe
|
||||
makemysarcophagus.com
|
||||
maladaptive.art
|
||||
masochi.st
|
||||
mastinator.com
|
||||
merovingian.club
|
||||
midwaytrades.com
|
||||
mirr0r.city
|
||||
moa.st
|
||||
mouse.services
|
||||
mugicha.club
|
||||
narrativerry.xyz
|
||||
natehiggers.online
|
||||
neckbeard.xyz
|
||||
needs.vodka
|
||||
neenster.org
|
||||
nicecrew.digital
|
||||
nnia.space
|
||||
noagendasocial.com
|
||||
noagendasocial.nl
|
||||
noagendatube.com
|
||||
nobodyhasthe.biz
|
||||
nukem.biz
|
||||
obo.sh
|
||||
onionfarms.org
|
||||
outpoa.st
|
||||
pawlicker.com
|
||||
pawoo.net
|
||||
pedo.school
|
||||
piazza.today
|
||||
pibvt.net
|
||||
pieville.net
|
||||
pisskey.io
|
||||
plagu.ee
|
||||
pmth.us
|
||||
poa.st
|
||||
poast.org
|
||||
poast.tv
|
||||
poster.place
|
||||
prospeech.space
|
||||
quodverum.com
|
||||
rakket.app
|
||||
rapemeat.solutions
|
||||
rdrama.cc
|
||||
rebelbase.site
|
||||
retardedniggers.forsale
|
||||
rojogato.com
|
||||
ryona.agency
|
||||
schwartzwelt.xyz
|
||||
seal.cafe
|
||||
shigusegubu.club
|
||||
shitpost.cloud
|
||||
shitposter.club
|
||||
shota.house
|
||||
silliness.observer
|
||||
skinheads.eu
|
||||
skinheads.io
|
||||
skinheads.social
|
||||
skinheads.uk
|
||||
skippers-bin.com
|
||||
skyshanty.xyz
|
||||
slash.cl
|
||||
sleepy.cafe
|
||||
smuglo.li
|
||||
sneed.social
|
||||
sonichu.com
|
||||
spinster.xyz
|
||||
springbo.cc
|
||||
starnix.network
|
||||
stereophonic.space
|
||||
strelizia.net
|
||||
syspxl.xyz
|
||||
tastingtraffic.net
|
||||
teci.world
|
||||
theapex.social
|
||||
thepostearthdestination.com
|
||||
tkammer.de
|
||||
trumpislovetrumpis.life
|
||||
truthsocial.co.in
|
||||
urchan.org
|
||||
varishangout.net
|
||||
whinge.house
|
||||
whinge.town
|
||||
wideboys.org
|
||||
wolfgirl.bar
|
||||
xn--p1abe3d.xn--80asehdb
|
||||
yggdrasil.social
|
||||
youjo.love
|
||||
zztails.gay
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
public static void exit(Context context) {
|
||||
Intent intent = new Intent(context, ExitActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,21 +3,24 @@ package org.joinmastodon.android;
|
||||
import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.jsoup.internal.StringUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
@@ -28,18 +31,43 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
|
||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);
|
||||
boolean isOpenable = isFediUrl || fediHandle.isPresent();
|
||||
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
if(sessions.isEmpty()){
|
||||
if (sessions.isEmpty()){
|
||||
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}else if(sessions.size()==1){
|
||||
} else if (isOpenable || sessions.size() > 1) {
|
||||
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
|
||||
if (isOpenable) sheet.setOnClick((accountId, open) -> {
|
||||
if (open && text.isPresent()) {
|
||||
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
|
||||
if (clazz == null) {
|
||||
Toast.makeText(this, R.string.sk_open_in_app_failed, Toast.LENGTH_SHORT).show();
|
||||
// TODO: do something about the window getting leaked
|
||||
sheet.dismiss();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
args.putString("fromExternalShare", clazz.getSimpleName());
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.putExtras(args);
|
||||
finish();
|
||||
startActivity(intent);
|
||||
};
|
||||
if (isFediUrl) UiUtils.lookupURL(this, accountId, text.get(), false, callback);
|
||||
else UiUtils.lookupAccountHandle(this, accountId, fediHandle.get(), callback);
|
||||
} else {
|
||||
openComposeFragment(accountId);
|
||||
}
|
||||
});
|
||||
sheet.show();
|
||||
} else if (sessions.size() == 1) {
|
||||
openComposeFragment(sessions.get(0).getID());
|
||||
}else{
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
|
||||
UiUtils.pickAccount(this, null, R.string.choose_account, 0,
|
||||
session -> openComposeFragment(session.getID()),
|
||||
b -> b.setOnCancelListener(d -> finish())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.SharedPreferences;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
@@ -47,16 +48,20 @@ public class GlobalUserPreferences{
|
||||
public static boolean replyLineAboveHeader;
|
||||
public static boolean compactReblogReplyLine;
|
||||
public static boolean confirmBeforeReblog;
|
||||
public static boolean allowRemoteLoading;
|
||||
public static String publishButtonText;
|
||||
public static ThemePreference theme;
|
||||
public static ColorPreference color;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
|
||||
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
||||
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
||||
public static Map<String, List<String>> recentLanguages;
|
||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||
public static Set<String> accountsWithLocalOnlySupport;
|
||||
public static Set<String> accountsInGlitchMode;
|
||||
public static Set<String> accountsWithContentTypesEnabled;
|
||||
public static Map<String, ContentType> accountsDefaultContentTypes;
|
||||
|
||||
/**
|
||||
* Pleroma
|
||||
@@ -73,6 +78,16 @@ public class GlobalUserPreferences{
|
||||
catch (JsonSyntaxException ignored) { return orElse; }
|
||||
}
|
||||
|
||||
public static void removeAccount(String accountId) {
|
||||
recentLanguages.remove(accountId);
|
||||
pinnedTimelines.remove(accountId);
|
||||
accountsInGlitchMode.remove(accountId);
|
||||
accountsWithLocalOnlySupport.remove(accountId);
|
||||
accountsWithContentTypesEnabled.remove(accountId);
|
||||
accountsDefaultContentTypes.remove(accountId);
|
||||
save();
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
@@ -111,6 +126,9 @@ public class GlobalUserPreferences{
|
||||
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
replyVisibility=prefs.getString("replyVisibility", null);
|
||||
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||
|
||||
try {
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
||||
@@ -158,6 +176,9 @@ public class GlobalUserPreferences{
|
||||
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
|
||||
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
|
||||
.putString("replyVisibility", replyVisibility)
|
||||
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
||||
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -178,4 +199,3 @@ public class GlobalUserPreferences{
|
||||
DARK
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ package org.joinmastodon.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
@@ -20,12 +23,13 @@ import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity{
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
@@ -35,10 +39,18 @@ public class MainActivity extends FragmentStackActivity{
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||
}else{
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
AccountSessionManager.getInstance()
|
||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
||||
showFragmentForExternalShare(intent.getExtras());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
@@ -52,6 +64,7 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
@@ -75,11 +88,12 @@ public class MainActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent){
|
||||
super.onNewIntent(intent);
|
||||
if(intent.getBooleanExtra("fromNotification", false)){
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras());
|
||||
else if (intent.getBooleanExtra("fromNotification", false)) {
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
AccountSession accountSession;
|
||||
try{
|
||||
accountSession=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
AccountSessionManager.getInstance().getAccount(accountID);
|
||||
}catch(IllegalStateException x){
|
||||
return;
|
||||
}
|
||||
@@ -103,23 +117,24 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
|
||||
private void showFragmentForNotification(Notification notification, String accountID){
|
||||
Fragment fragment;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("_can_go_back", true);
|
||||
try{
|
||||
notification.postprocess();
|
||||
}catch(ObjectValidationException x){
|
||||
Log.w("MainActivity", x);
|
||||
return;
|
||||
}
|
||||
if(notification.status!=null){
|
||||
fragment=new ThreadFragment();
|
||||
args.putParcelable("status", Parcels.wrap(notification.status));
|
||||
}else{
|
||||
fragment=new ProfileFragment();
|
||||
args.putParcelable("profileAccount", Parcels.wrap(notification.account));
|
||||
}
|
||||
UiUtils.showFragmentForNotification(this, notification, accountID, null);
|
||||
}
|
||||
|
||||
private void showFragmentForExternalShare(Bundle args) {
|
||||
String clazz = args.getString("fromExternalShare");
|
||||
Fragment fragment = switch (clazz) {
|
||||
case "ThreadFragment" -> new ThreadFragment();
|
||||
case "ProfileFragment" -> new ProfileFragment();
|
||||
default -> null;
|
||||
};
|
||||
if (fragment == null) return;
|
||||
args.putBoolean("_can_go_back", true);
|
||||
fragment.setArguments(args);
|
||||
showFragment(fragment);
|
||||
}
|
||||
@@ -153,18 +168,40 @@ public class MainActivity extends FragmentStackActivity{
|
||||
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
|
||||
);
|
||||
Bundle currentArgs = currentFragment.getArguments();
|
||||
if (this.fragmentContainers.size() == 1
|
||||
&& currentArgs != null
|
||||
&& currentArgs.getBoolean("_can_go_back", false)
|
||||
&& currentArgs.containsKey("account")) {
|
||||
if (fragmentContainers.size() != 1
|
||||
|| currentArgs == null
|
||||
|| !currentArgs.getBoolean("_can_go_back", false)) {
|
||||
super.onBackPressed();
|
||||
return;
|
||||
}
|
||||
if (currentArgs.getBoolean("_finish_on_back", false)) {
|
||||
finish();
|
||||
} else if (currentArgs.containsKey("account")) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString("account", currentArgs.getString("account"));
|
||||
args.putString("tab", "notifications");
|
||||
if (getIntent().getBooleanExtra("fromNotification", false)) {
|
||||
args.putString("tab", "notifications");
|
||||
}
|
||||
Fragment fragment=new HomeFragment();
|
||||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
public Fragment getCurrentFragment() {
|
||||
for (int i = fragmentContainers.size() - 1; i >= 0; i--) {
|
||||
FrameLayout fl = fragmentContainers.get(i);
|
||||
if (fl.getVisibility() == View.VISIBLE) {
|
||||
return getFragmentManager().findFragmentById(fl.getId());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
super.onProvideAssistContent(assistContent);
|
||||
Fragment fragment = getCurrentFragment();
|
||||
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
|
||||
public class PanicResponderActivity extends Activity {
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Intent intent = getIntent();
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
AccountSessionManager.getInstance().getLoggedInAccounts().forEach(accountSession -> logOut(accountSession.getID()));
|
||||
ExitActivity.exit(this);
|
||||
}
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
private void logOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.NotificationAction;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
@@ -33,6 +34,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@@ -273,8 +275,23 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
}
|
||||
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
|
||||
|
||||
// copied from ComposeFragment - TODO: generalize?
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
Status status = notification.status;
|
||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!status.account.id.equals(ownID))
|
||||
mentions.add('@'+status.account.acct);
|
||||
for(Mention mention:status.mentions){
|
||||
if(mention.id.equals(ownID))
|
||||
continue;
|
||||
String m='@'+mention.acct;
|
||||
if(!mentions.contains(m))
|
||||
mentions.add(m);
|
||||
}
|
||||
String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
|
||||
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
req.status = input.toString();
|
||||
req.status = initialText + input.toString();
|
||||
req.language = preferences.postingDefaultLanguage;
|
||||
req.visibility = preferences.postingDefaultVisibility;
|
||||
req.inReplyToId = notification.status.id;
|
||||
@@ -282,7 +299,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
req.spoilerText = "re: " + notification.status.spoilerText;
|
||||
}
|
||||
|
||||
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<Status>() {
|
||||
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Status status) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
@@ -37,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=3;
|
||||
private static final int DB_VERSION=4;
|
||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
@@ -62,7 +61,7 @@ public class CacheController{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Status> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
@@ -74,10 +73,8 @@ public class CacheController{
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
|
||||
continue outer;
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
@@ -92,7 +89,7 @@ public class CacheController{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -115,7 +112,7 @@ public class CacheController{
|
||||
runOnDbThread((db)->{
|
||||
if(clear)
|
||||
db.delete("home_timeline", null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Status s:posts){
|
||||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
@@ -123,6 +120,7 @@ public class CacheController{
|
||||
if(s.hasGapAfter)
|
||||
flags|=POST_FLAG_GAP_AFTER;
|
||||
values.put("flags", flags);
|
||||
values.put("time", s.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
@@ -137,7 +135,7 @@ public class CacheController{
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Notification> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
@@ -148,10 +146,8 @@ public class CacheController{
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
@@ -164,17 +160,13 @@ public class CacheController{
|
||||
}
|
||||
}
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
@@ -201,7 +193,7 @@ public class CacheController{
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
if(clear)
|
||||
db.delete(table, null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Notification n:notifications){
|
||||
if(n.type==null){
|
||||
continue;
|
||||
@@ -209,6 +201,7 @@ public class CacheController{
|
||||
values.put("id", n.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||
values.put("type", n.type.ordinal());
|
||||
values.put("time", n.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
@@ -305,21 +298,24 @@ public class CacheController{
|
||||
CREATE TABLE `home_timeline` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_all` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_mentions` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
createRecentSearchesTable(db);
|
||||
createPostsNotificationsTable(db);
|
||||
@@ -327,12 +323,16 @@ public class CacheController{
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||
if(oldVersion==1){
|
||||
if(oldVersion<2){
|
||||
createRecentSearchesTable(db);
|
||||
}
|
||||
if(oldVersion==2){
|
||||
if(oldVersion<3){
|
||||
// MEGALODON-SPECIFIC
|
||||
createPostsNotificationsTable(db);
|
||||
}
|
||||
if(oldVersion<4){
|
||||
addTimeColumns(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||
@@ -350,9 +350,21 @@ public class CacheController{
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
}
|
||||
|
||||
private void addTimeColumns(SQLiteDatabase db){
|
||||
db.execSQL("DELETE FROM `home_timeline`");
|
||||
db.execSQL("DELETE FROM `notifications_all`");
|
||||
db.execSQL("DELETE FROM `notifications_mentions`");
|
||||
db.execSQL("DELETE FROM `notifications_posts`");
|
||||
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_posts` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -51,7 +52,9 @@ public class MastodonAPIController{
|
||||
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
|
||||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
private AccountSession session;
|
||||
private static List<String> badDomains = new ArrayList<>();
|
||||
@@ -60,7 +63,7 @@ public class MastodonAPIController{
|
||||
thread.start();
|
||||
try {
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||
MastodonApp.context.getAssets().open("blocks.tsv")
|
||||
MastodonApp.context.getAssets().open("blocks.txt")
|
||||
));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
@@ -91,7 +94,7 @@ public class MastodonAPIController{
|
||||
Request.Builder builder=new Request.Builder()
|
||||
.url(req.getURL().toString())
|
||||
.method(req.getMethod(), req.getRequestBody())
|
||||
.header("User-Agent", "MastodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
.header("User-Agent", "MegalodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
|
||||
String token=null;
|
||||
if(session!=null)
|
||||
|
||||
@@ -20,9 +20,11 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -44,7 +46,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
TypeToken<T> respTypeToken;
|
||||
Call okhttpCall;
|
||||
Token token;
|
||||
boolean canceled;
|
||||
boolean canceled, isRemote;
|
||||
Map<String, String> headers;
|
||||
private ProgressDialog progressDialog;
|
||||
protected boolean removeUnsupportedItems;
|
||||
@@ -101,6 +103,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> execRemote(String domain) {
|
||||
return execRemote(domain, null);
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> execRemote(String domain, @Nullable AccountSession remoteSession) {
|
||||
this.isRemote = true;
|
||||
return Optional.ofNullable(remoteSession)
|
||||
.or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream()
|
||||
.filter(acc -> acc.domain.equals(domain))
|
||||
.findAny())
|
||||
.map(AccountSession::getID)
|
||||
.map(this::exec)
|
||||
.orElse(this.execNoAuth(domain));
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||
return wrapProgress(activity, message, cancelable, null);
|
||||
}
|
||||
@@ -167,6 +184,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
@CallSuper
|
||||
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
||||
if(respObj instanceof BaseModel){
|
||||
((BaseModel) respObj).isRemote = isRemote;
|
||||
((BaseModel) respObj).postprocess();
|
||||
}else if(respObj instanceof List){
|
||||
if(removeUnsupportedItems){
|
||||
@@ -175,6 +193,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
Object item=itr.next();
|
||||
if(item instanceof BaseModel){
|
||||
try{
|
||||
((BaseModel) item).isRemote = isRemote;
|
||||
((BaseModel) item).postprocess();
|
||||
}catch(ObjectValidationException x){
|
||||
Log.w(TAG, "Removing invalid object from list", x);
|
||||
@@ -182,15 +201,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
}
|
||||
}
|
||||
// no idea why we're post-processing twice, but well, as long
|
||||
// as upstream does it like this, i don't wanna break anything
|
||||
for(Object item:((List<?>) respObj)){
|
||||
if(item instanceof BaseModel){
|
||||
((BaseModel) item).isRemote = isRemote;
|
||||
((BaseModel) item).postprocess();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
for(Object item:((List<?>) respObj)){
|
||||
if(item instanceof BaseModel)
|
||||
if(item instanceof BaseModel) {
|
||||
((BaseModel) item).isRemote = isRemote;
|
||||
((BaseModel) item).postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class StatusInteractionController{
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
runningFavoriteRequests.remove(status.id);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class StatusInteractionController{
|
||||
public void onSuccess(Status reblog){
|
||||
Status result = reblog.getContentStatus();
|
||||
runningReblogRequests.remove(status.id);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetAccountByHandle extends MastodonAPIRequest<Account>{
|
||||
/**
|
||||
* note that this method usually only returns a result if the instance already knows about an
|
||||
* account - so it makes sense for looking up local users, search might be preferred otherwise
|
||||
*/
|
||||
public GetAccountByHandle(String acct){
|
||||
super(HttpMethod.GET, "/accounts/lookup", Account.class);
|
||||
addQueryParameter("acct", acct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
|
||||
private String maxID;
|
||||
public PleromaMarkNotificationsRead(String maxID) {
|
||||
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
|
||||
this.maxID = maxID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() {
|
||||
MultipartBody.Builder builder=new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
builder.addFormDataPart("max_id", maxID);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
@@ -46,6 +47,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public String language;
|
||||
|
||||
public String quoteId;
|
||||
public ContentType contentType;
|
||||
|
||||
public static class Poll{
|
||||
public ArrayList<String> options=new ArrayList<>();
|
||||
|
||||
@@ -2,17 +2,22 @@ package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
|
||||
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
|
||||
public GetStatusSourceText(String id){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
|
||||
}
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public static class Response extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String text;
|
||||
@RequiredField
|
||||
public String spoilerText;
|
||||
public ContentType contentType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetBubbleTimeline(String maxID, int limit) {
|
||||
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(GlobalUserPreferences.replyVisibility != null)
|
||||
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
@@ -7,6 +9,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@@ -14,6 +17,7 @@ import org.joinmastodon.android.model.Token;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AccountSession{
|
||||
public Token token;
|
||||
@@ -87,4 +91,15 @@ public class AccountSession{
|
||||
pushSubscriptionManager=new PushSubscriptionManager(getID());
|
||||
return pushSubscriptionManager;
|
||||
}
|
||||
|
||||
public Optional<Instance> getInstance() {
|
||||
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
|
||||
}
|
||||
|
||||
public Uri getInstanceUri() {
|
||||
return new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.util.Log;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -110,6 +111,12 @@ public class AccountSessionManager{
|
||||
sessions.put(session.getID(), session);
|
||||
lastActiveAccountID=session.getID();
|
||||
writeAccountsFile();
|
||||
|
||||
// write initial instance info to file immediately to avoid sessions without instance info
|
||||
InstanceInfoStorageWrapper wrapper = new InstanceInfoStorageWrapper();
|
||||
wrapper.instance = instance;
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
||||
|
||||
updateMoreInstanceInfo(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
@@ -118,14 +125,16 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public synchronized void writeAccountsFile(){
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~");
|
||||
File file = new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
try{
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
try(FileOutputStream out=new FileOutputStream(tmpFile)){
|
||||
SessionsStorageWrapper w=new SessionsStorageWrapper();
|
||||
w.accounts=new ArrayList<>(sessions.values());
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(w, writer);
|
||||
writer.flush();
|
||||
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing accounts file", x);
|
||||
@@ -151,6 +160,11 @@ public class AccountSessionManager{
|
||||
return sessions.get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession tryGetAccount(Account account) {
|
||||
return sessions.get(account.getDomainFromURL() + "_" + account.id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccountSession getLastActiveAccount(){
|
||||
if(sessions.isEmpty() || lastActiveAccountID==null)
|
||||
@@ -178,6 +192,7 @@ public class AccountSessionManager{
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
GlobalUserPreferences.removeAccount(id);
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
if(sessions.isEmpty())
|
||||
@@ -248,31 +263,35 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public void maybeUpdateLocalInfo(){
|
||||
maybeUpdateLocalInfo(null);
|
||||
}
|
||||
|
||||
public void maybeUpdateLocalInfo(AccountSession activeSession){
|
||||
long now=System.currentTimeMillis();
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
for(AccountSession session:sessions.values()){
|
||||
domains.add(session.domain.toLowerCase());
|
||||
// if(now-session.infoLastUpdated>24L*3600_000L){
|
||||
updateSessionPreferences(session);
|
||||
updateSessionLocalInfo(session);
|
||||
// }
|
||||
// if(now-session.filtersLastUpdated>3600_000L){
|
||||
updateSessionWordFilters(session);
|
||||
// }
|
||||
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
|
||||
updateSessionPreferences(session);
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
|
||||
updateSessionWordFilters(session);
|
||||
}
|
||||
updateSessionMarkers(session);
|
||||
}
|
||||
if(loadedInstances){
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateCustomEmojis(Set<String> domains){
|
||||
private void maybeUpdateCustomEmojis(Set<String> domains, String activeDomain){
|
||||
long now=System.currentTimeMillis();
|
||||
for(String domain:domains){
|
||||
// Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||
updateInstanceInfo(domain);
|
||||
// }
|
||||
Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
|
||||
updateInstanceInfo(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +419,9 @@ public class AccountSessionManager{
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
|
||||
wrapper.instance = instance;
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
@@ -411,10 +432,13 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
|
||||
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
|
||||
File file = getInstanceInfoFile(domain);
|
||||
File tmpFile = new File(file.getPath() + "~");
|
||||
try(FileOutputStream out=new FileOutputStream(tmpFile)){
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(emojis, writer);
|
||||
writer.flush();
|
||||
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "Error writing instance info file for "+domain, x);
|
||||
}
|
||||
@@ -434,7 +458,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
if(!loadedInstances){
|
||||
loadedInstances=true;
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
maybeUpdateCustomEmojis(domains, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,10 +482,6 @@ public class AccountSessionManager{
|
||||
return instances.get(domain);
|
||||
}
|
||||
|
||||
public Instance getInstanceInfoForAccount(String account) {
|
||||
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
|
||||
}
|
||||
|
||||
public void updateAccountInfo(String id, Account account){
|
||||
AccountSession session=getAccount(id);
|
||||
session.self=account;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
@@ -23,6 +20,7 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
@@ -64,7 +62,12 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList());
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
result=result.stream().filter(status -> {
|
||||
// don't hide own posts in own profile
|
||||
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
|
||||
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
|
||||
}).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -84,7 +87,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user))
|
||||
return;
|
||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||
@@ -92,10 +96,11 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
return;
|
||||
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
||||
if(ev.status.mediaAttachments.isEmpty())
|
||||
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
|
||||
return;
|
||||
}
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
if (isOnTop()) scrollToTop();
|
||||
}
|
||||
|
||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||
@@ -122,4 +127,19 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
// could return different uris based on filter (e.g. media -> "/media"), but i want to
|
||||
// return the remote url to the user, and i don't know whether i'd need to append
|
||||
// '#media' (akkoma/pleroma) or '/media' (glitch/mastodon) since i don't know anything
|
||||
// about the remote instance. so, just returning the base url to the user instead
|
||||
return Uri.parse(user.url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -103,4 +104,9 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? base.path("/announcements").build() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
@@ -33,6 +34,7 @@ import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
@@ -45,6 +47,7 @@ import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -68,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{
|
||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||
protected DisplayItemsAdapter adapter;
|
||||
protected String accountID;
|
||||
@@ -79,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected Rect tmpRect=new Rect();
|
||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||
protected boolean currentlyScrolling;
|
||||
|
||||
public BaseStatusListFragment(){
|
||||
super(20);
|
||||
@@ -92,7 +96,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
UiUtils.loadMaxWidth(getContext());
|
||||
if(GlobalUserPreferences.disableMarquee){
|
||||
setTitleMarqueeEnabled(false);
|
||||
setSubtitleMarqueeEnabled(false);
|
||||
@@ -129,7 +132,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
displayItems.clear();
|
||||
}
|
||||
|
||||
protected void prependItems(List<T> items, boolean notify){
|
||||
protected int prependItems(List<T> items, boolean notify){
|
||||
data.addAll(0, items);
|
||||
int offset=0;
|
||||
for(T s:items){
|
||||
@@ -142,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
if(notify)
|
||||
adapter.notifyItemRangeInserted(0, offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected String getMaxID(){
|
||||
@@ -202,7 +206,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||
MediaAttachmentViewController holder=findPhotoViewHolder(index);
|
||||
if(holder!=null){
|
||||
if(holder!=null && list!=null){
|
||||
transitioningHolder=holder;
|
||||
View view=transitioningHolder.photo;
|
||||
int[] pos={0, 0};
|
||||
@@ -268,34 +272,43 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getFab() {
|
||||
if (getParentFragment() instanceof HasFab l) return l.getFab();
|
||||
else return fab;
|
||||
}
|
||||
|
||||
public void animateFab(boolean show) {
|
||||
@Override
|
||||
public void showFab() {
|
||||
View fab = getFab();
|
||||
if (fab == null) return;
|
||||
if (show && fab.getVisibility() != View.VISIBLE) {
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
} else if (!show && fab.getVisibility() == View.VISIBLE) {
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
scrollDiff = 0;
|
||||
}
|
||||
if (fab == null || fab.getVisibility() == View.VISIBLE) return;
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2,
|
||||
0);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
}
|
||||
|
||||
public boolean isScrolling() {
|
||||
return currentlyScrolling;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideFab() {
|
||||
View fab = getFab();
|
||||
if (fab == null || fab.getVisibility() != View.VISIBLE) return;
|
||||
TranslateAnimation animate = new TranslateAnimation(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
fab.getHeight() * 2);
|
||||
animate.setDuration(300);
|
||||
fab.startAnimation(animate);
|
||||
fab.setVisibility(View.INVISIBLE);
|
||||
scrollDiff = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -310,12 +323,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
currentPhotoViewer.offsetView(-dx, -dy);
|
||||
|
||||
View fab = getFab();
|
||||
if (fab!=null && GlobalUserPreferences.autoHideFab) {
|
||||
if (fab!=null && GlobalUserPreferences.autoHideFab && dy != UiUtils.SCROLL_TO_TOP_DELTA) {
|
||||
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
||||
animateFab(false);
|
||||
hideFab();
|
||||
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
||||
if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) {
|
||||
animateFab(true);
|
||||
showFab();
|
||||
scrollDiff = 0;
|
||||
} else {
|
||||
scrollDiff += Math.abs(dy);
|
||||
@@ -323,12 +336,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
super.onScrollStateChanged(recyclerView, newState);
|
||||
currentlyScrolling = newState != RecyclerView.SCROLL_STATE_IDLE;
|
||||
}
|
||||
});
|
||||
list.addItemDecoration(new StatusListItemDecoration());
|
||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||
private Rect tmpRect=new Rect();
|
||||
@Override
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||
int lastIndex = -1, firstIndex = -1;
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
@@ -340,18 +361,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
View child=list.getChildAt(i);
|
||||
holder=list.getChildViewHolder(child);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> h){
|
||||
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
|
||||
if(otherID.equals(id)){
|
||||
if (firstIndex < 0) firstIndex = i;
|
||||
lastIndex = i;
|
||||
StatusDisplayItem item = h.getItem();
|
||||
hasDescendant = item.hasDescendantNeighbor;
|
||||
// no for direct descendants because main status (right above) is
|
||||
// being displayed with an extended footer - no connected layout
|
||||
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
|
||||
list.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
outRect.left=Math.min(outRect.left, tmpRect.left);
|
||||
outRect.top=Math.min(outRect.top, tmpRect.top);
|
||||
outRect.right=Math.max(outRect.right, tmpRect.right);
|
||||
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
|
||||
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
|
||||
isWarning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// shifting the selection box down
|
||||
// see also: FooterStatusDisplayItem#onBind (setMargins)
|
||||
if (isWarning || firstIndex < 0 || lastIndex < 0 ||
|
||||
!(list.getChildViewHolder(list.getChildAt(lastIndex))
|
||||
instanceof FooterStatusDisplayItem.Holder)) return;
|
||||
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
|
||||
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
|
||||
list.getChildViewHolder(list.getChildAt(prevIndex))
|
||||
instanceof WarningFilteredStatusDisplayItem.Holder;
|
||||
boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() &&
|
||||
list.getChildViewHolder(list.getChildAt(nextIndex))
|
||||
instanceof WarningFilteredStatusDisplayItem.Holder;
|
||||
if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4);
|
||||
if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4);
|
||||
}
|
||||
});
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
@@ -546,6 +591,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
|
||||
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
|
||||
holder.rebind();
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
|
||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||
@@ -557,6 +610,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
warning.getItem().status.filterRevealed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID(){
|
||||
return accountID;
|
||||
}
|
||||
@@ -626,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
protected int getListWidthForMediaLayout(){
|
||||
return list.getWidth();
|
||||
}
|
||||
@@ -690,6 +749,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
@@ -751,6 +814,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
||||
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
||||
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
|
||||
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -35,4 +37,14 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/bookmarks").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
@@ -179,8 +180,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private int charCount, charLimit, trimmedCharCount;
|
||||
|
||||
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
|
||||
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
|
||||
private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss, contentTypeBtn;
|
||||
private ImageView sensitiveIcon;
|
||||
private ComposeMediaLayout attachmentsView;
|
||||
private TextView replyText;
|
||||
@@ -234,6 +235,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private Runnable updateUploadEtaRunnable;
|
||||
|
||||
private String language, encoding;
|
||||
private ContentType contentType;
|
||||
private MastodonLanguage.LanguageResolver languageResolver;
|
||||
|
||||
@Override
|
||||
@@ -242,6 +244,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
setRetainInstance(true);
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
|
||||
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
self=session.self;
|
||||
instanceDomain=session.domain;
|
||||
@@ -259,9 +263,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Nav.finish(this);
|
||||
return;
|
||||
}
|
||||
if(customEmojis.isEmpty()){
|
||||
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
|
||||
}
|
||||
|
||||
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
|
||||
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
|
||||
@@ -330,6 +331,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
emojiBtn=view.findViewById(R.id.btn_emoji);
|
||||
spoilerBtn=view.findViewById(R.id.btn_spoiler);
|
||||
visibilityBtn=view.findViewById(R.id.btn_visibility);
|
||||
contentTypeBtn=view.findViewById(R.id.btn_content_type);
|
||||
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
|
||||
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
|
||||
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
|
||||
@@ -364,6 +366,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
|
||||
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
|
||||
|
||||
buildContentTypePopup(contentTypeBtn);
|
||||
contentTypeBtn.setOnClickListener(v->contentTypePopup.show());
|
||||
contentTypeBtn.setOnTouchListener(contentTypePopup.getDragToOpenListener());
|
||||
|
||||
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
|
||||
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
|
||||
|
||||
@@ -466,8 +472,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
if(editingStatus!=null && editingStatus.visibility!=null) {
|
||||
statusVisibility=editingStatus.visibility;
|
||||
if (savedInstanceState != null) {
|
||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
} else if (editingStatus != null && editingStatus.visibility != null) {
|
||||
statusVisibility = editingStatus.visibility;
|
||||
} else {
|
||||
loadDefaultStatusVisibility(savedInstanceState);
|
||||
}
|
||||
@@ -482,6 +490,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}).setChecked(true);
|
||||
visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly);
|
||||
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey("contentType")) {
|
||||
contentType = (ContentType) savedInstanceState.getSerializable("contentType");
|
||||
} else if (getArguments().containsKey("sourceContentType")) {
|
||||
try {
|
||||
String val = getArguments().getString("sourceContentType");
|
||||
contentType = val == null ? null : ContentType.valueOf(val);
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
|
||||
int contentTypeId = ContentType.getContentTypeRes(contentType);
|
||||
contentTypePopup.getMenu().findItem(contentTypeId).setChecked(true);
|
||||
contentTypeBtn.setSelected(contentTypeId != R.id.content_type_null && contentTypeId != R.id.content_type_plain);
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
@@ -518,6 +540,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
outState.putParcelableArrayList("attachments", serializedAttachments);
|
||||
}
|
||||
outState.putSerializable("visibility", statusVisibility);
|
||||
outState.putSerializable("contentType", contentType);
|
||||
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
|
||||
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
|
||||
}
|
||||
@@ -556,8 +579,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
if(s.length()==0)
|
||||
if(s.length()==0){
|
||||
updateCharCounter();
|
||||
return;
|
||||
}
|
||||
int start=lastChangeStart;
|
||||
int count=lastChangeCount;
|
||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||
@@ -907,6 +932,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
});
|
||||
}
|
||||
|
||||
private int getContentTypeName(String id) {
|
||||
return switch (id) {
|
||||
case "text/plain" -> R.string.sk_content_type_plain;
|
||||
case "text/html" -> R.string.sk_content_type_html;
|
||||
case "text/markdown" -> R.string.sk_content_type_markdown;
|
||||
case "text/bbcode" -> R.string.sk_content_type_bbcode;
|
||||
case "text/x.misskeymarkdown" -> R.string.sk_content_type_mfm;
|
||||
default -> throw new IllegalArgumentException("Invalid content type");
|
||||
};
|
||||
}
|
||||
|
||||
private void addBottomLanguage(Menu menu) {
|
||||
if (menu.findItem(allLanguages.size()) == null) {
|
||||
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
|
||||
@@ -1050,9 +1086,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
req.status=text;
|
||||
req.localOnly=localOnly;
|
||||
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
|
||||
req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
|
||||
req.sensitive=sensitive;
|
||||
req.language=language;
|
||||
req.contentType=contentType;
|
||||
req.scheduledAt = scheduledAt;
|
||||
if(!attachments.isEmpty()){
|
||||
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
||||
@@ -1705,11 +1742,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollChanged=true;
|
||||
updatePublishButtonState();
|
||||
}));
|
||||
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
|
||||
|
||||
int maxCharactersPerOption = 50;
|
||||
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
|
||||
maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption;
|
||||
else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
|
||||
maxCharactersPerOption = instance.pollLimits.maxOptionChars;
|
||||
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)});
|
||||
|
||||
pollOptionsView.addView(option.view);
|
||||
pollOptions.add(option);
|
||||
if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
|
||||
|
||||
int maxPollOptions = 4;
|
||||
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
|
||||
maxPollOptions = instance.configuration.polls.maxOptions;
|
||||
else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
|
||||
maxPollOptions = instance.pollLimits.maxOptions;
|
||||
|
||||
if(pollOptions.size()==maxPollOptions)
|
||||
addPollOptionBtn.setVisibility(View.GONE);
|
||||
return option;
|
||||
}
|
||||
@@ -1851,7 +1901,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Menu m=visibilityPopup.getMenu();
|
||||
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
||||
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||
if (instance.pleroma != null) {
|
||||
if (instance.isAkkoma()) {
|
||||
m.findItem(R.id.vis_local).setVisible(true);
|
||||
} else if (localOnly || prefsSaysSupported) {
|
||||
localOnlyItem.setVisible(true);
|
||||
@@ -1895,14 +1945,36 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void buildContentTypePopup(View btn) {
|
||||
contentTypePopup=new PopupMenu(getActivity(), btn);
|
||||
contentTypePopup.inflate(R.menu.compose_content_type);
|
||||
Menu m = contentTypePopup.getMenu();
|
||||
ContentType.adaptMenuToInstance(m, instance);
|
||||
if (contentType != null) m.findItem(R.id.content_type_null).setVisible(false);
|
||||
|
||||
contentTypePopup.setOnMenuItemClickListener(i->{
|
||||
int id=i.getItemId();
|
||||
if (id == R.id.content_type_null) contentType = null;
|
||||
else if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
|
||||
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
|
||||
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
|
||||
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
|
||||
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
|
||||
else return false;
|
||||
btn.setSelected(id != R.id.content_type_null && id != R.id.content_type_plain);
|
||||
i.setChecked(true);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
|
||||
btn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
|
||||
if(replyTo != null) statusVisibility = replyTo.visibility;
|
||||
|
||||
// A saved privacy setting from a previous compose session wins over the reply visibility
|
||||
if(savedInstanceState !=null){
|
||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
Preferences prefs = asm.getAccount(accountID).preferences;
|
||||
if (prefs != null) {
|
||||
|
||||
@@ -30,8 +30,11 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
@@ -164,7 +167,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
@@ -190,7 +193,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
|
||||
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
@@ -35,4 +37,16 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.ACCOUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.encodedPath(isInstanceAkkoma()
|
||||
? '/' + getSession().self.username + "#favorites"
|
||||
: "/favourites").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
@@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
|
||||
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
@@ -148,6 +150,16 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma() ? "/friend-requests" : "/follow_requests").build();
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public AccountsAdapter(){
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop {
|
||||
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String nextMaxID;
|
||||
private String accountId;
|
||||
private String accountID;
|
||||
|
||||
public FollowedHashtagsFragment() {
|
||||
super(20);
|
||||
@@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
accountID=args.getString("account");
|
||||
setTitle(R.string.sk_hashtags_you_follow);
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,6 +77,16 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? null : base.path("/followed_tags").build();
|
||||
}
|
||||
|
||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -109,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface HasAccountID {
|
||||
String getAccountID();
|
||||
|
||||
default AccountSession getSession() {
|
||||
return AccountSessionManager.getInstance().getAccount(getAccountID());
|
||||
}
|
||||
|
||||
default boolean isInstanceAkkoma() {
|
||||
return getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
}
|
||||
|
||||
default Optional<Instance> getInstance() {
|
||||
return getSession().getInstance();
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,7 @@ import android.view.View;
|
||||
|
||||
public interface HasFab {
|
||||
View getFab();
|
||||
void showFab();
|
||||
void hideFab();
|
||||
boolean isScrolling();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.Menu;
|
||||
@@ -8,7 +9,6 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -123,7 +123,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -154,4 +154,14 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -16,6 +17,11 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
@@ -30,16 +36,13 @@ import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
import java.util.Optional;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -52,7 +55,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID {
|
||||
private FragmentRootLinearLayout content;
|
||||
private HomeTabFragment homeTabFragment;
|
||||
private NotificationsFragment notificationsFragment;
|
||||
@@ -66,6 +69,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
private int currentTab=R.id.tab_home;
|
||||
|
||||
private String accountID;
|
||||
private boolean isPleroma;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -73,16 +77,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
E.register(this);
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.sk_app_name);
|
||||
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
|
||||
.map(Instance::isAkkoma)
|
||||
.orElse(false);
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
// TODO: clean up
|
||||
if(savedInstanceState==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
homeTabFragment=new HomeTabFragment();
|
||||
homeTabFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("disableDiscover", isPleroma);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
searchFragment.setArguments(args);
|
||||
@@ -160,6 +169,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||
currentTab=savedInstanceState.getInt("selectedTab");
|
||||
tabBar.selectTab(currentTab);
|
||||
Fragment current=fragmentForTab(currentTab);
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.hide(homeTabFragment)
|
||||
@@ -216,6 +226,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
public void setCurrentTab(@IdRes int tab){
|
||||
if(tab==currentTab)
|
||||
return;
|
||||
tabBar.selectTab(tab);
|
||||
onTabSelected(tab);
|
||||
}
|
||||
|
||||
private void onTabSelected(@IdRes int tab){
|
||||
Fragment newFragment=fragmentForTab(tab);
|
||||
if(tab==currentTab){
|
||||
@@ -227,8 +244,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
}
|
||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||
maybeTriggerLoading(newFragment);
|
||||
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
|
||||
currentTab=tab;
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
|
||||
}
|
||||
|
||||
private void maybeTriggerLoading(Fragment newFragment){
|
||||
@@ -255,7 +274,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new AccountSwitcherSheet(getActivity()).show();
|
||||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -288,10 +307,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
public void updateNotificationBadge() {
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
if (instance == null) return;
|
||||
Optional<Instance> instance = session.getInstance();
|
||||
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
|
||||
|
||||
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
|
||||
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Notification> notifications) {
|
||||
@@ -328,4 +347,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
|
||||
setNotificationBadge(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -54,6 +55,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -71,7 +73,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab {
|
||||
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
|
||||
private static final int ANNOUNCEMENTS_RESULT = 654;
|
||||
|
||||
private String accountID;
|
||||
@@ -106,7 +108,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
super.onCreate(savedInstanceState);
|
||||
E.register(this);
|
||||
accountID = getArguments().getString("account");
|
||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
|
||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
|
||||
assert timelineDefinitions != null;
|
||||
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
|
||||
count = timelineDefinitions.size();
|
||||
@@ -448,10 +450,26 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
updateSwitcherIcon(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFab() {
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.showFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideFab() {
|
||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolling() {
|
||||
return (fragments[pager.getCurrentItem()] instanceof HasFab fabulous)
|
||||
&& fabulous.isScrolling();
|
||||
}
|
||||
|
||||
private void updateSwitcherIcon(int i) {
|
||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||
if (fragments[i] instanceof BaseStatusListFragment<?> l) l.animateFab(true);
|
||||
showFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -683,6 +701,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
return fab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
|
||||
}
|
||||
|
||||
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -8,8 +9,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -150,7 +149,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
@@ -229,7 +228,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
|
||||
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
@@ -282,4 +281,14 @@ public class HomeTimelineFragment extends StatusListFragment {
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.HOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -162,4 +162,15 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
protected void onSetFabBottomInset(int inset) {
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.HOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/lists/" + listID).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListTimelinesFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop {
|
||||
private String accountId;
|
||||
private String profileAccountId;
|
||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
private final HashMap<String, Boolean> userInList = new HashMap<>();
|
||||
private ListsAdapter adapter;
|
||||
|
||||
public ListTimelinesFragment() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
setHasOptionsMenu(true);
|
||||
E.register(this);
|
||||
|
||||
if(args.containsKey("profileAccount")){
|
||||
profileAccountId=args.getString("profileAccount");
|
||||
String profileDisplayUsername = args.getString("profileDisplayUsername");
|
||||
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
|
||||
} else {
|
||||
setTitle(R.string.sk_your_lists);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.create) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(getContext());
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_create_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountId)
|
||||
)
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||
req.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
userInListBefore.clear();
|
||||
userInList.clear();
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
if (getActivity() == null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
if (getActivity() == null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
}
|
||||
}
|
||||
userInList.putAll(userInListBefore);
|
||||
onDataLoaded(newLists, false);
|
||||
}
|
||||
}).exec(accountId);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
item.repliesPolicy = event.repliesPolicy;
|
||||
adapter.notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
|
||||
return adapter = new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ListViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final CheckBox listToggle;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
listToggle=findViewById(R.id.list_toggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
|
||||
listToggle.setOnClickListener(this::onClickToggle);
|
||||
} else {
|
||||
listToggle.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickToggle(View view) {
|
||||
saveListMembership(item.id, listToggle.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountId);
|
||||
args.putString("listID", item.id);
|
||||
args.putString("listTitle", item.title);
|
||||
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private String profileAccountId;
|
||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
private final HashMap<String, Boolean> userInList = new HashMap<>();
|
||||
private ListsAdapter adapter;
|
||||
|
||||
public ListsFragment() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args = getArguments();
|
||||
accountID = args.getString("account");
|
||||
setHasOptionsMenu(true);
|
||||
E.register(this);
|
||||
|
||||
if(args.containsKey("profileAccount")){
|
||||
profileAccountId=args.getString("profileAccount");
|
||||
String profileDisplayUsername = args.getString("profileDisplayUsername");
|
||||
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
|
||||
} else {
|
||||
setTitle(R.string.sk_your_lists);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.create) {
|
||||
ListTimelineEditor editor = new ListTimelineEditor(getContext());
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_create_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID)
|
||||
)
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {})
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||
req.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
userInListBefore.clear();
|
||||
userInList.clear();
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
if (getActivity() == null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
if (getActivity() == null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
}
|
||||
}
|
||||
userInList.putAll(userInListBefore);
|
||||
onDataLoaded(newLists, false);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
item.repliesPolicy = event.repliesPolicy;
|
||||
adapter.notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
|
||||
return adapter = new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path("/lists").build();
|
||||
}
|
||||
|
||||
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ListViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final CheckBox listToggle;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
listToggle=findViewById(R.id.list_toggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
|
||||
listToggle.setOnClickListener(this::onClickToggle);
|
||||
} else {
|
||||
listToggle.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickToggle(View view) {
|
||||
saveListMembership(item.id, listToggle.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("listID", item.id);
|
||||
args.putString("listTitle", item.title);
|
||||
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,9 @@ public abstract class MastodonToolbarFragment extends ToolbarFragment{
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted() {
|
||||
return false; // else, badged icons don't work :(
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -13,6 +14,12 @@ import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -24,12 +31,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
@@ -47,7 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -227,6 +228,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -10,17 +11,23 @@ import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Markers;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
@@ -151,12 +158,17 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
loadRelationships(needRelationships);
|
||||
maxID=result.maxID;
|
||||
|
||||
if(offset==0 && !result.items.isEmpty() && !result.isFromCache()){
|
||||
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
|
||||
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
|
||||
E.post(new AllNotificationsSeenEvent());
|
||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).markers
|
||||
.notifications.lastReadId = result.items.get(0).id;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
|
||||
if (isInstanceAkkoma()) {
|
||||
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -183,23 +195,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Notification n=getNotificationByID(id);
|
||||
if(n.status!=null){
|
||||
Status status=n.status;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}else if(n.report != null){
|
||||
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
|
||||
}else{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
if(n.status != null && n.status.inReplyToAccountId != null && knownAccounts.containsKey(n.status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(n.status.inReplyToAccountId)));
|
||||
UiUtils.showFragmentForNotification(getContext(), n, accountID, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -231,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
|
||||
// copied from StatusListFragment.EventListener (just like the method above)
|
||||
// (which assumes this.data to be a list of statuses...)
|
||||
@Subscribe
|
||||
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
|
||||
for(Notification n:data){
|
||||
if (n.status == null) continue;
|
||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||
n.status.getContentStatus().update(ev);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||
footer.rebind();
|
||||
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||
footer.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Notification n:preloadedData){
|
||||
if (n.status == null) continue;
|
||||
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||
n.status.getContentStatus().update(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||
@@ -263,4 +288,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma()
|
||||
? "/users/" + getSession().self.username + "/interactions"
|
||||
: "/notifications").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
|
||||
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
@@ -31,7 +32,6 @@ import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -45,6 +45,7 @@ import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
@@ -74,6 +75,7 @@ import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -91,6 +93,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -110,7 +113,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||
private static final int AVATAR_RESULT=722;
|
||||
private static final int COVER_RESULT=343;
|
||||
|
||||
@@ -135,8 +138,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private TextView followsYouView;
|
||||
private ViewGroup rolesView;
|
||||
|
||||
private Account account;
|
||||
private Account account, remoteAccount;
|
||||
private String accountID;
|
||||
private String domain;
|
||||
private Relationship relationship;
|
||||
private int statusBarHeight;
|
||||
private boolean isOwnProfile;
|
||||
@@ -151,7 +155,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private PhotoViewer currentPhotoViewer;
|
||||
private boolean editModeLoading;
|
||||
|
||||
private static final int MAX_FIELDS=4;
|
||||
private int maxFields = 4;
|
||||
|
||||
// from ProfileAboutFragment
|
||||
public UsableRecyclerView list;
|
||||
@@ -172,13 +176,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
setRetainInstance(true);
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
if(getArguments().containsKey("profileAccount")){
|
||||
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
if (getArguments().containsKey("remoteAccount")) {
|
||||
remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||
if(!getArguments().getBoolean("noAutoLoad", false))
|
||||
loadData();
|
||||
} else if(getArguments().containsKey("profileAccount")){
|
||||
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
profileAccountID=account.id;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
loaded=true;
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else if (isInstanceAkkoma() && getInstance().isPresent())
|
||||
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
||||
}else{
|
||||
profileAccountID=getArguments().getString("profileAccountID");
|
||||
if(!getArguments().getBoolean("noAutoLoad", false))
|
||||
@@ -324,7 +335,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
username.setOnLongClickListener(v->{
|
||||
String usernameString=account.acct;
|
||||
if(!usernameString.contains("@")){
|
||||
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
usernameString+="@"+domain;
|
||||
}
|
||||
UiUtils.copyText(username, '@'+usernameString);
|
||||
return true;
|
||||
@@ -341,36 +352,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
private void onAccountLoaded(Account result) {
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
bindHeaderView();
|
||||
dataLoaded();
|
||||
if(!tabLayoutMediator.isAttached())
|
||||
tabLayoutMediator.attach();
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
if(refreshing){
|
||||
refreshing=false;
|
||||
refreshLayout.setRefreshing(false);
|
||||
if(postsFragment.loaded)
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(pinnedPostsFragment.loaded)
|
||||
pinnedPostsFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
}
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
if (remoteAccount != null) {
|
||||
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
|
||||
if (getContext() == null) return;
|
||||
if (args == null || !args.containsKey("profileAccount")) {
|
||||
onError(new MastodonErrorResponse(
|
||||
getContext().getString(R.string.sk_error_loading_profile),
|
||||
0, null
|
||||
));
|
||||
return;
|
||||
}
|
||||
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
currentRequest=new GetAccountByID(profileAccountID)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
if (getActivity() == null) return;
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
bindHeaderView();
|
||||
dataLoaded();
|
||||
if(!tabLayoutMediator.isAttached())
|
||||
tabLayoutMediator.attach();
|
||||
if(!isOwnProfile)
|
||||
loadRelationship();
|
||||
else
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
if(refreshing){
|
||||
refreshing=false;
|
||||
refreshLayout.setRefreshing(false);
|
||||
if(postsFragment.loaded)
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(pinnedPostsFragment.loaded)
|
||||
pinnedPostsFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
}
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
onAccountLoaded(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -484,9 +514,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
if (account.roles != null && !account.roles.isEmpty()) {
|
||||
rolesView.setVisibility(View.VISIBLE);
|
||||
@@ -505,13 +535,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
String acct = ((isSelf || account.isRemote)
|
||||
? account.getFullyQualifiedName()
|
||||
: account.acct);
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
if(isSelf){
|
||||
ssb.append('@');
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb.append(acct);
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
@@ -520,7 +549,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
// noinspection SetTextI18n
|
||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||
username.setText('@'+acct);
|
||||
}
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
@@ -590,6 +619,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
if(isOwnProfile && isInEditMode){
|
||||
@@ -703,7 +737,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
args.putString("profileAccount", profileAccountID);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
}
|
||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
||||
Nav.go(getActivity(), ListsFragment.class, args);
|
||||
}else if(id==R.id.followed_hashtags){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -758,6 +792,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return fab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFab() {
|
||||
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.showFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideFab() {
|
||||
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrolling() {
|
||||
return getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous
|
||||
&& fabulous.isScrolling();
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||
if(scrollY>avatarBorder.getTop()-topBarsH){
|
||||
@@ -1149,6 +1199,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if (adapter != null) adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(account.url);
|
||||
}
|
||||
|
||||
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
|
||||
public MetadataAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -1179,7 +1244,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
public int getItemCount(){
|
||||
if(isInEditMode){
|
||||
int size=metadataListData.size();
|
||||
if(size<MAX_FIELDS)
|
||||
if(size<maxFields)
|
||||
size++;
|
||||
return size;
|
||||
}
|
||||
@@ -1296,7 +1361,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onClick(){
|
||||
metadataListData.add(new AccountField());
|
||||
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
|
||||
if(metadataListData.size()==maxFields){ // replace this row with new row
|
||||
adapter.notifyItemChanged(metadataListData.size()-1);
|
||||
}else{
|
||||
adapter.notifyItemInserted(metadataListData.size()-1);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
@@ -79,7 +80,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true);
|
||||
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,6 +97,8 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
args.putString("sourceText", status.text);
|
||||
args.putString("sourceSpoiler", status.spoilerText);
|
||||
args.putBoolean("redraftStatus", true);
|
||||
args.putString("sourceContentType", scheduledStatus.params.contentType != null ?
|
||||
scheduledStatus.params.contentType.name() : null);
|
||||
setResult(true, null);
|
||||
|
||||
// closing this scheduled status list if another status list is opened from compose fragment
|
||||
@@ -179,4 +182,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
// TODO: adapt when frontends finally implement a scheduled posts list
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package org.joinmastodon.android.fragments;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
public interface ScrollableToTop{
|
||||
void scrollToTop();
|
||||
@@ -21,7 +22,7 @@ public interface ScrollableToTop{
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
list.scrollBy(0, V.dp(300));
|
||||
list.scrollBy(0, UiUtils.SCROLL_TO_TOP_DELTA);
|
||||
list.smoothScrollToPosition(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.LruCache;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -48,6 +49,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
@@ -57,13 +59,16 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -75,12 +80,13 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SettingsFragment extends MastodonToolbarFragment{
|
||||
public class SettingsFragment extends MastodonToolbarFragment implements ProvidesAssistContent.ProvidesWebUri {
|
||||
private UsableRecyclerView list;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private ThemeItem themeItem;
|
||||
private NotificationPolicyItem notificationPolicyItem;
|
||||
private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem;
|
||||
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
|
||||
private ButtonItem defaultContentTypeButtonItem;
|
||||
private String accountID;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean needAppRestart;
|
||||
@@ -89,7 +95,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private ImageView themeTransitionWindowView;
|
||||
private TextItem checkForUpdateItem, clearImageCacheItem;
|
||||
private ImageCache imageCache;
|
||||
private Menu contentTypeMenu;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -99,7 +107,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
imageCache = ImageCache.getInstance(getActivity());
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
Optional<Instance> instance = session.getInstance();
|
||||
String instanceName = UiUtils.getInstanceName(accountID);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
@@ -211,13 +219,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
|
||||
GlobalUserPreferences.allowRemoteLoading=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_timelines));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
GlobalUserPreferences.showReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
if (instance.pleroma != null) {
|
||||
if (isInstanceAkkoma()) {
|
||||
items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.reply_visibility);
|
||||
@@ -239,15 +252,15 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
showNewPostsButtonItem.enabled = i.checked;
|
||||
showNewPostsItem.enabled = i.checked;
|
||||
if (!i.checked) {
|
||||
GlobalUserPreferences.showNewPostsButton = false;
|
||||
showNewPostsButtonItem.checked = false;
|
||||
showNewPostsItem.checked = false;
|
||||
}
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsButtonItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(showNewPostsButtonItem = new SwitchItem(R.string.sk_settings_see_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{
|
||||
items.add(showNewPostsItem = new SwitchItem(R.string.sk_settings_see_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{
|
||||
GlobalUserPreferences.showNewPostsButton=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
@@ -293,7 +306,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
|
||||
boolean translationAvailable = instance
|
||||
.map(i -> i.v2 != null && i.v2.configuration.translation != null && i.v2.configuration.translation.enabled)
|
||||
.orElse(false);
|
||||
items.add(new SmallTextItem(getString(translationAvailable ?
|
||||
R.string.sk_settings_translation_availability_note_available :
|
||||
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
|
||||
@@ -318,23 +333,55 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
|
||||
|
||||
items.add(new HeaderItem(instanceName));
|
||||
items.add(new TextItem(R.string.sk_settings_rules, ()->{
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
items.add(new TextItem(R.string.sk_settings_rules, instance.<Runnable>map(i -> () -> {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(i));
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
|
||||
}).orElse(null), R.drawable.ic_fluent_task_list_ltr_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
|
||||
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version)));
|
||||
items.add(new SmallTextItem(instance
|
||||
.map(i -> getString(R.string.sk_settings_server_version, i.version))
|
||||
.orElse(getString(R.string.sk_instance_info_unavailable))));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_instance_features));
|
||||
items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{
|
||||
if (i.checked) {
|
||||
GlobalUserPreferences.accountsWithContentTypesEnabled.add(accountID);
|
||||
if (GlobalUserPreferences.accountsDefaultContentTypes.get(accountID) == null) {
|
||||
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, ContentType.PLAIN);
|
||||
}
|
||||
} else {
|
||||
GlobalUserPreferences.accountsWithContentTypesEnabled.remove(accountID);
|
||||
GlobalUserPreferences.accountsDefaultContentTypes.remove(accountID);
|
||||
}
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(defaultContentTypeButtonItem))
|
||||
instanceof ButtonViewHolder bvh) bvh.rebind();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_content_types_explanation)));
|
||||
items.add(defaultContentTypeButtonItem = new ButtonItem(R.string.sk_settings_default_content_type, 0, b->{
|
||||
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.compose_content_type);
|
||||
popupMenu.setOnMenuItemClickListener(item -> this.onContentTypeChanged(item, b));
|
||||
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
b.setOnClickListener(v->popupMenu.show());
|
||||
ContentType contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
|
||||
b.setText(getContentTypeString(contentType));
|
||||
contentTypeMenu = popupMenu.getMenu();
|
||||
contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true);
|
||||
instance.ifPresent(i -> ContentType.adaptMenuToInstance(contentTypeMenu, i));
|
||||
}));
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation)));
|
||||
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
|
||||
glitchModeItem.enabled = i.checked;
|
||||
if (i.checked) {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
|
||||
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
if (!isInstanceAkkoma()) {
|
||||
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
}
|
||||
} else {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
|
||||
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
|
||||
@@ -500,6 +547,34 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private @StringRes int getContentTypeString(@Nullable ContentType contentType) {
|
||||
if (contentType == null) return R.string.sk_content_type_unspecified;
|
||||
return switch (contentType) {
|
||||
case PLAIN -> R.string.sk_content_type_plain;
|
||||
case HTML -> R.string.sk_content_type_html;
|
||||
case MARKDOWN -> R.string.sk_content_type_markdown;
|
||||
case BBCODE -> R.string.sk_content_type_bbcode;
|
||||
case MISSKEY_MARKDOWN -> R.string.sk_content_type_mfm;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean onContentTypeChanged(MenuItem item, Button btn){
|
||||
int id = item.getItemId();
|
||||
ContentType contentType = switch (id) {
|
||||
case R.id.content_type_plain -> ContentType.PLAIN;
|
||||
case R.id.content_type_html -> ContentType.HTML;
|
||||
case R.id.content_type_markdown -> ContentType.MARKDOWN;
|
||||
case R.id.content_type_bbcode -> ContentType.BBCODE;
|
||||
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
|
||||
default -> null;
|
||||
};
|
||||
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
|
||||
GlobalUserPreferences.save();
|
||||
btn.setText(getContentTypeString(contentType));
|
||||
item.setChecked(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onReplyVisibilityChanged(MenuItem item, Button btn){
|
||||
String pref = null;
|
||||
int id = item.getItemId();
|
||||
@@ -672,6 +747,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma() ? "/about" : "/settings").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
@@ -755,7 +840,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private class SmallTextItem extends Item {
|
||||
private String text;
|
||||
private final String text;
|
||||
|
||||
public SmallTextItem(@StringRes int text) {
|
||||
this.text = getString(text);
|
||||
}
|
||||
|
||||
public SmallTextItem(String text) {
|
||||
this.text = text;
|
||||
@@ -996,7 +1085,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private final ImageView icon;
|
||||
private final TextView text;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public ButtonViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_button, list);
|
||||
text=findViewById(R.id.text);
|
||||
@@ -1004,10 +1092,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
button=findViewById(R.id.button);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(ButtonItem item){
|
||||
text.setText(item.text);
|
||||
icon.setImageResource(item.icon);
|
||||
icon.setVisibility(item.icon == 0 ? View.GONE : View.VISIBLE);
|
||||
icon.setImageResource(item.icon == 0 ? 0 : item.icon);
|
||||
// reset listeners before letting the button consumer consume the button
|
||||
// (and potentially set some listeners, but not others)
|
||||
button.setOnTouchListener(null);
|
||||
button.setOnClickListener(null);
|
||||
button.setOnLongClickListener(null);
|
||||
item.buttonConsumer.accept(button);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
@@ -23,13 +25,13 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
private String id;
|
||||
|
||||
private String id, url;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
id=getArguments().getString("id");
|
||||
url=getArguments().getString("url");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null);
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
@@ -156,4 +158,14 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
|
||||
@@ -28,15 +29,17 @@ import java.util.stream.Stream;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
public abstract class StatusListFragment extends BaseStatusListFragment<Status> {
|
||||
protected EventListener eventListener=new EventListener();
|
||||
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
|
||||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME);
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
|
||||
}
|
||||
|
||||
protected abstract Filter.FilterContext getFilterContext();
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Status s){
|
||||
if(!knownAccounts.containsKey(s.account.id))
|
||||
@@ -65,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
status.filterRevealed = true;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
@@ -180,7 +183,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
|
||||
}
|
||||
|
||||
@@ -1,36 +1,55 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment{
|
||||
protected Status mainStatus;
|
||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||
protected Status mainStatus, updatedStatus;
|
||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||
protected boolean contextInitiallyRendered;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -47,13 +66,44 @@ public class ThreadFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
for(StatusDisplayItem item:items){
|
||||
// "what the fuck is a deque"? yes
|
||||
// (it's just so the last-added item automatically comes first when looping over it)
|
||||
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
|
||||
|
||||
// modifying hidden filtered items if status is displayed as a warning
|
||||
List<StatusDisplayItem> itemsToModify =
|
||||
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
|
||||
? warning.filteredItems
|
||||
: items;
|
||||
|
||||
for(int i = 0; i < itemsToModify.size(); i++){
|
||||
StatusDisplayItem item = itemsToModify.get(i);
|
||||
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
|
||||
if (ancestryInfo != null) {
|
||||
item.setAncestryInfo(
|
||||
ancestryInfo.descendantNeighbor != null,
|
||||
ancestryInfo.ancestoringNeighbor != null,
|
||||
s.id.equals(mainStatus.id),
|
||||
Optional.ofNullable(ancestryInfo.ancestoringNeighbor)
|
||||
.map(ancestor -> ancestor.id.equals(mainStatus.id))
|
||||
.orElse(false)
|
||||
);
|
||||
}
|
||||
|
||||
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
|
||||
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
|
||||
deleteTheseItems.add(i);
|
||||
}
|
||||
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
if(item instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
else if(item instanceof FooterStatusDisplayItem footer)
|
||||
footer.hideCounts=true;
|
||||
}
|
||||
}
|
||||
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||
if(s.id.equals(mainStatus.id)) {
|
||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
|
||||
}
|
||||
return items;
|
||||
@@ -61,43 +111,30 @@ public class ThreadFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
loadMainStatus();
|
||||
currentRequest=new GetStatusContext(mainStatus.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(StatusContext result){
|
||||
if (getActivity() == null) return;
|
||||
if (getContext() == null) return;
|
||||
if(refreshing){
|
||||
data.clear();
|
||||
ancestryMap.clear();
|
||||
displayItems.clear();
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
}
|
||||
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
|
||||
if(instance.pleroma != null){
|
||||
List<String> threadIds=new ArrayList<>();
|
||||
threadIds.add(mainStatus.id);
|
||||
for(Status s:result.descendants){
|
||||
if(threadIds.contains(s.inReplyToId)){
|
||||
threadIds.add(s.id);
|
||||
}
|
||||
}
|
||||
threadIds.add(mainStatus.inReplyToId);
|
||||
for(int i=result.ancestors.size()-1; i >= 0; i--){
|
||||
Status s=result.ancestors.get(i);
|
||||
if(s.inReplyToId != null && threadIds.contains(s.id)){
|
||||
threadIds.add(s.inReplyToId);
|
||||
}
|
||||
}
|
||||
|
||||
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
|
||||
result.descendants=getDescendantsOrdered(mainStatus.id,
|
||||
result.descendants.stream()
|
||||
.filter(s -> threadIds.contains(s.id))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
// TODO: figure out how this code works
|
||||
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
|
||||
|
||||
result.descendants=filterStatuses(result.descendants);
|
||||
result.ancestors=filterStatuses(result.ancestors);
|
||||
|
||||
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
|
||||
ancestryMap.put(i.status.id, i);
|
||||
}
|
||||
|
||||
if(footerProgress!=null)
|
||||
footerProgress.setVisibility(View.GONE);
|
||||
data.addAll(result.descendants);
|
||||
@@ -106,19 +143,120 @@ public class ThreadFragment extends StatusListFragment{
|
||||
int count=displayItems.size();
|
||||
if(!refreshing)
|
||||
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
|
||||
prependItems(result.ancestors, !refreshing);
|
||||
int prependedCount = prependItems(result.ancestors, !refreshing);
|
||||
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
|
||||
displayItems.remove(prependedCount);
|
||||
adapter.notifyItemRemoved(prependedCount);
|
||||
count--;
|
||||
}
|
||||
dataLoaded();
|
||||
if(refreshing){
|
||||
refreshDone();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
list.scrollToPosition(displayItems.size()-count);
|
||||
|
||||
// no animation is going to happen, so proceeding to apply right now
|
||||
if (data.size() == 1) {
|
||||
contextInitiallyRendered = true;
|
||||
// for the case that the main status has already finished loading
|
||||
maybeApplyMainStatus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<Status> getDescendantsOrdered(String id, List<Status> statuses){
|
||||
private void loadMainStatus() {
|
||||
new GetStatusByID(mainStatus.id)
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Status status) {
|
||||
if (getContext() == null || status == null) return;
|
||||
updatedStatus = status;
|
||||
// for the case that the context has already loaded (and the animation has
|
||||
// already finished), falling back to applying it ourselves:
|
||||
maybeApplyMainStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
protected Object maybeApplyMainStatus() {
|
||||
if (updatedStatus == null || !contextInitiallyRendered) return null;
|
||||
|
||||
// returning fired event object to facilitate testing
|
||||
Object event;
|
||||
if (updatedStatus.editedAt != null &&
|
||||
(mainStatus.editedAt == null ||
|
||||
updatedStatus.editedAt.isAfter(mainStatus.editedAt))) {
|
||||
event = new StatusUpdatedEvent(updatedStatus);
|
||||
} else {
|
||||
event = new StatusCountersUpdatedEvent(updatedStatus);
|
||||
}
|
||||
|
||||
mainStatus = updatedStatus;
|
||||
updatedStatus = null;
|
||||
E.post(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
|
||||
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
|
||||
|
||||
List<Status> statuses = new ArrayList<>(context.ancestors);
|
||||
statuses.add(mainStatus);
|
||||
statuses.addAll(context.descendants);
|
||||
|
||||
int count = statuses.size();
|
||||
for (int index = 0; index < count; index++) {
|
||||
Status current = statuses.get(index);
|
||||
ancestry.add(new NeighborAncestryInfo(
|
||||
current,
|
||||
// descendant neighbor
|
||||
Optional
|
||||
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
|
||||
.filter(s -> s.inReplyToId.equals(current.id))
|
||||
.orElse(null),
|
||||
// ancestoring neighbor
|
||||
Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
|
||||
.filter(ancestor -> Optional.ofNullable(ancestor.descendantNeighbor)
|
||||
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
|
||||
.orElse(false))
|
||||
.map(a -> a.status)
|
||||
.orElse(null)
|
||||
));
|
||||
}
|
||||
|
||||
return ancestry;
|
||||
}
|
||||
|
||||
public static void sortStatusContext(Status mainStatus, StatusContext context) {
|
||||
List<String> threadIds=new ArrayList<>();
|
||||
threadIds.add(mainStatus.id);
|
||||
for(Status s:context.descendants){
|
||||
if(threadIds.contains(s.inReplyToId)){
|
||||
threadIds.add(s.id);
|
||||
}
|
||||
}
|
||||
threadIds.add(mainStatus.inReplyToId);
|
||||
for(int i=context.ancestors.size()-1; i >= 0; i--){
|
||||
Status s=context.ancestors.get(i);
|
||||
if(s.inReplyToId != null && threadIds.contains(s.id)){
|
||||
threadIds.add(s.inReplyToId);
|
||||
}
|
||||
}
|
||||
|
||||
context.ancestors=context.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
|
||||
context.descendants=getDescendantsOrdered(mainStatus.id,
|
||||
context.descendants.stream()
|
||||
.filter(s -> threadIds.contains(s.id))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private static List<Status> getDescendantsOrdered(String id, List<Status> statuses){
|
||||
List<Status> out=new ArrayList<>();
|
||||
for(Status s:getDirectDescendants(id, statuses)){
|
||||
out.add(s);
|
||||
@@ -130,14 +268,14 @@ public class ThreadFragment extends StatusListFragment{
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<Status> getDirectDescendants(String id, List<Status> statuses){
|
||||
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
|
||||
return statuses.stream()
|
||||
.filter(s -> s.inReplyToId.equals(id))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
|
||||
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,getFilterContext());
|
||||
return statuses.stream()
|
||||
.filter(statusFilterPredicate)
|
||||
.collect(Collectors.toList());
|
||||
@@ -159,10 +297,21 @@ public class ThreadFragment extends StatusListFragment{
|
||||
showContent();
|
||||
if(!loaded)
|
||||
footerProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
list.setItemAnimator(new BetterItemAnimator() {
|
||||
@Override
|
||||
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.onAnimationFinished(viewHolder);
|
||||
contextInitiallyRendered = true;
|
||||
// for the case that both requests are already done (and thus won't apply it)
|
||||
maybeApplyMainStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
|
||||
data.add(ev.status);
|
||||
onAppendItems(Collections.singletonList(ev.status));
|
||||
}
|
||||
}
|
||||
@@ -181,4 +330,52 @@ public class ThreadFragment extends StatusListFragment{
|
||||
public boolean wantsLightNavigationBar(){
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.THREAD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(mainStatus.url);
|
||||
}
|
||||
|
||||
protected static class NeighborAncestryInfo {
|
||||
protected Status status, descendantNeighbor, ancestoringNeighbor;
|
||||
|
||||
protected NeighborAncestryInfo(@NonNull Status status, Status descendantNeighbor, Status ancestoringNeighbor) {
|
||||
this.status = status;
|
||||
this.descendantNeighbor = descendantNeighbor;
|
||||
this.ancestoringNeighbor = ancestoringNeighbor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
NeighborAncestryInfo that = (NeighborAncestryInfo) o;
|
||||
return status.equals(that.status)
|
||||
&& Objects.equals(descendantNeighbor, that.descendantNeighbor)
|
||||
&& Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onErrorRetryClick(){
|
||||
if(preloadingFailed){
|
||||
preloadingFailed=false;
|
||||
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
|
||||
V.setVisibilityAnimated(footerError, View.GONE);
|
||||
doLoadData();
|
||||
return;
|
||||
}
|
||||
super.onErrorRetryClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,68 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
|
||||
protected Account account;
|
||||
protected String initialSubtitle = "";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
if (getArguments().containsKey("remoteAccount")) {
|
||||
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||
}
|
||||
setTitle("@"+account.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma()
|
||||
? "/users/" + account.id
|
||||
: '@' + account.acct).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteDomain() {
|
||||
return account.getDomainFromURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getCurrentInfo() {
|
||||
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MastodonAPIRequest<Account> loadRemoteInfo() {
|
||||
return new GetAccountByHandle(account.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AccountSession getRemoteSession() {
|
||||
return Optional.ofNullable(remoteInfo)
|
||||
.map(AccountSessionManager.getInstance()::tryGetAccount)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoteLoadingFailed() {
|
||||
super.onRemoteLoadingFailed();
|
||||
String prefix = initialSubtitle == null ? "" :
|
||||
initialSubtitle + " " + getContext().getString(R.string.sk_separator) + " ";
|
||||
String str = prefix +
|
||||
getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain);
|
||||
setSubtitle(str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Animatable;
|
||||
@@ -23,7 +25,8 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||
import org.joinmastodon.android.fragments.HasAccountID;
|
||||
import org.joinmastodon.android.fragments.ListsFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.RecyclerFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
@@ -34,6 +37,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -44,6 +48,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
@@ -57,7 +62,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> {
|
||||
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
@@ -170,6 +175,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -230,10 +245,13 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(AccountItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
username.setText("@"+ (item.account.isRemote
|
||||
? item.account.getFullyQualifiedName()
|
||||
: item.account.acct));
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
@@ -269,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
|
||||
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -387,7 +406,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
args.putString("account", accountID);
|
||||
args.putString("profileAccount", account.id);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
||||
Nav.go(getActivity(), ListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -410,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof AccountItem i && i.account.url.equals(account.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -12,11 +13,17 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
|
||||
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetAccountFollowers(account.id, maxID, count);
|
||||
return new GetAccountFollowers(getCurrentInfo().id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return super.getWebUri(base).buildUpon()
|
||||
.appendPath(isInstanceAkkoma() ? "#followers" : "/followers").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -12,11 +13,17 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
|
||||
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetAccountFollowing(account.id, maxID, count);
|
||||
return new GetAccountFollowing(getCurrentInfo().id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return super.getWebUri(base).buildUpon()
|
||||
.appendPath(isInstanceAkkoma() ? "#followees" : "/following").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,173 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
|
||||
public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
|
||||
private String nextMaxID;
|
||||
private MastodonAPIRequest<T> remoteInfoRequest;
|
||||
protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled;
|
||||
protected int localOffset;
|
||||
protected T remoteInfo;
|
||||
|
||||
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
|
||||
|
||||
protected abstract MastodonAPIRequest<T> loadRemoteInfo();
|
||||
public abstract T getCurrentInfo();
|
||||
public abstract String getRemoteDomain();
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
// already have remote info (e.g. from arguments), so no need to fetch it again
|
||||
if (remoteInfo != null) {
|
||||
onRemoteInfoLoaded(remoteInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
remoteDisabled = !GlobalUserPreferences.allowRemoteLoading
|
||||
|| getSession().domain.equals(getRemoteDomain());
|
||||
if (!remoteDisabled) {
|
||||
remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
if (getContext() == null) return;
|
||||
onRemoteInfoLoaded(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
if (getContext() == null) return;
|
||||
onRemoteLoadingFailed();
|
||||
}
|
||||
});
|
||||
remoteInfoRequest.execRemote(getRemoteDomain(), getRemoteSession());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* override to provide an ideal account session (e.g. if you're logged into the author's remote
|
||||
* account) to make the remote request from. if null is provided, will try to get any session
|
||||
* on the remote domain, or tries the request without authentication.
|
||||
*/
|
||||
protected AccountSession getRemoteSession() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void onRemoteInfoLoaded(T info) {
|
||||
this.remoteInfo = info;
|
||||
this.remoteInfoRequest = null;
|
||||
maybeStartLoadingRemote();
|
||||
}
|
||||
|
||||
protected void onRemoteLoadingFailed() {
|
||||
this.remoteRequestFailed = true;
|
||||
this.remoteInfo = null;
|
||||
this.remoteInfoRequest = null;
|
||||
if (doneWithHomeInstance) dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dataLoaded() {
|
||||
super.dataLoaded();
|
||||
footerProgress.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void maybeStartLoadingRemote() {
|
||||
if (startedRemoteLoading || remoteDisabled) return;
|
||||
if (!remoteRequestFailed) {
|
||||
if (data.size() == 0) showProgress();
|
||||
else footerProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (doneWithHomeInstance && remoteInfo != null) {
|
||||
startedRemoteLoading = true;
|
||||
loadData(localOffset, itemsPerPage * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
localOffset = 0;
|
||||
doneWithHomeInstance = false;
|
||||
startedRemoteLoading = false;
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(int offset, int count) {
|
||||
// always subtract the amount loaded through the home instance once loading from remote
|
||||
// since loadData gets called with data.size() (data includes both local and remote)
|
||||
if (doneWithHomeInstance) offset -= localOffset;
|
||||
super.loadData(offset, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count)
|
||||
MastodonAPIRequest<?> request = onCreateRequest(offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
|
||||
Collection<AccountItem> d = justRefreshed ? List.of() : data;
|
||||
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
List<AccountItem> items = result.stream()
|
||||
.filter(a -> d.size() > 1000 || d.stream()
|
||||
.noneMatch(i -> i.account.url.equals(a.url)))
|
||||
.map(AccountItem::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
boolean hasMore = nextMaxID != null;
|
||||
|
||||
if (!hasMore && !doneWithHomeInstance) {
|
||||
// only runs last time data was fetched from the home instance
|
||||
localOffset = d.size() + items.size();
|
||||
doneWithHomeInstance = true;
|
||||
}
|
||||
|
||||
onDataLoaded(items, hasMore);
|
||||
if (doneWithHomeInstance) maybeStartLoadingRemote();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
if (doneWithHomeInstance) {
|
||||
onRemoteLoadingFailed();
|
||||
onDataLoaded(Collections.emptyList(), false);
|
||||
return;
|
||||
}
|
||||
super.onError(error);
|
||||
}
|
||||
});
|
||||
|
||||
if (doneWithHomeInstance && remoteInfo == null) return; // we are waiting
|
||||
if (doneWithHomeInstance && remoteInfo != null) {
|
||||
request.execRemote(getRemoteDomain(), getRemoteSession());
|
||||
} else {
|
||||
request.exec(accountID);
|
||||
}
|
||||
currentRequest = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
updateTitle(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTitle(Status status) {
|
||||
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetStatusFavorites(status.id, maxID, count);
|
||||
return new GetStatusFavorites(getCurrentInfo().id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
Uri statusUri = super.getWebUri(base);
|
||||
return isInstanceAkkoma()
|
||||
? statusUri
|
||||
: statusUri.buildUpon().appendPath("favourites").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
updateTitle(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTitle(Status status) {
|
||||
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetStatusReblogs(status.id, maxID, count);
|
||||
return new GetStatusReblogs(getCurrentInfo().id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
Uri statusUri = super.getWebUri(base);
|
||||
return isInstanceAkkoma()
|
||||
? statusUri
|
||||
: statusUri.buildUpon().appendPath("reblogs").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
|
||||
protected Status status;
|
||||
|
||||
protected abstract void updateTitle(Status status);
|
||||
|
||||
protected MastodonAPIRequest<Status> loadRemoteInfo() {
|
||||
String[] parts = status.url.split("/");
|
||||
if (parts.length == 0) return null;
|
||||
return new GetStatusByID(parts[parts.length - 1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -16,6 +32,46 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
|
||||
|
||||
@Override
|
||||
protected boolean hasSubtitle(){
|
||||
return false;
|
||||
return remoteRequestFailed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base
|
||||
.encodedPath(isInstanceAkkoma()
|
||||
? "/notice/" + status.id
|
||||
: '@' + status.account.acct + '/' + status.id)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteDomain() {
|
||||
return Uri.parse(status.url).getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status getCurrentInfo() {
|
||||
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : status;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AccountSession getRemoteSession() {
|
||||
return Optional.ofNullable(remoteInfo)
|
||||
.map(s -> s.account)
|
||||
.map(AccountSessionManager.getInstance()::tryGetAccount)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoteInfoLoaded(Status info) {
|
||||
super.onRemoteInfoLoaded(info);
|
||||
updateTitle(remoteInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoteLoadingFailed() {
|
||||
super.onRemoteLoadingFailed();
|
||||
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain));
|
||||
updateToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class BubbleTimelineFragment extends StatusListFragment {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
protected boolean wantsComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? base.path("/main/bubble").build() : null;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
@@ -27,6 +28,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -49,7 +51,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop {
|
||||
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
@@ -145,6 +147,16 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? null : base.path("/explore/suggestions").build();
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public AccountsAdapter(){
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
@@ -19,12 +20,14 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -36,7 +39,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop {
|
||||
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent {
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager2 pager;
|
||||
@@ -49,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private ProgressBar searchProgress;
|
||||
|
||||
private DiscoverPostsFragment postsFragment;
|
||||
private TrendingHashtagsFragment hashtagsFragment;
|
||||
private DiscoverHashtagsFragment hashtagsFragment;
|
||||
private DiscoverNewsFragment newsFragment;
|
||||
private DiscoverAccountsFragment accountsFragment;
|
||||
private SearchFragment searchFragment;
|
||||
@@ -117,7 +120,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
postsFragment=new DiscoverPostsFragment();
|
||||
postsFragment.setArguments(args);
|
||||
|
||||
hashtagsFragment=new TrendingHashtagsFragment();
|
||||
hashtagsFragment=new DiscoverHashtagsFragment();
|
||||
hashtagsFragment.setArguments(args);
|
||||
|
||||
newsFragment=new DiscoverNewsFragment();
|
||||
@@ -238,7 +241,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
else scrollToTop();
|
||||
}
|
||||
|
||||
private void selectSearch() {
|
||||
public void selectSearch() {
|
||||
searchEdit.requestFocus();
|
||||
onSearchEditFocusChanged(searchEdit, true);
|
||||
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
|
||||
@@ -272,6 +275,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
searchBack.setEnabled(false);
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
|
||||
if (getArguments().getBoolean("disableDiscover"))
|
||||
((HomeFragment) getParentFragment()).onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -318,6 +323,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
callFragmentToProvideAssistContent(searchActive
|
||||
? searchFragment
|
||||
: getFragmentForPage(pager.getCurrentItem()), assistContent);
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
|
||||
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -15,6 +19,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.HashtagChartView;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,11 +29,11 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop {
|
||||
public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
|
||||
|
||||
public TrendingHashtagsFragment(){
|
||||
public DiscoverHashtagsFragment(){
|
||||
super(10);
|
||||
}
|
||||
|
||||
@@ -73,6 +78,16 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
|
||||
}
|
||||
|
||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -105,6 +120,14 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
||||
@Override
|
||||
public void onBind(Hashtag item){
|
||||
title.setText('#'+item.name);
|
||||
if (item.history == null || item.history.isEmpty()) {
|
||||
subtitle.setText(null);
|
||||
chart.setVisibility(View.GONE);
|
||||
title.setLayoutParams(withoutHistoryParams);
|
||||
return;
|
||||
}
|
||||
chart.setVisibility(View.VISIBLE);
|
||||
title.setLayoutParams(withHistoryParams);
|
||||
int numPeople=item.history.get(0).accounts;
|
||||
if(item.history.size()>1)
|
||||
numPeople+=item.history.get(1).accounts;
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -19,6 +20,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -35,7 +37,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop {
|
||||
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
|
||||
@@ -88,6 +90,16 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
|
||||
return isRecyclerViewOnTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccountID() {
|
||||
return accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
|
||||
}
|
||||
|
||||
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public LinksAdapter(){
|
||||
super(imgLoader);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop {
|
||||
public class DiscoverPostsFragment extends StatusListFragment {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
|
||||
|
||||
@Override
|
||||
@@ -26,7 +26,7 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
}).exec(accountID);
|
||||
@@ -39,7 +39,12 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return isInstanceAkkoma() ? null : base.path("/explore/posts").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -25,7 +26,6 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
||||
@@ -35,7 +35,7 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -47,4 +47,14 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma() ? "/main/all" : "/public").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -24,7 +26,6 @@ public class LocalTimelineFragment extends StatusListFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
||||
@@ -34,7 +35,7 @@ public class LocalTimelineFragment extends StatusListFragment {
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -46,4 +47,14 @@ public class LocalTimelineFragment extends StatusListFragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return Filter.FilterContext.PUBLIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -11,10 +12,10 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
@@ -41,7 +42,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
|
||||
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
|
||||
private String currentQuery;
|
||||
private List<StatusDisplayItem> prevDisplayItems;
|
||||
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
||||
@@ -80,7 +81,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
|
||||
return switch(s.type){
|
||||
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
|
||||
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null);
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.PUBLIC);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,8 +312,11 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnTop() {
|
||||
return isRecyclerViewOnTop(list);
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
Uri.Builder searchUri = base.path("/search");
|
||||
return isInstanceAkkoma()
|
||||
? searchUri.appendQueryParameter("query", currentQuery).build()
|
||||
: searchUri.build();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -89,13 +89,6 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
@@ -110,7 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
new AccountSwitcherSheet(getActivity()).show();
|
||||
new AccountSwitcherSheet(getActivity(), null).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -124,6 +124,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
((UsableRecyclerView) list).setSelector(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
@@ -24,6 +26,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -38,7 +41,7 @@ import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class InstanceRulesFragment extends ToolbarFragment{
|
||||
public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAssistContent {
|
||||
private UsableRecyclerView list;
|
||||
private MergeRecyclerAdapter adapter;
|
||||
private Button btn;
|
||||
@@ -130,6 +133,15 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
assistContent.setWebUri(new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(instance.normalizedUri)
|
||||
.path("/about")
|
||||
.build());
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.SparseIntArray;
|
||||
@@ -21,6 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
@@ -261,4 +263,16 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
protected boolean wantsOverlaySystemNavigation(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterContext getFilterContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
if (reportStatus != null) return Uri.parse(reportStatus.url);
|
||||
if (reportAccount != null) return Uri.parse(reportAccount.url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
@@ -135,6 +138,8 @@ public class Account extends BaseModel implements Searchable{
|
||||
|
||||
public List<Role> roles;
|
||||
|
||||
public @Nullable String fqn; // akkoma has this, mastodon't
|
||||
|
||||
@Override
|
||||
public String getQuery() {
|
||||
return url;
|
||||
@@ -162,6 +167,7 @@ public class Account extends BaseModel implements Searchable{
|
||||
moved.postprocess();
|
||||
if(TextUtils.isEmpty(displayName))
|
||||
displayName=username;
|
||||
if(fqn == null) fqn = getFullyQualifiedName();
|
||||
}
|
||||
|
||||
public boolean isLocal(){
|
||||
@@ -173,6 +179,10 @@ public class Account extends BaseModel implements Searchable{
|
||||
return parts.length==1 ? null : parts[1];
|
||||
}
|
||||
|
||||
public String getDomainFromURL() {
|
||||
return Uri.parse(url).getHost();
|
||||
}
|
||||
|
||||
public String getDisplayUsername(){
|
||||
return '@'+acct;
|
||||
}
|
||||
@@ -181,6 +191,10 @@ public class Account extends BaseModel implements Searchable{
|
||||
return '@'+acct.split("@")[0];
|
||||
}
|
||||
|
||||
public String getFullyQualifiedName() {
|
||||
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Account{"+
|
||||
|
||||
@@ -8,8 +8,17 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class BaseModel implements Cloneable{
|
||||
|
||||
/**
|
||||
* indicates the profile has been fetched from a foreign instance.
|
||||
*
|
||||
* @see MastodonAPIRequest#execRemote
|
||||
*/
|
||||
public transient boolean isRemote;
|
||||
|
||||
public abstract class BaseModel{
|
||||
@CallSuper
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
try{
|
||||
@@ -23,4 +32,14 @@ public abstract class BaseModel{
|
||||
}
|
||||
}catch(IllegalAccessException ignore){}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object clone(){
|
||||
try{
|
||||
return super.clone();
|
||||
}catch(CloneNotSupportedException x){
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.view.Menu;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
public enum ContentType {
|
||||
@SerializedName("text/plain")
|
||||
PLAIN,
|
||||
@SerializedName("text/html")
|
||||
HTML,
|
||||
@SerializedName("text/markdown")
|
||||
MARKDOWN,
|
||||
@SerializedName("text/bbcode")
|
||||
BBCODE, // akkoma
|
||||
@SerializedName("text/x.misskeymarkdown")
|
||||
MISSKEY_MARKDOWN; // akkoma/*key
|
||||
|
||||
public static int getContentTypeRes(@Nullable ContentType contentType) {
|
||||
return contentType == null ? R.id.content_type_null : switch(contentType) {
|
||||
case PLAIN -> R.id.content_type_plain;
|
||||
case HTML -> R.id.content_type_html;
|
||||
case MARKDOWN -> R.id.content_type_markdown;
|
||||
case BBCODE -> R.id.content_type_bbcode;
|
||||
case MISSKEY_MARKDOWN -> R.id.content_type_misskey_markdown;
|
||||
};
|
||||
}
|
||||
|
||||
public static void adaptMenuToInstance(Menu m, Instance i) {
|
||||
if (i.pleroma == null) {
|
||||
// memo: change this if glitch or another mastodon fork supports bbcode or mfm
|
||||
m.findItem(R.id.content_type_bbcode).setVisible(false);
|
||||
m.findItem(R.id.content_type_misskey_markdown).setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,22 @@ package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Parcel
|
||||
public class Filter extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String phrase;
|
||||
public String title;
|
||||
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
@Parcel
|
||||
public class FilterResult extends BaseModel {
|
||||
public Filter filter;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException {
|
||||
super.postprocess();
|
||||
if (filter != null) filter.postprocess();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.net.IDN;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Parcel
|
||||
public class Instance extends BaseModel{
|
||||
@@ -86,6 +87,11 @@ public class Instance extends BaseModel{
|
||||
|
||||
public Pleroma pleroma;
|
||||
|
||||
public PleromaPollLimits pollLimits;
|
||||
|
||||
/** like uri, but always without scheme and trailing slash */
|
||||
public transient String normalizedUri;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
@@ -95,6 +101,10 @@ public class Instance extends BaseModel{
|
||||
rules=Collections.emptyList();
|
||||
if(shortDescription==null)
|
||||
shortDescription="";
|
||||
// akkoma says uri is "https://example.social" while just "example.social" on mastodon
|
||||
normalizedUri = uri
|
||||
.replaceFirst("^https://", "")
|
||||
.replaceFirst("/$", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,6 +144,26 @@ public class Instance extends BaseModel{
|
||||
return ci;
|
||||
}
|
||||
|
||||
public boolean isAkkoma() {
|
||||
return pleroma != null;
|
||||
}
|
||||
|
||||
public boolean hasFeature(Feature feature) {
|
||||
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
|
||||
.map(p -> p.metadata)
|
||||
.map(m -> m.features);
|
||||
|
||||
return switch (feature) {
|
||||
case BUBBLE_TIMELINE -> pleromaFeatures
|
||||
.map(f -> f.contains("bubble_timeline"))
|
||||
.orElse(false);
|
||||
};
|
||||
}
|
||||
|
||||
public enum Feature {
|
||||
BUBBLE_TIMELINE
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class Rule{
|
||||
public String id;
|
||||
@@ -198,6 +228,28 @@ public class Instance extends BaseModel{
|
||||
|
||||
@Parcel
|
||||
public static class Pleroma extends BaseModel {
|
||||
// metadata etc
|
||||
public Pleroma.Metadata metadata;
|
||||
|
||||
@Parcel
|
||||
public static class Metadata {
|
||||
public List<String> features;
|
||||
public Pleroma.Metadata.FieldsLimits fieldsLimits;
|
||||
|
||||
@Parcel
|
||||
public static class FieldsLimits {
|
||||
public int maxFields;
|
||||
public int maxRemoteFields;
|
||||
public int nameLength;
|
||||
public int valueLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class PleromaPollLimits {
|
||||
public int maxExpiration;
|
||||
public int maxOptionChars;
|
||||
public int maxOptions;
|
||||
public int minExpiration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
||||
@NonNull
|
||||
@Override
|
||||
public PushSubscription clone(){
|
||||
PushSubscription copy=null;
|
||||
try{
|
||||
copy=(PushSubscription) super.clone();
|
||||
}catch(CloneNotSupportedException ignore){}
|
||||
PushSubscription copy=(PushSubscription) super.clone();
|
||||
copy.alerts=alerts.clone();
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
||||
public String idempotency;
|
||||
public String applicationId;
|
||||
public List<String> mediaIds;
|
||||
public ContentType contentType;
|
||||
}
|
||||
|
||||
@Parcel
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@Parcel
|
||||
public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
@RequiredField
|
||||
@@ -50,6 +52,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
public long favouritesCount;
|
||||
public long repliesCount;
|
||||
public Instant editedAt;
|
||||
// might not be provided (by older mastodon servers),
|
||||
// so megalodon will use the locally cached filters if filtered == null
|
||||
public List<FilterResult> filtered;
|
||||
|
||||
public String url;
|
||||
@@ -101,6 +105,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
card.postprocess();
|
||||
if(reblog!=null)
|
||||
reblog.postprocess();
|
||||
if(filtered!=null)
|
||||
for(FilterResult fr : filtered)
|
||||
fr.postprocess();
|
||||
|
||||
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
|
||||
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
|
||||
@@ -177,7 +184,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
s.mentions = List.of();
|
||||
s.tags = List.of();
|
||||
s.emojis = List.of();
|
||||
s.filtered = List.of();
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -187,7 +193,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
}
|
||||
|
||||
public static class StatusDeserializer implements JsonDeserializer<Status> {
|
||||
|
||||
@Override
|
||||
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
|
||||
@@ -8,17 +8,20 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.HomeTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.discover.BubbleTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TimelineDefinition {
|
||||
private TimelineType type;
|
||||
@@ -58,6 +61,14 @@ public class TimelineDefinition {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public boolean isCompatible(AccountSession session) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean wantsDefault(AccountSession session) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getTitle(Context ctx) {
|
||||
return title != null ? title : getDefaultTitle(ctx);
|
||||
}
|
||||
@@ -78,6 +89,7 @@ public class TimelineDefinition {
|
||||
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
|
||||
case LIST -> listTitle;
|
||||
case HASHTAG -> hashtagName;
|
||||
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,6 +101,7 @@ public class TimelineDefinition {
|
||||
case POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
|
||||
case LIST -> Icon.LIST;
|
||||
case HASHTAG -> Icon.HASHTAG;
|
||||
case BUBBLE -> Icon.BUBBLE;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,6 +113,7 @@ public class TimelineDefinition {
|
||||
case LIST -> new ListTimelineFragment();
|
||||
case HASHTAG -> new HashtagTimelineFragment();
|
||||
case POST_NOTIFICATIONS -> new NotificationsListFragment();
|
||||
case BUBBLE -> new BubbleTimelineFragment();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,7 +170,7 @@ public class TimelineDefinition {
|
||||
return args;
|
||||
}
|
||||
|
||||
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG }
|
||||
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, BUBBLE }
|
||||
|
||||
public enum Icon {
|
||||
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
|
||||
@@ -219,7 +233,8 @@ public class TimelineDefinition {
|
||||
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
|
||||
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
|
||||
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
|
||||
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true);
|
||||
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
|
||||
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
|
||||
|
||||
public final int iconRes, nameRes;
|
||||
public final boolean hidden;
|
||||
@@ -239,14 +254,50 @@ public class TimelineDefinition {
|
||||
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
|
||||
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
|
||||
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
|
||||
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
|
||||
@Override
|
||||
public boolean isCompatible(AccountSession session) {
|
||||
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of
|
||||
// at least one instance that supports it, but doesn't list "bubble_timeline"
|
||||
return session.getInstance().map(Instance::isAkkoma).orElse(false);
|
||||
}
|
||||
|
||||
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
|
||||
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
|
||||
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
|
||||
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
|
||||
HOME_TIMELINE.copy(),
|
||||
LOCAL_TIMELINE.copy(),
|
||||
FEDERATED_TIMELINE.copy(),
|
||||
POSTS_TIMELINE.copy()
|
||||
@Override
|
||||
public boolean wantsDefault(AccountSession session) {
|
||||
return session.getInstance()
|
||||
.map(i -> i.hasFeature(Instance.Feature.BUBBLE_TIMELINE))
|
||||
.orElse(false);
|
||||
}
|
||||
};
|
||||
|
||||
public static List<TimelineDefinition> getDefaultTimelines(String accountId) {
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
|
||||
return DEFAULT_TIMELINES.stream()
|
||||
.filter(tl -> tl.isCompatible(session) && tl.wantsDefault(session))
|
||||
.map(TimelineDefinition::copy)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<TimelineDefinition> getAllTimelines(String accountId) {
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
|
||||
return ALL_TIMELINES.stream()
|
||||
.filter(tl -> tl.isCompatible(session))
|
||||
.map(TimelineDefinition::copy)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static final List<TimelineDefinition> DEFAULT_TIMELINES = List.of(
|
||||
HOME_TIMELINE,
|
||||
LOCAL_TIMELINE,
|
||||
BUBBLE_TIMELINE,
|
||||
FEDERATED_TIMELINE
|
||||
);
|
||||
|
||||
private static final List<TimelineDefinition> ALL_TIMELINES = List.of(
|
||||
HOME_TIMELINE,
|
||||
LOCAL_TIMELINE,
|
||||
FEDERATED_TIMELINE,
|
||||
POSTS_TIMELINE,
|
||||
BUBBLE_TIMELINE
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package org.joinmastodon.android.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -14,7 +14,7 @@ import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
@@ -23,13 +23,21 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.SplashFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -49,14 +57,26 @@ 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 BiConsumer<String, Boolean> onClick;
|
||||
private UsableRecyclerView list;
|
||||
private List<WrappedAccount> accounts;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private AccountsAdapter accountsAdapter;
|
||||
|
||||
public AccountSwitcherSheet(@NonNull Activity activity){
|
||||
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
|
||||
this(activity, fragment, false, false);
|
||||
}
|
||||
|
||||
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp){
|
||||
super(activity);
|
||||
this.activity=activity;
|
||||
|
||||
this.fragment=fragment;
|
||||
this.externalShare = externalShare;
|
||||
this.openInApp = openInApp;
|
||||
this.onClick = onClick;
|
||||
|
||||
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
|
||||
list=new UsableRecyclerView(activity);
|
||||
@@ -67,41 +87,63 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
View handle=new View(activity);
|
||||
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
|
||||
handle.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(36)));
|
||||
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
|
||||
adapter.addAdapter(new AccountsAdapter());
|
||||
AccountViewHolder holder=new AccountViewHolder();
|
||||
holder.more.setVisibility(View.GONE);
|
||||
holder.currentIcon.setVisibility(View.GONE);
|
||||
holder.name.setText(R.string.add_account);
|
||||
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
|
||||
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled);
|
||||
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
|
||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
|
||||
Nav.go(activity, CustomWelcomeFragment.class, null);
|
||||
dismiss();
|
||||
}));
|
||||
|
||||
if (externalShare) {
|
||||
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);
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(shareHeading));
|
||||
|
||||
setOnDismissListener((d) -> activity.finish());
|
||||
}
|
||||
|
||||
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
|
||||
|
||||
if (!externalShare) {
|
||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
|
||||
Nav.go(activity, CustomWelcomeFragment.class, null);
|
||||
dismiss();
|
||||
}));
|
||||
// disabled in megalodon
|
||||
// adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.log_out_all_accounts, R.drawable.ic_fluent_person_arrow_right_24_filled), this::confirmLogOutAll));
|
||||
}
|
||||
|
||||
list.setAdapter(adapter);
|
||||
DividerItemDecoration divider=new DividerItemDecoration(activity, R.attr.colorPollVoted, .5f, 72, 16, DividerItemDecoration.NOT_FIRST);
|
||||
divider.setDrawBelowLastItem(true);
|
||||
list.addItemDecoration(divider);
|
||||
|
||||
FrameLayout content=new FrameLayout(activity);
|
||||
content.setBackgroundResource(R.drawable.bg_bottom_sheet);
|
||||
content.addView(list);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(activity, R.attr.colorM3Surface),
|
||||
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||
}
|
||||
|
||||
public void setOnClick(BiConsumer<String, Boolean> onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
private void confirmLogOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(activity)
|
||||
.setTitle(R.string.log_out)
|
||||
.setMessage(R.string.confirm_log_out)
|
||||
.setMessage(activity.getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmLogOutAll(){
|
||||
new M3AlertDialogBuilder(activity)
|
||||
.setMessage(R.string.confirm_log_out_all_accounts)
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOutAll())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void logOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
@@ -120,9 +162,55 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void logOutAll(){
|
||||
final ProgressDialog progress=new ProgressDialog(activity);
|
||||
progress.setMessage(activity.getString(R.string.loading));
|
||||
progress.setCancelable(false);
|
||||
progress.show();
|
||||
ArrayList<AccountSession> sessions=new ArrayList<>(AccountSessionManager.getInstance().getLoggedInAccounts());
|
||||
for(AccountSession session:sessions){
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
AccountSessionManager.getInstance().removeAccount(session.getID());
|
||||
sessions.remove(session);
|
||||
if(sessions.isEmpty()){
|
||||
progress.dismiss();
|
||||
Nav.goClearingStack(activity, SplashFragment.class, null);
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
AccountSessionManager.getInstance().removeAccount(session.getID());
|
||||
sessions.remove(session);
|
||||
if(sessions.isEmpty()){
|
||||
progress.dismiss();
|
||||
Nav.goClearingStack(activity, SplashFragment.class, null);
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
dismiss();
|
||||
String activeAccountID = fragment != null
|
||||
? fragment.getAccountID()
|
||||
: AccountSessionManager.getInstance().getLastActiveAccountID();
|
||||
if (accountID.equals(activeAccountID)) {
|
||||
activity.finish();
|
||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||
} else {
|
||||
accounts.stream().filter(w -> accountID.equals(w.session.getID())).findAny().ifPresent(w -> {
|
||||
accountsAdapter.notifyItemRemoved(accounts.indexOf(w));
|
||||
accounts.remove(w);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,6 +228,13 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
}
|
||||
}
|
||||
|
||||
private View makeSimpleListItem(@StringRes int title, @DrawableRes int icon){
|
||||
TextView tv=(TextView) activity.getLayoutInflater().inflate(R.layout.item_text_with_icon, list, false);
|
||||
tv.setText(title);
|
||||
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, 0, 0, 0);
|
||||
return tv;
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -173,45 +268,42 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name;
|
||||
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
|
||||
private final TextView name, username;
|
||||
private final ImageView avatar;
|
||||
private final ImageButton more;
|
||||
private final View currentIcon;
|
||||
private final PopupMenu menu;
|
||||
private final CheckableRelativeLayout view;
|
||||
private final View radioButton, extraBtnWrap;
|
||||
private final ImageButton extraBtn;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(activity, R.layout.item_account_switcher, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
radioButton=findViewById(R.id.radiobtn);
|
||||
radioButton.setBackground(new RadioButton(activity).getButtonDrawable());
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
currentIcon=findViewById(R.id.current);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(OutlineProviders.RADIUS_MEDIUM));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
menu=new PopupMenu(activity, more);
|
||||
menu.inflate(R.menu.account_switcher);
|
||||
menu.setOnMenuItemClickListener(item1 -> {
|
||||
confirmLogOut(item.getID());
|
||||
return true;
|
||||
});
|
||||
more.setOnClickListener(v->menu.show());
|
||||
view=(CheckableRelativeLayout) itemView;
|
||||
extraBtnWrap = findViewById(R.id.extra_btn_wrap);
|
||||
extraBtn = findViewById(R.id.extra_btn);
|
||||
extraBtn.setOnClickListener(this::onExtraBtnClick);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(AccountSession item){
|
||||
name.setText("@"+item.self.username+"@"+item.domain);
|
||||
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
|
||||
more.setVisibility(View.GONE);
|
||||
currentIcon.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
more.setVisibility(View.VISIBLE);
|
||||
currentIcon.setVisibility(View.GONE);
|
||||
name.setText(item.self.displayName);
|
||||
username.setText(item.getFullUsername());
|
||||
radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
|
||||
extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);
|
||||
if (externalShare) view.setCheckable(false);
|
||||
else {
|
||||
String accountId = fragment != null
|
||||
? fragment.getAccountID()
|
||||
: AccountSessionManager.getInstance().getLastActiveAccountID();
|
||||
view.setChecked(accountId.equals(item.getID()));
|
||||
}
|
||||
menu.getMenu().findItem(R.id.log_out).setTitle(activity.getString(R.string.log_out_account, "@"+item.self.username));
|
||||
UiUtils.enablePopupMenuIcons(activity, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -226,12 +318,32 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
private void onExtraBtnClick(View view) {
|
||||
setOnDismissListener(null);
|
||||
dismiss();
|
||||
onClick.accept(item.getID(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
setOnDismissListener(null);
|
||||
if (onClick != null) {
|
||||
dismiss();
|
||||
onClick.accept(item.getID(), false);
|
||||
return;
|
||||
}
|
||||
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||
activity.finish();
|
||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(){
|
||||
if (externalShare) return false;
|
||||
confirmLogOut(item.getID());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappedAccount{
|
||||
|
||||
@@ -18,13 +18,15 @@ package org.joinmastodon.android.ui;
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.view.View;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -358,7 +360,14 @@ public class BetterItemAnimator extends SimpleItemAnimator{
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
|
||||
float alpha = 0;
|
||||
if (holder instanceof MediaGridStatusDisplayItem.Holder mediaItemHolder) {
|
||||
if (mediaItemHolder.isSizeUpdating()) {
|
||||
alpha = 1; // Image will flicker out and then in if alpha is 0
|
||||
mediaItemHolder.sizeUpdated();
|
||||
}
|
||||
}
|
||||
oldViewAnim.alpha(alpha).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
|
||||
@@ -8,7 +8,15 @@ import android.view.ViewOutlineProvider;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class OutlineProviders{
|
||||
private static SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> topRoundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> endRoundedRects=new SparseArray<>();
|
||||
|
||||
public static final int RADIUS_XSMALL=4;
|
||||
public static final int RADIUS_SMALL=8;
|
||||
public static final int RADIUS_MEDIUM=12;
|
||||
public static final int RADIUS_LARGE=16;
|
||||
public static final int RADIUS_XLARGE=28;
|
||||
|
||||
private OutlineProviders(){
|
||||
//no instance
|
||||
@@ -21,6 +29,12 @@ public class OutlineProviders{
|
||||
outline.setAlpha(view.getAlpha());
|
||||
}
|
||||
};
|
||||
public static final ViewOutlineProvider OVAL=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setOval(0, 0, view.getWidth(), view.getHeight());
|
||||
}
|
||||
};
|
||||
|
||||
public static ViewOutlineProvider roundedRect(int dp){
|
||||
ViewOutlineProvider provider=roundedRects.get(dp);
|
||||
@@ -31,6 +45,24 @@ public class OutlineProviders{
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static ViewOutlineProvider topRoundedRect(int dp){
|
||||
ViewOutlineProvider provider=topRoundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
return provider;
|
||||
provider=new TopRoundRectOutlineProvider(V.dp(dp));
|
||||
topRoundedRects.put(dp, provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static ViewOutlineProvider endRoundedRect(int dp){
|
||||
ViewOutlineProvider provider=endRoundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
return provider;
|
||||
provider=new EndRoundRectOutlineProvider(V.dp(dp));
|
||||
endRoundedRects.put(dp, provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static class RoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
@@ -43,4 +75,34 @@ public class OutlineProviders{
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TopRoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
private TopRoundRectOutlineProvider(int radius){
|
||||
this.radius=radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+radius, radius);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
private EndRoundRectOutlineProvider(int radius){
|
||||
this.radius=radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
if(view.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL){
|
||||
outline.setRoundRect(-radius, 0, view.getWidth(), view.getHeight(), radius);
|
||||
}else{
|
||||
outline.setRoundRect(0, 0, view.getWidth()+radius, view.getHeight(), radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("id", item.status.id);
|
||||
args.putString("url", item.status.url);
|
||||
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
|
||||
public class FileStatusDisplayItem extends StatusDisplayItem{
|
||||
private final Attachment attachment;
|
||||
|
||||
public FileStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Attachment attachment) {
|
||||
super(parentID, parentFragment);
|
||||
this.attachment=attachment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.FILE;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<FileStatusDisplayItem> {
|
||||
private final TextView title, domain;
|
||||
|
||||
public Holder(Context context, ViewGroup parent) {
|
||||
super(context, R.layout.display_item_file, parent);
|
||||
title=findViewById(R.id.title);
|
||||
domain=findViewById(R.id.domain);
|
||||
findViewById(R.id.inner).setOnClickListener(this::onClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(FileStatusDisplayItem item) {
|
||||
Uri url = Uri.parse(getUrl());
|
||||
title.setText(item.attachment.description != null
|
||||
? item.attachment.description
|
||||
: url.getLastPathSegment());
|
||||
title.setEllipsize(item.attachment.description != null ? TextUtils.TruncateAt.END : TextUtils.TruncateAt.MIDDLE);
|
||||
domain.setText(url.getHost());
|
||||
}
|
||||
|
||||
private void onClick(View v) {
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), getUrl());
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return item.attachment.remoteUrl == null ? item.attachment.url : item.attachment.remoteUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
@@ -17,7 +16,6 @@ import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
@@ -56,8 +54,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||
private final TextView reply, boost, favorite, bookmark;
|
||||
private final ImageView share;
|
||||
private final TextView replies, boosts, favorites;
|
||||
private final View reply, boost, favorite, share, bookmark;
|
||||
private static final Animation opacityOut, opacityIn;
|
||||
|
||||
private View touchingView = null;
|
||||
@@ -91,22 +89,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_footer, parent);
|
||||
reply=findViewById(R.id.reply);
|
||||
boost=findViewById(R.id.boost);
|
||||
favorite=findViewById(R.id.favorite);
|
||||
bookmark=findViewById(R.id.bookmark);
|
||||
share=findViewById(R.id.share);
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
|
||||
}
|
||||
View reply=findViewById(R.id.reply_btn);
|
||||
View boost=findViewById(R.id.boost_btn);
|
||||
View favorite=findViewById(R.id.favorite_btn);
|
||||
View share=findViewById(R.id.share_btn);
|
||||
View bookmark=findViewById(R.id.bookmark_btn);
|
||||
replies=findViewById(R.id.reply);
|
||||
boosts=findViewById(R.id.boost);
|
||||
favorites=findViewById(R.id.favorite);
|
||||
|
||||
reply=findViewById(R.id.reply_btn);
|
||||
boost=findViewById(R.id.boost_btn);
|
||||
favorite=findViewById(R.id.favorite_btn);
|
||||
share=findViewById(R.id.share_btn);
|
||||
bookmark=findViewById(R.id.bookmark_btn);
|
||||
|
||||
reply.setOnTouchListener(this::onButtonTouch);
|
||||
reply.setOnClickListener(this::onReplyClick);
|
||||
reply.setOnLongClickListener(this::onReplyLongClick);
|
||||
@@ -131,18 +123,33 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onBind(FooterStatusDisplayItem item){
|
||||
bindButton(reply, item.status.repliesCount);
|
||||
bindButton(boost, item.status.reblogsCount);
|
||||
bindButton(favorite, item.status.favouritesCount);
|
||||
reply.setSelected(item.status.repliesCount > 0);
|
||||
bindText(replies, item.status.repliesCount);
|
||||
bindText(boosts, item.status.reblogsCount);
|
||||
bindText(favorites, item.status.favouritesCount);
|
||||
// in thread view, direct descendant posts display one direct reply to themselves,
|
||||
// hence in that case displaying whether there is another reply
|
||||
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
|
||||
reply.setSelected(item.status.repliesCount > compareTo);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
favorite.setSelected(item.status.favourited);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|
||||
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
|
||||
|
||||
int nextPos = getAbsoluteAdapterPosition() + 1;
|
||||
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
|
||||
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
|
||||
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
|
||||
!nextIsWarning;
|
||||
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
|
||||
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
|
||||
condenseBottom ? V.dp(-5) : 0);
|
||||
|
||||
itemView.requestLayout();
|
||||
}
|
||||
|
||||
private void bindButton(TextView btn, long count){
|
||||
private void bindText(TextView btn, long count){
|
||||
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
||||
btn.setText(UiUtils.abbreviateNumber(count));
|
||||
btn.setCompoundDrawablePadding(V.dp(8));
|
||||
@@ -166,8 +173,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
longClickPerformed = false;
|
||||
touchingView = v;
|
||||
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
|
||||
v.setPivotX(V.dp(20));
|
||||
// 28dp to center in middle of icon, because:
|
||||
// (icon width = 24dp) / 2 + (paddingStart = 8dp) + (paddingHorizontal = 8dp)
|
||||
v.setPivotX(UiUtils.sp(v.getContext(), 28));
|
||||
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
|
||||
if (disabled) return true;
|
||||
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
||||
@@ -211,7 +219,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
private void boostConsumer(View v, Status r) {
|
||||
v.startAnimation(opacityIn);
|
||||
bindButton(boost, r.reblogsCount);
|
||||
bindText(boosts, r.reblogsCount);
|
||||
}
|
||||
|
||||
private boolean onBoostLongClick(View v){
|
||||
@@ -300,7 +308,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
favorite.setSelected(!item.status.favourited);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
||||
v.startAnimation(opacityIn);
|
||||
bindButton(favorite, r.favouritesCount);
|
||||
bindText(favorites, r.favouritesCount);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -25,6 +27,13 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
|
||||
private final TextView title, subtitle;
|
||||
private final HashtagChartView chart;
|
||||
public static final RelativeLayout.LayoutParams
|
||||
withHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
|
||||
withoutHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
static {
|
||||
withoutHistoryParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
|
||||
}
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_trending_hashtag, parent);
|
||||
@@ -37,6 +46,14 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(HashtagStatusDisplayItem _item){
|
||||
Hashtag item=_item.tag;
|
||||
title.setText('#'+item.name);
|
||||
if (item.history == null || item.history.isEmpty()) {
|
||||
subtitle.setText(null);
|
||||
chart.setVisibility(View.GONE);
|
||||
title.setLayoutParams(withoutHistoryParams);
|
||||
return;
|
||||
}
|
||||
chart.setVisibility(View.VISIBLE);
|
||||
title.setLayoutParams(withHistoryParams);
|
||||
int numPeople=item.history.get(0).accounts;
|
||||
if(item.history.size()>1)
|
||||
numPeople+=item.history.get(1).accounts;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -24,8 +22,6 @@ import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
@@ -36,7 +32,7 @@ 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.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||
import org.joinmastodon.android.fragments.ListsFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
@@ -48,7 +44,6 @@ import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -58,7 +53,6 @@ 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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -217,6 +211,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onSuccess(GetStatusSourceText.Response result){
|
||||
args.putString("sourceText", result.text);
|
||||
args.putString("sourceSpoiler", result.spoilerText);
|
||||
if (result.contentType != null) {
|
||||
args.putString("sourceContentType", result.contentType.name());
|
||||
}
|
||||
if (redraft) {
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
@@ -280,7 +277,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("profileAccount", account.id);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
Nav.go(item.parentFragment.getActivity(), ListTimelinesFragment.class, args);
|
||||
Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -17,7 +18,6 @@ import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
@@ -40,7 +40,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private static final String TAG="MediaGridDisplayItem";
|
||||
|
||||
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
|
||||
private final List<Attachment> attachments;
|
||||
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
||||
@@ -55,7 +55,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
for(Attachment att:attachments){
|
||||
requests.add(new UrlImageLoaderRequest(switch(att.type){
|
||||
case IMAGE -> att.url;
|
||||
case VIDEO, GIFV -> att.previewUrl;
|
||||
case VIDEO, GIFV -> att.previewUrl != null ? att.previewUrl : att.url;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+att.type);
|
||||
}, 1000, 1000));
|
||||
}
|
||||
@@ -98,6 +98,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private int altTextIndex=-1;
|
||||
private Animator altTextAnimator;
|
||||
|
||||
private boolean sizeUpdating = false;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
|
||||
wrapper=(FrameLayout)itemView;
|
||||
@@ -126,6 +128,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
layout.removeAllViews();
|
||||
controllers.clear();
|
||||
|
||||
int i=0;
|
||||
for(Attachment att:item.attachments){
|
||||
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
|
||||
@@ -158,6 +161,19 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
Rect bounds=drawable.getBounds();
|
||||
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
|
||||
if(item.attachments.get(index).meta==null){
|
||||
Attachment.Metadata metadata = new Attachment.Metadata();
|
||||
metadata.width=drawable.getIntrinsicWidth();
|
||||
metadata.height=drawable.getIntrinsicHeight();
|
||||
item.attachments.get(index).meta=metadata;
|
||||
|
||||
item.tiledLayout=PhotoLayoutHelper.processThumbs(item.attachments);
|
||||
sizeUpdating = true;
|
||||
item.parentFragment.onImageUpdated(this, index);
|
||||
}
|
||||
|
||||
controllers.get(index).setImage(drawable);
|
||||
}
|
||||
|
||||
@@ -314,5 +330,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
layout.setClipChildren(clip);
|
||||
wrapper.setClipChildren(clip);
|
||||
}
|
||||
|
||||
public boolean isSizeUpdating() {
|
||||
return sizeUpdating;
|
||||
}
|
||||
|
||||
public void sizeUpdated() {
|
||||
sizeUpdating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
public void updateVisibility(StatusPrivacy visibility) {
|
||||
this.visibility = visibility;
|
||||
this.iconEnd = visibility != null ? switch (visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20sp_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20sp_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20sp_filled;
|
||||
default -> 0;
|
||||
} : 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -26,6 +27,7 @@ import org.joinmastodon.android.model.ScheduledStatus;
|
||||
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.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -45,6 +47,23 @@ public abstract class StatusDisplayItem{
|
||||
public final BaseStatusListFragment parentFragment;
|
||||
public boolean inset;
|
||||
public int index;
|
||||
public boolean
|
||||
hasDescendantNeighbor = false,
|
||||
hasAncestoringNeighbor = false,
|
||||
isMainStatus = true,
|
||||
isDirectDescendant = false;
|
||||
|
||||
public void setAncestryInfo(
|
||||
boolean hasDescendantNeighbor,
|
||||
boolean hasAncestoringNeighbor,
|
||||
boolean isMainStatus,
|
||||
boolean isDirectDescendant
|
||||
) {
|
||||
this.hasDescendantNeighbor = hasDescendantNeighbor;
|
||||
this.hasAncestoringNeighbor = hasAncestoringNeighbor;
|
||||
this.isMainStatus = isMainStatus;
|
||||
this.isDirectDescendant = isDirectDescendant;
|
||||
}
|
||||
|
||||
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||
this.parentID=parentID;
|
||||
@@ -78,21 +97,14 @@ public abstract class StatusDisplayItem{
|
||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
||||
case FILE -> new FileStatusDisplayItem.Holder(activity, parent);
|
||||
};
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
|
||||
String parentID=parentObject.getID();
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
@@ -102,14 +114,6 @@ public abstract class StatusDisplayItem{
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
||||
|
||||
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream()
|
||||
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList());
|
||||
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
|
||||
|
||||
if(!statusForContent.filterRevealed){
|
||||
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
|
||||
}
|
||||
|
||||
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
||||
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||
@@ -125,7 +129,7 @@ public abstract class StatusDisplayItem{
|
||||
: fragment.getString(R.string.in_reply_to, account.displayName);
|
||||
replyLine = new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, text, account == null ? List.of() : account.emojis,
|
||||
R.drawable.ic_fluent_arrow_reply_20_filled, null, null, fullText
|
||||
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +137,7 @@ public abstract class StatusDisplayItem{
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
||||
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
|
||||
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);
|
||||
}, fullText));
|
||||
@@ -148,7 +152,7 @@ public abstract class StatusDisplayItem{
|
||||
// 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_20_filled, null,
|
||||
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||
i -> {
|
||||
args.putString("hashtag", hashtag.name);
|
||||
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
||||
@@ -177,14 +181,32 @@ public abstract class StatusDisplayItem{
|
||||
items.add(replyLine);
|
||||
}
|
||||
|
||||
if (statusForContent.quote != null) {
|
||||
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
|
||||
if (!hasQuoteInlineTag) {
|
||||
String quoteUrl = statusForContent.quote.url;
|
||||
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
|
||||
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
|
||||
statusForContent.content += quoteInline;
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(statusForContent.content))
|
||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
|
||||
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)
|
||||
replyLine.needBottomPadding=true;
|
||||
else
|
||||
header.needBottomPadding=true;
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream()
|
||||
.filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN))
|
||||
.collect(Collectors.toList());
|
||||
if(!imageAttachments.isEmpty()){
|
||||
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorAccentLightest);
|
||||
for (Attachment att : imageAttachments) {
|
||||
if (att.blurhashPlaceholder == null) {
|
||||
att.blurhashPlaceholder = new ColorDrawable(color);
|
||||
}
|
||||
}
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
||||
}
|
||||
@@ -193,6 +215,12 @@ public abstract class StatusDisplayItem{
|
||||
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
}
|
||||
|
||||
statusForContent.mediaAttachments.stream()
|
||||
.filter(att->att.type.equals(Attachment.Type.UNKNOWN))
|
||||
.map(att -> new FileStatusDisplayItem(parentID, fragment, att))
|
||||
.forEach(items::add);
|
||||
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||
}
|
||||
@@ -208,8 +236,15 @@ public abstract class StatusDisplayItem{
|
||||
item.index=i++;
|
||||
}
|
||||
|
||||
Filter applyingFilter = null;
|
||||
if (!statusForContent.filterRevealed) {
|
||||
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN);
|
||||
statusForContent.filterRevealed = predicate.test(status);
|
||||
applyingFilter = predicate.getApplyingFilter();
|
||||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> result = statusForContent.filterRevealed ? items :
|
||||
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)));
|
||||
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items, applyingFilter)));
|
||||
|
||||
if (addFooter && status.hasGapAfter && !(fragment instanceof ThreadFragment)) {
|
||||
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
|
||||
@@ -242,7 +277,8 @@ public abstract class StatusDisplayItem{
|
||||
GAP,
|
||||
EXTENDED_FOOTER,
|
||||
MEDIA_GRID,
|
||||
WARNING
|
||||
WARNING,
|
||||
FILE
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
|
||||
@@ -236,13 +236,33 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
// remove additional padding when (transparently padded) translate button is visible
|
||||
int nextPos = getAbsoluteAdapterPosition() + 1;
|
||||
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
|
||||
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
|
||||
int bottomPadding = (translateVisible && nextIsFooter) ? 0
|
||||
: nextIsFooter ? V.dp(6)
|
||||
: V.dp(12);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
|
||||
|
||||
if (!GlobalUserPreferences.collapseLongPosts) {
|
||||
textScrollView.setLayoutParams(wrapParams);
|
||||
readMore.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// incredibly ugly workaround for https://github.com/sk22/megalodon/issues/520
|
||||
// i am so, so sorry. FIXME
|
||||
// attempts to use OnPreDrawListener, OnGlobalLayoutListener and .post have failed -
|
||||
// the view didn't want to reliably update after calling .setVisibility etc :(
|
||||
int width = parent.getWidth() != 0 ? parent.getWidth()
|
||||
: item.parentFragment.getView().getWidth() != 0
|
||||
? item.parentFragment.getView().getWidth()
|
||||
: item.parentFragment.getParentFragment() != null && item.parentFragment.getParentFragment().getView().getWidth() != 0
|
||||
? item.parentFragment.getParentFragment().getView().getWidth() // YIKES
|
||||
: UiUtils.MAX_WIDTH;
|
||||
|
||||
text.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
|
||||
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
@@ -15,11 +16,13 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
public final Status status;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
public Filter applyingFilter;
|
||||
|
||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems){
|
||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, Filter applyingFilter){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.filteredItems = filteredItems;
|
||||
this.applyingFilter = applyingFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,7 +44,7 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onBind(WarningFilteredStatusDisplayItem item) {
|
||||
filteredItems = item.filteredItems;
|
||||
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.status.filtered.get(item.status.filtered.size() -1).filter.title));
|
||||
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.applyingFilter.title));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user