Compare commits
229 Commits
v1.2.3+for
...
v1.2.3+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b94741feae | ||
|
|
e43d6c35d8 | ||
|
|
4a6f9e80b1 | ||
|
|
ec02680507 | ||
|
|
5fc569a45a | ||
|
|
4bc9c5691d | ||
|
|
19b68855ac | ||
|
|
70fdfb612e | ||
|
|
0a32c217d8 | ||
|
|
5dfa9237ad | ||
|
|
573ff75498 | ||
|
|
87c37df370 | ||
|
|
7fb0944e66 | ||
|
|
35c8a3d121 | ||
|
|
9e58413d1a | ||
|
|
90e60aef84 | ||
|
|
8547ce05ed | ||
|
|
0825faee5c | ||
|
|
d43a697df7 | ||
|
|
3b742c4391 | ||
|
|
a43a396043 | ||
|
|
bcb4fac553 | ||
|
|
35bf858a83 | ||
|
|
870bfaf08c | ||
|
|
c4238fb19b | ||
|
|
ba7aeb358b | ||
|
|
6f3fd4d454 | ||
|
|
c890195567 | ||
|
|
b50a327b17 | ||
|
|
97547f334f | ||
|
|
1ab953d819 | ||
|
|
dbe7eb25ff | ||
|
|
45ecec09f5 | ||
|
|
9b4556d293 | ||
|
|
307d483a56 | ||
|
|
9612248695 | ||
|
|
1f63401e5b | ||
|
|
d35ec18a88 | ||
|
|
b93b1847c3 | ||
|
|
cd46ed565f | ||
|
|
4a0e4edef8 | ||
|
|
2ea7333daa | ||
|
|
fa7a66809d | ||
|
|
71884ab760 | ||
|
|
f31205c670 | ||
|
|
0091ae87ce | ||
|
|
ad13b1e927 | ||
|
|
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 | ||
|
|
2e1795dc6f | ||
|
|
cd1be782fa | ||
|
|
67059f3d71 | ||
|
|
15f4d3326b | ||
|
|
e65404a466 | ||
|
|
3d47d1b4db | ||
|
|
d1749ab610 | ||
|
|
806c264686 | ||
|
|
34a9cb5a74 | ||
|
|
64fad2e871 | ||
|
|
9c89c26097 | ||
|
|
e3b6a5d389 | ||
|
|
0fb54efde5 | ||
|
|
a4a3f32dba | ||
|
|
03a1e29e0c | ||
|
|
eda9ff272b | ||
|
|
b3728e06ac |
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId "org.joinmastodon.android.sk"
|
applicationId "org.joinmastodon.android.sk"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 83
|
versionCode 92
|
||||||
versionName "1.2.3+fork.83"
|
versionName "1.2.3+fork.92"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
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']
|
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:dynamicanimation:1.1.0-alpha03'
|
||||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||||
implementation 'me.grishka.litex:viewpager2: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 'com.google.code.gson:gson:2.9.0'
|
||||||
implementation 'org.jsoup:jsoup:1.14.3'
|
implementation 'org.jsoup:jsoup:1.14.3'
|
||||||
implementation 'com.squareup:otto:1.3.8'
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,8 @@
|
|||||||
<data android:scheme="megalodon-android-auth" android:host="callback"/>
|
<data android:scheme="megalodon-android-auth" android:host="callback"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|||||||
@@ -3,21 +3,25 @@ package org.joinmastodon.android;
|
|||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Pair;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
|
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.jsoup.internal.StringUtil;
|
import org.jsoup.internal.StringUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
@@ -28,18 +32,52 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||||||
UiUtils.setUserPreferredTheme(this);
|
UiUtils.setUserPreferredTheme(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
if(savedInstanceState==null){
|
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();
|
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();
|
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}else if(sessions.size()==1){
|
} else if (isOpenable || sessions.size() > 1) {
|
||||||
|
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
fediHandle
|
||||||
|
.<MastodonAPIRequest<?>>map(handle ->
|
||||||
|
UiUtils.lookupAccountHandle(this, accountId, handle, callback))
|
||||||
|
.or(() ->
|
||||||
|
UiUtils.lookupURL(this, accountId, text.get(), callback))
|
||||||
|
.ifPresent(req ->
|
||||||
|
req.wrapProgress(this, R.string.loading, true, d -> {
|
||||||
|
UiUtils.transformDialogForLookup(this, accountId, isFediUrl ? text.get() : null, d);
|
||||||
|
d.setOnDismissListener((ev) -> finish());
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
openComposeFragment(accountId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sheet.show();
|
||||||
|
} else if (sessions.size() == 1) {
|
||||||
openComposeFragment(sessions.get(0).getID());
|
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())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean replyLineAboveHeader;
|
public static boolean replyLineAboveHeader;
|
||||||
public static boolean compactReblogReplyLine;
|
public static boolean compactReblogReplyLine;
|
||||||
public static boolean confirmBeforeReblog;
|
public static boolean confirmBeforeReblog;
|
||||||
|
public static boolean allowRemoteLoading;
|
||||||
|
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||||
public static String publishButtonText;
|
public static String publishButtonText;
|
||||||
public static ThemePreference theme;
|
public static ThemePreference theme;
|
||||||
public static ColorPreference color;
|
public static ColorPreference color;
|
||||||
@@ -77,6 +79,16 @@ public class GlobalUserPreferences{
|
|||||||
catch (JsonSyntaxException ignored) { return orElse; }
|
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(){
|
public static void load(){
|
||||||
SharedPreferences prefs=getPrefs();
|
SharedPreferences prefs=getPrefs();
|
||||||
playGifs=prefs.getBoolean("playGifs", true);
|
playGifs=prefs.getBoolean("playGifs", true);
|
||||||
@@ -117,6 +129,8 @@ public class GlobalUserPreferences{
|
|||||||
replyVisibility=prefs.getString("replyVisibility", null);
|
replyVisibility=prefs.getString("replyVisibility", null);
|
||||||
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||||
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||||
|
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||||
|
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
||||||
@@ -166,6 +180,8 @@ public class GlobalUserPreferences{
|
|||||||
.putString("replyVisibility", replyVisibility)
|
.putString("replyVisibility", replyVisibility)
|
||||||
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
||||||
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
||||||
|
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||||
|
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,5 +201,10 @@ public class GlobalUserPreferences{
|
|||||||
LIGHT,
|
LIGHT,
|
||||||
DARK
|
DARK
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
public enum AutoRevealMode {
|
||||||
|
NEVER,
|
||||||
|
THREADS,
|
||||||
|
DISCUSSIONS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package org.joinmastodon.android;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
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.model.Notification;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
|
|
||||||
public class MainActivity extends FragmentStackActivity{
|
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||||
UiUtils.setUserPreferredTheme(this);
|
UiUtils.setUserPreferredTheme(this);
|
||||||
@@ -35,10 +39,18 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||||
}else{
|
}else{
|
||||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
|
||||||
AccountSession session;
|
AccountSession session;
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
Intent intent=getIntent();
|
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 fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||||
boolean hasNotification = intent.hasExtra("notification");
|
boolean hasNotification = intent.hasExtra("notification");
|
||||||
if(fromNotification){
|
if(fromNotification){
|
||||||
@@ -52,6 +64,7 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
}else{
|
}else{
|
||||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||||
}
|
}
|
||||||
|
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||||
args.putString("account", session.getID());
|
args.putString("account", session.getID());
|
||||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
@@ -75,11 +88,12 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent){
|
protected void onNewIntent(Intent intent){
|
||||||
super.onNewIntent(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");
|
String accountID=intent.getStringExtra("accountID");
|
||||||
AccountSession accountSession;
|
|
||||||
try{
|
try{
|
||||||
accountSession=AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
}catch(IllegalStateException x){
|
}catch(IllegalStateException x){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -103,23 +117,24 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showFragmentForNotification(Notification notification, String accountID){
|
private void showFragmentForNotification(Notification notification, String accountID){
|
||||||
Fragment fragment;
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putBoolean("_can_go_back", true);
|
|
||||||
try{
|
try{
|
||||||
notification.postprocess();
|
notification.postprocess();
|
||||||
}catch(ObjectValidationException x){
|
}catch(ObjectValidationException x){
|
||||||
Log.w("MainActivity", x);
|
Log.w("MainActivity", x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(notification.status!=null){
|
UiUtils.showFragmentForNotification(this, notification, accountID, null);
|
||||||
fragment=new ThreadFragment();
|
}
|
||||||
args.putParcelable("status", Parcels.wrap(notification.status));
|
|
||||||
}else{
|
private void showFragmentForExternalShare(Bundle args) {
|
||||||
fragment=new ProfileFragment();
|
String className = args.getString("fromExternalShare");
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(notification.account));
|
Fragment fragment = switch (className) {
|
||||||
}
|
case "ThreadFragment" -> new ThreadFragment();
|
||||||
|
case "ProfileFragment" -> new ProfileFragment();
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (fragment == null) return;
|
||||||
|
args.putBoolean("_can_go_back", true);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
showFragment(fragment);
|
showFragment(fragment);
|
||||||
}
|
}
|
||||||
@@ -153,18 +168,40 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
|
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
|
||||||
);
|
);
|
||||||
Bundle currentArgs = currentFragment.getArguments();
|
Bundle currentArgs = currentFragment.getArguments();
|
||||||
if (this.fragmentContainers.size() == 1
|
if (fragmentContainers.size() != 1
|
||||||
&& currentArgs != null
|
|| currentArgs == null
|
||||||
&& currentArgs.getBoolean("_can_go_back", false)
|
|| !currentArgs.getBoolean("_can_go_back", false)) {
|
||||||
&& currentArgs.containsKey("account")) {
|
super.onBackPressed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentArgs.getBoolean("_finish_on_back", false)) {
|
||||||
|
finish();
|
||||||
|
} else if (currentArgs.containsKey("account")) {
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putString("account", currentArgs.getString("account"));
|
args.putString("account", currentArgs.getString("account"));
|
||||||
args.putString("tab", "notifications");
|
if (getIntent().getBooleanExtra("fromNotification", false)) {
|
||||||
|
args.putString("tab", "notifications");
|
||||||
|
}
|
||||||
Fragment fragment=new HomeFragment();
|
Fragment fragment=new HomeFragment();
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
showFragmentClearingBackStack(fragment);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Token token){
|
public void onSuccess(Token token){
|
||||||
new GetOwnAccount()
|
new GetOwnAccount()
|
||||||
|
// in case the instance (looking at pixelfed) wants to redirect to a
|
||||||
|
// website, we need to pass a context so we can launch a browser
|
||||||
|
.setContext(OAuthActivity.this)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Account account){
|
public void onSuccess(Account account){
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
|||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Mention;
|
||||||
import org.joinmastodon.android.model.NotificationAction;
|
import org.joinmastodon.android.model.NotificationAction;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushNotification;
|
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.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
@@ -273,8 +275,23 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
}
|
}
|
||||||
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
|
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();
|
CreateStatus.Request req=new CreateStatus.Request();
|
||||||
req.status = input.toString();
|
req.status = initialText + input.toString();
|
||||||
req.language = preferences.postingDefaultLanguage;
|
req.language = preferences.postingDefaultLanguage;
|
||||||
req.visibility = preferences.postingDefaultVisibility;
|
req.visibility = preferences.postingDefaultVisibility;
|
||||||
req.inReplyToId = notification.status.id;
|
req.inReplyToId = notification.status.id;
|
||||||
@@ -282,7 +299,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
req.spoilerText = "re: " + notification.status.spoilerText;
|
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
|
@Override
|
||||||
public void onSuccess(Status status) {
|
public void onSuccess(Status status) {
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
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.Filter;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
|
||||||
import org.joinmastodon.android.model.SearchResult;
|
import org.joinmastodon.android.model.SearchResult;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||||
@@ -37,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
|||||||
|
|
||||||
public class CacheController{
|
public class CacheController{
|
||||||
private static final String TAG="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 WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
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());
|
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
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){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Status> result=new ArrayList<>();
|
ArrayList<Status> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
@@ -113,7 +112,7 @@ public class CacheController{
|
|||||||
runOnDbThread((db)->{
|
runOnDbThread((db)->{
|
||||||
if(clear)
|
if(clear)
|
||||||
db.delete("home_timeline", null, null);
|
db.delete("home_timeline", null, null);
|
||||||
ContentValues values=new ContentValues(3);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Status s:posts){
|
for(Status s:posts){
|
||||||
values.put("id", s.id);
|
values.put("id", s.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||||
@@ -121,6 +120,7 @@ public class CacheController{
|
|||||||
if(s.hasGapAfter)
|
if(s.hasGapAfter)
|
||||||
flags|=POST_FLAG_GAP_AFTER;
|
flags|=POST_FLAG_GAP_AFTER;
|
||||||
values.put("flags", flags);
|
values.put("flags", flags);
|
||||||
|
values.put("time", s.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -135,7 +135,7 @@ public class CacheController{
|
|||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
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){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Notification> result=new ArrayList<>();
|
ArrayList<Notification> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
@@ -160,7 +160,7 @@ public class CacheController{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
|
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<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Notification> result){
|
public void onSuccess(List<Notification> result){
|
||||||
@@ -193,7 +193,7 @@ public class CacheController{
|
|||||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||||
if(clear)
|
if(clear)
|
||||||
db.delete(table, null, null);
|
db.delete(table, null, null);
|
||||||
ContentValues values=new ContentValues(3);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Notification n:notifications){
|
for(Notification n:notifications){
|
||||||
if(n.type==null){
|
if(n.type==null){
|
||||||
continue;
|
continue;
|
||||||
@@ -201,6 +201,7 @@ public class CacheController{
|
|||||||
values.put("id", n.id);
|
values.put("id", n.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||||
values.put("type", n.type.ordinal());
|
values.put("type", n.type.ordinal());
|
||||||
|
values.put("time", n.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -297,21 +298,24 @@ public class CacheController{
|
|||||||
CREATE TABLE `home_timeline` (
|
CREATE TABLE `home_timeline` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE `notifications_all` (
|
CREATE TABLE `notifications_all` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE `notifications_mentions` (
|
CREATE TABLE `notifications_mentions` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
createPostsNotificationsTable(db);
|
createPostsNotificationsTable(db);
|
||||||
@@ -319,12 +323,16 @@ public class CacheController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||||
if(oldVersion==1){
|
if(oldVersion<2){
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
}
|
}
|
||||||
if(oldVersion==2){
|
if(oldVersion<3){
|
||||||
|
// MEGALODON-SPECIFIC
|
||||||
createPostsNotificationsTable(db);
|
createPostsNotificationsTable(db);
|
||||||
}
|
}
|
||||||
|
if(oldVersion<4){
|
||||||
|
addTimeColumns(db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||||
@@ -342,9 +350,21 @@ public class CacheController{
|
|||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`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
|
@FunctionalInterface
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
|||||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -161,6 +162,11 @@ public class MastodonAPIController{
|
|||||||
respObj=gson.fromJson(reader, req.respClass);
|
respObj=gson.fromJson(reader, req.respClass);
|
||||||
}
|
}
|
||||||
}catch(JsonIOException|JsonSyntaxException x){
|
}catch(JsonIOException|JsonSyntaxException x){
|
||||||
|
if (req.context != null && response.body().contentType().subtype().equals("html")) {
|
||||||
|
UiUtils.launchWebBrowser(req.context, response.request().url().toString());
|
||||||
|
req.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
@@ -20,9 +21,11 @@ import java.util.HashMap;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import me.grishka.appkit.api.APIRequest;
|
import me.grishka.appkit.api.APIRequest;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
@@ -44,10 +47,11 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
TypeToken<T> respTypeToken;
|
TypeToken<T> respTypeToken;
|
||||||
Call okhttpCall;
|
Call okhttpCall;
|
||||||
Token token;
|
Token token;
|
||||||
boolean canceled;
|
boolean canceled, isRemote;
|
||||||
Map<String, String> headers;
|
Map<String, String> headers;
|
||||||
private ProgressDialog progressDialog;
|
private ProgressDialog progressDialog;
|
||||||
protected boolean removeUnsupportedItems;
|
protected boolean removeUnsupportedItems;
|
||||||
|
@Nullable Context context;
|
||||||
|
|
||||||
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
||||||
this.path=path;
|
this.path=path;
|
||||||
@@ -101,6 +105,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
return this;
|
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){
|
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||||
return wrapProgress(activity, message, cancelable, null);
|
return wrapProgress(activity, message, cancelable, null);
|
||||||
}
|
}
|
||||||
@@ -164,9 +183,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MastodonAPIRequest<T> setContext(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Context getContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
||||||
if(respObj instanceof BaseModel){
|
if(respObj instanceof BaseModel){
|
||||||
|
((BaseModel) respObj).isRemote = isRemote;
|
||||||
((BaseModel) respObj).postprocess();
|
((BaseModel) respObj).postprocess();
|
||||||
}else if(respObj instanceof List){
|
}else if(respObj instanceof List){
|
||||||
if(removeUnsupportedItems){
|
if(removeUnsupportedItems){
|
||||||
@@ -175,6 +205,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
Object item=itr.next();
|
Object item=itr.next();
|
||||||
if(item instanceof BaseModel){
|
if(item instanceof BaseModel){
|
||||||
try{
|
try{
|
||||||
|
((BaseModel) item).isRemote = isRemote;
|
||||||
((BaseModel) item).postprocess();
|
((BaseModel) item).postprocess();
|
||||||
}catch(ObjectValidationException x){
|
}catch(ObjectValidationException x){
|
||||||
Log.w(TAG, "Removing invalid object from list", x);
|
Log.w(TAG, "Removing invalid object from list", x);
|
||||||
@@ -182,15 +213,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)){
|
for(Object item:((List<?>) respObj)){
|
||||||
if(item instanceof BaseModel){
|
if(item instanceof BaseModel){
|
||||||
|
((BaseModel) item).isRemote = isRemote;
|
||||||
((BaseModel) item).postprocess();
|
((BaseModel) item).postprocess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
for(Object item:((List<?>) respObj)){
|
for(Object item:((List<?>) respObj)){
|
||||||
if(item instanceof BaseModel)
|
if(item instanceof BaseModel) {
|
||||||
|
((BaseModel) item).isRemote = isRemote;
|
||||||
((BaseModel) item).postprocess();
|
((BaseModel) item).postprocess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class StatusInteractionController{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
runningFavoriteRequests.remove(status.id);
|
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);
|
cb.accept(result);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ public class StatusInteractionController{
|
|||||||
public void onSuccess(Status reblog){
|
public void onSuccess(Status reblog){
|
||||||
Status result = reblog.getContentStatus();
|
Status result = reblog.getContentStatus();
|
||||||
runningReblogRequests.remove(status.id);
|
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);
|
cb.accept(result);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
@@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
|
|||||||
addQueryParameter("min_id", minID);
|
addQueryParameter("min_id", minID);
|
||||||
if(limit>0)
|
if(limit>0)
|
||||||
addQueryParameter("limit", ""+limit);
|
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 com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
@@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
|||||||
addQueryParameter("limit", ""+limit);
|
addQueryParameter("limit", ""+limit);
|
||||||
if(sinceID!=null)
|
if(sinceID!=null)
|
||||||
addQueryParameter("since_id", sinceID);
|
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 com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
@@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
|||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
if(limit>0)
|
if(limit>0)
|
||||||
addQueryParameter("limit", limit+"");
|
addQueryParameter("limit", limit+"");
|
||||||
|
if(GlobalUserPreferences.replyVisibility != null)
|
||||||
|
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.joinmastodon.android.api.session;
|
package org.joinmastodon.android.api.session;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.CacheController;
|
import org.joinmastodon.android.api.CacheController;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
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.Account;
|
||||||
import org.joinmastodon.android.model.Application;
|
import org.joinmastodon.android.model.Application;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Markers;
|
import org.joinmastodon.android.model.Markers;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
@@ -14,6 +17,7 @@ import org.joinmastodon.android.model.Token;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class AccountSession{
|
public class AccountSession{
|
||||||
public Token token;
|
public Token token;
|
||||||
@@ -87,4 +91,15 @@ public class AccountSession{
|
|||||||
pushSubscriptionManager=new PushSubscriptionManager(getID());
|
pushSubscriptionManager=new PushSubscriptionManager(getID());
|
||||||
return pushSubscriptionManager;
|
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.BuildConfig;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.MainActivity;
|
import org.joinmastodon.android.MainActivity;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -110,6 +111,12 @@ public class AccountSessionManager{
|
|||||||
sessions.put(session.getID(), session);
|
sessions.put(session.getID(), session);
|
||||||
lastActiveAccountID=session.getID();
|
lastActiveAccountID=session.getID();
|
||||||
writeAccountsFile();
|
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);
|
updateMoreInstanceInfo(instance, instance.uri);
|
||||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||||
@@ -118,14 +125,16 @@ public class AccountSessionManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void writeAccountsFile(){
|
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{
|
||||||
try(FileOutputStream out=new FileOutputStream(file)){
|
try(FileOutputStream out=new FileOutputStream(tmpFile)){
|
||||||
SessionsStorageWrapper w=new SessionsStorageWrapper();
|
SessionsStorageWrapper w=new SessionsStorageWrapper();
|
||||||
w.accounts=new ArrayList<>(sessions.values());
|
w.accounts=new ArrayList<>(sessions.values());
|
||||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||||
MastodonAPIController.gson.toJson(w, writer);
|
MastodonAPIController.gson.toJson(w, writer);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
|
||||||
}
|
}
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
Log.e(TAG, "Error writing accounts file", x);
|
Log.e(TAG, "Error writing accounts file", x);
|
||||||
@@ -151,6 +160,11 @@ public class AccountSessionManager{
|
|||||||
return sessions.get(id);
|
return sessions.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public AccountSession tryGetAccount(Account account) {
|
||||||
|
return sessions.get(account.getDomainFromURL() + "_" + account.id);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public AccountSession getLastActiveAccount(){
|
public AccountSession getLastActiveAccount(){
|
||||||
if(sessions.isEmpty() || lastActiveAccountID==null)
|
if(sessions.isEmpty() || lastActiveAccountID==null)
|
||||||
@@ -178,6 +192,7 @@ public class AccountSessionManager{
|
|||||||
AccountSession session=getAccount(id);
|
AccountSession session=getAccount(id);
|
||||||
session.getCacheController().closeDatabase();
|
session.getCacheController().closeDatabase();
|
||||||
MastodonApp.context.deleteDatabase(id+".db");
|
MastodonApp.context.deleteDatabase(id+".db");
|
||||||
|
GlobalUserPreferences.removeAccount(id);
|
||||||
sessions.remove(id);
|
sessions.remove(id);
|
||||||
if(lastActiveAccountID.equals(id)){
|
if(lastActiveAccountID.equals(id)){
|
||||||
if(sessions.isEmpty())
|
if(sessions.isEmpty())
|
||||||
@@ -248,31 +263,35 @@ public class AccountSessionManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void maybeUpdateLocalInfo(){
|
public void maybeUpdateLocalInfo(){
|
||||||
|
maybeUpdateLocalInfo(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void maybeUpdateLocalInfo(AccountSession activeSession){
|
||||||
long now=System.currentTimeMillis();
|
long now=System.currentTimeMillis();
|
||||||
HashSet<String> domains=new HashSet<>();
|
HashSet<String> domains=new HashSet<>();
|
||||||
for(AccountSession session:sessions.values()){
|
for(AccountSession session:sessions.values()){
|
||||||
domains.add(session.domain.toLowerCase());
|
domains.add(session.domain.toLowerCase());
|
||||||
// if(now-session.infoLastUpdated>24L*3600_000L){
|
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
|
||||||
updateSessionPreferences(session);
|
updateSessionPreferences(session);
|
||||||
updateSessionLocalInfo(session);
|
updateSessionLocalInfo(session);
|
||||||
// }
|
}
|
||||||
// if(now-session.filtersLastUpdated>3600_000L){
|
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
|
||||||
updateSessionWordFilters(session);
|
updateSessionWordFilters(session);
|
||||||
// }
|
}
|
||||||
updateSessionMarkers(session);
|
updateSessionMarkers(session);
|
||||||
}
|
}
|
||||||
if(loadedInstances){
|
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();
|
long now=System.currentTimeMillis();
|
||||||
for(String domain:domains){
|
for(String domain:domains){
|
||||||
// Long lastUpdated=instancesLastUpdated.get(domain);
|
Long lastUpdated=instancesLastUpdated.get(domain);
|
||||||
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
|
||||||
updateInstanceInfo(domain);
|
updateInstanceInfo(domain);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +419,9 @@ public class AccountSessionManager{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
|
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
|
||||||
|
wrapper.instance = instance;
|
||||||
|
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.execNoAuth(domain);
|
.execNoAuth(domain);
|
||||||
@@ -411,10 +432,13 @@ public class AccountSessionManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
|
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);
|
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||||
MastodonAPIController.gson.toJson(emojis, writer);
|
MastodonAPIController.gson.toJson(emojis, writer);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
Log.w(TAG, "Error writing instance info file for "+domain, x);
|
Log.w(TAG, "Error writing instance info file for "+domain, x);
|
||||||
}
|
}
|
||||||
@@ -434,7 +458,7 @@ public class AccountSessionManager{
|
|||||||
}
|
}
|
||||||
if(!loadedInstances){
|
if(!loadedInstances){
|
||||||
loadedInstances=true;
|
loadedInstances=true;
|
||||||
maybeUpdateCustomEmojis(domains);
|
maybeUpdateCustomEmojis(domains, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,10 +482,6 @@ public class AccountSessionManager{
|
|||||||
return instances.get(domain);
|
return instances.get(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instance getInstanceInfoForAccount(String account) {
|
|
||||||
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateAccountInfo(String id, Account account){
|
public void updateAccountInfo(String id, Account account){
|
||||||
AccountSession session=getAccount(id);
|
AccountSession session=getAccount(id);
|
||||||
session.self=account;
|
session.self=account;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import org.parceler.Parcels;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
@@ -85,7 +87,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
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;
|
return;
|
||||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||||
@@ -93,10 +96,11 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||||
return;
|
return;
|
||||||
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
||||||
if(ev.status.mediaAttachments.isEmpty())
|
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prependItems(Collections.singletonList(ev.status), true);
|
prependItems(Collections.singletonList(ev.status), true);
|
||||||
|
if (isOnTop()) scrollToTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||||
@@ -129,4 +133,13 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.ACCOUNT;
|
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 static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -103,4 +104,9 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
|||||||
})
|
})
|
||||||
.exec(accountID);
|
.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;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
@@ -33,6 +34,7 @@ import org.joinmastodon.android.model.Relationship;
|
|||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
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.GapStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
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.photoviewer.PhotoViewerHost;
|
||||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -68,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab{
|
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||||
protected DisplayItemsAdapter adapter;
|
protected DisplayItemsAdapter adapter;
|
||||||
protected String accountID;
|
protected String accountID;
|
||||||
@@ -79,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||||
protected Rect tmpRect=new Rect();
|
protected Rect tmpRect=new Rect();
|
||||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||||
|
protected boolean currentlyScrolling;
|
||||||
|
|
||||||
public BaseStatusListFragment(){
|
public BaseStatusListFragment(){
|
||||||
super(20);
|
super(20);
|
||||||
@@ -92,7 +96,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
UiUtils.loadMaxWidth(getContext());
|
|
||||||
if(GlobalUserPreferences.disableMarquee){
|
if(GlobalUserPreferences.disableMarquee){
|
||||||
setTitleMarqueeEnabled(false);
|
setTitleMarqueeEnabled(false);
|
||||||
setSubtitleMarqueeEnabled(false);
|
setSubtitleMarqueeEnabled(false);
|
||||||
@@ -129,7 +132,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
displayItems.clear();
|
displayItems.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void prependItems(List<T> items, boolean notify){
|
protected int prependItems(List<T> items, boolean notify){
|
||||||
data.addAll(0, items);
|
data.addAll(0, items);
|
||||||
int offset=0;
|
int offset=0;
|
||||||
for(T s:items){
|
for(T s:items){
|
||||||
@@ -142,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
if(notify)
|
if(notify)
|
||||||
adapter.notifyItemRangeInserted(0, offset);
|
adapter.notifyItemRangeInserted(0, offset);
|
||||||
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getMaxID(){
|
protected String getMaxID(){
|
||||||
@@ -202,7 +206,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||||
MediaAttachmentViewController holder=findPhotoViewHolder(index);
|
MediaAttachmentViewController holder=findPhotoViewHolder(index);
|
||||||
if(holder!=null){
|
if(holder!=null && list!=null){
|
||||||
transitioningHolder=holder;
|
transitioningHolder=holder;
|
||||||
View view=transitioningHolder.photo;
|
View view=transitioningHolder.photo;
|
||||||
int[] pos={0, 0};
|
int[] pos={0, 0};
|
||||||
@@ -288,6 +292,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
fab.startAnimation(animate);
|
fab.startAnimation(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isScrolling() {
|
||||||
|
return currentlyScrolling;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hideFab() {
|
public void hideFab() {
|
||||||
View fab = getFab();
|
View fab = getFab();
|
||||||
@@ -315,7 +323,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
currentPhotoViewer.offsetView(-dx, -dy);
|
currentPhotoViewer.offsetView(-dx, -dy);
|
||||||
|
|
||||||
View fab = getFab();
|
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) {
|
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
||||||
hideFab();
|
hideFab();
|
||||||
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
||||||
@@ -328,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());
|
list.addItemDecoration(new StatusListItemDecoration());
|
||||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||||
private Rect tmpRect=new Rect();
|
private Rect tmpRect=new Rect();
|
||||||
@Override
|
@Override
|
||||||
public void getSelectorBounds(View view, Rect outRect){
|
public void getSelectorBounds(View view, Rect outRect){
|
||||||
|
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||||
|
int lastIndex = -1, firstIndex = -1;
|
||||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||||
if(holder instanceof StatusDisplayItem.Holder){
|
if(holder instanceof StatusDisplayItem.Holder){
|
||||||
@@ -345,18 +361,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
for(int i=0;i<list.getChildCount();i++){
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
View child=list.getChildAt(i);
|
View child=list.getChildAt(i);
|
||||||
holder=list.getChildViewHolder(child);
|
holder=list.getChildViewHolder(child);
|
||||||
if(holder instanceof StatusDisplayItem.Holder){
|
if(holder instanceof StatusDisplayItem.Holder<?> h){
|
||||||
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
|
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
|
||||||
if(otherID.equals(id)){
|
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);
|
list.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||||
outRect.left=Math.min(outRect.left, tmpRect.left);
|
outRect.left=Math.min(outRect.left, tmpRect.left);
|
||||||
outRect.top=Math.min(outRect.top, tmpRect.top);
|
outRect.top=Math.min(outRect.top, tmpRect.top);
|
||||||
outRect.right=Math.max(outRect.right, tmpRect.right);
|
outRect.right=Math.max(outRect.right, tmpRect.right);
|
||||||
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
|
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());
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
@@ -551,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 onGapClick(GapStatusDisplayItem.Holder item){}
|
||||||
|
|
||||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||||
@@ -562,6 +610,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
warning.getItem().status.filterRevealed = true;
|
warning.getItem().status.filterRevealed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getAccountID(){
|
public String getAccountID(){
|
||||||
return accountID;
|
return accountID;
|
||||||
}
|
}
|
||||||
@@ -631,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
smoothScrollRecyclerViewToTop(list);
|
smoothScrollRecyclerViewToTop(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnTop() {
|
||||||
|
return isRecyclerViewOnTop(list);
|
||||||
|
}
|
||||||
|
|
||||||
protected int getListWidthForMediaLayout(){
|
protected int getListWidthForMediaLayout(){
|
||||||
return list.getWidth();
|
return list.getWidth();
|
||||||
}
|
}
|
||||||
@@ -695,6 +749,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
return attachmentViewsPool;
|
return attachmentViewsPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProvideAssistContent(AssistContent assistContent) {
|
||||||
|
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDataLoaded(List<T> d, boolean more) {
|
||||||
|
super.onDataLoaded(d, more);
|
||||||
|
// more available, but the page isn't even full yet? seems wrong, let's load some more
|
||||||
|
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
|
||||||
|
}
|
||||||
|
|
||||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
@@ -756,6 +821,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
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){
|
&& (!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);
|
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||||
@@ -41,4 +42,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.ACCOUNT;
|
return Filter.FilterContext.ACCOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return base.path("/bookmarks").build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
|
|||||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
|
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
||||||
|
|
||||||
private static final int MEDIA_RESULT=717;
|
private static final int MEDIA_RESULT=717;
|
||||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||||
@@ -245,10 +245,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
|
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
|
||||||
if (contentType == null && GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
|
|
||||||
// if formatting is enabled, use plain to avoid confusing unspecified default setting
|
|
||||||
contentType = ContentType.PLAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
self=session.self;
|
self=session.self;
|
||||||
@@ -267,9 +263,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
Nav.finish(this);
|
Nav.finish(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(customEmojis.isEmpty()){
|
|
||||||
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
|
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
|
||||||
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
|
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
|
||||||
@@ -362,6 +355,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
} else {
|
} else {
|
||||||
mediaBtn.setOnClickListener(v -> openFilePicker(false));
|
mediaBtn.setOnClickListener(v -> openFilePicker(false));
|
||||||
}
|
}
|
||||||
|
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
|
||||||
pollBtn.setOnClickListener(v->togglePoll());
|
pollBtn.setOnClickListener(v->togglePoll());
|
||||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||||
@@ -586,8 +580,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable s){
|
public void afterTextChanged(Editable s){
|
||||||
if(s.length()==0)
|
if(s.length()==0){
|
||||||
|
updateCharCounter();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
int start=lastChangeStart;
|
int start=lastChangeStart;
|
||||||
int count=lastChangeCount;
|
int count=lastChangeCount;
|
||||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||||
@@ -852,12 +848,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
||||||
buildLanguageSelector(languageButton);
|
buildLanguageSelector(languageButton);
|
||||||
|
|
||||||
if (editingStatus != null && scheduledStatus == null) {
|
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||||
|
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
|
||||||
// editing an already published post
|
// editing an already published post
|
||||||
draftsBtn.setVisibility(View.GONE);
|
draftsBtn.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccountID() {
|
||||||
|
return accountID;
|
||||||
|
}
|
||||||
|
|
||||||
private void navigateToUnsentPosts() {
|
private void navigateToUnsentPosts() {
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
@@ -1014,7 +1016,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if(att.state!=AttachmentUploadState.DONE)
|
if(att.state!=AttachmentUploadState.DONE)
|
||||||
nonDoneAttachmentCount++;
|
nonDoneAttachmentCount++;
|
||||||
}
|
}
|
||||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
publishButton.setEnabled((!isInstancePixelfed() || attachments.size() > 0) && (trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||||
sendError.setVisibility(View.GONE);
|
sendError.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,7 +1093,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
}
|
}
|
||||||
req.status=text;
|
req.status=text;
|
||||||
req.localOnly=localOnly;
|
req.localOnly=localOnly;
|
||||||
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
|
req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
|
||||||
req.sensitive=sensitive;
|
req.sensitive=sensitive;
|
||||||
req.language=language;
|
req.language=language;
|
||||||
req.contentType=contentType;
|
req.contentType=contentType;
|
||||||
@@ -1156,7 +1158,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
sendProgress.setVisibility(View.VISIBLE);
|
sendProgress.setVisibility(View.VISIBLE);
|
||||||
sendError.setVisibility(View.GONE);
|
sendError.setVisibility(View.GONE);
|
||||||
|
|
||||||
Callback<Status> resCallback=new Callback<>(){
|
Callback<Status> resCallback = new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
maybeDeleteScheduledPost(() -> {
|
maybeDeleteScheduledPost(() -> {
|
||||||
@@ -1169,7 +1171,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
E.post(new StatusUpdatedEvent(result));
|
// pixelfed doesn't return the edited status :/
|
||||||
|
Status editedStatus = result == null ? editingStatus : result;
|
||||||
|
if (result == null) {
|
||||||
|
editedStatus.text = req.status;
|
||||||
|
editedStatus.spoilerText = req.spoilerText;
|
||||||
|
editedStatus.sensitive = req.sensitive;
|
||||||
|
editedStatus.language = req.language;
|
||||||
|
// user will have to reload to see html
|
||||||
|
editedStatus.content = req.status;
|
||||||
|
}
|
||||||
|
E.post(new StatusUpdatedEvent(editedStatus));
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
||||||
Nav.finish(ComposeFragment.this);
|
Nav.finish(ComposeFragment.this);
|
||||||
@@ -1747,11 +1759,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
pollChanged=true;
|
pollChanged=true;
|
||||||
updatePublishButtonState();
|
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);
|
pollOptionsView.addView(option.view);
|
||||||
pollOptions.add(option);
|
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);
|
addPollOptionBtn.setVisibility(View.GONE);
|
||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
@@ -1891,9 +1916,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
visibilityPopup=new PopupMenu(getActivity(), v);
|
visibilityPopup=new PopupMenu(getActivity(), v);
|
||||||
visibilityPopup.inflate(R.menu.compose_visibility);
|
visibilityPopup.inflate(R.menu.compose_visibility);
|
||||||
Menu m=visibilityPopup.getMenu();
|
Menu m=visibilityPopup.getMenu();
|
||||||
|
if (isInstancePixelfed()) {
|
||||||
|
m.findItem(R.id.vis_private).setVisible(false);
|
||||||
|
}
|
||||||
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
||||||
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||||
if (instance.pleroma != null) {
|
if (isInstanceAkkoma()) {
|
||||||
m.findItem(R.id.vis_local).setVisible(true);
|
m.findItem(R.id.vis_local).setVisible(true);
|
||||||
} else if (localOnly || prefsSaysSupported) {
|
} else if (localOnly || prefsSaysSupported) {
|
||||||
localOnlyItem.setVisible(true);
|
localOnlyItem.setVisible(true);
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
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.Hashtag;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.ListTimeline;
|
import org.joinmastodon.android.model.ListTimeline;
|
||||||
import org.joinmastodon.android.model.TimelineDefinition;
|
import org.joinmastodon.android.model.TimelineDefinition;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
@@ -164,7 +167,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
|||||||
makeBackItem(listsMenu);
|
makeBackItem(listsMenu);
|
||||||
makeBackItem(hashtagsMenu);
|
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));
|
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||||
|
|
||||||
@@ -190,7 +193,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
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();
|
updateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||||
@@ -41,4 +42,11 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.ACCOUNT;
|
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.Rect;
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextUtils;
|
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.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 String accountID;
|
||||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||||
private GetAccountRelationships relationshipsRequest;
|
private GetAccountRelationships relationshipsRequest;
|
||||||
@@ -148,6 +150,16 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
|||||||
smoothScrollRecyclerViewToTop(list);
|
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{
|
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
public AccountsAdapter(){
|
public AccountsAdapter(){
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
|
|||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 nextMaxID;
|
||||||
private String accountId;
|
private String accountID;
|
||||||
|
|
||||||
public FollowedHashtagsFragment() {
|
public FollowedHashtagsFragment() {
|
||||||
super(20);
|
super(20);
|
||||||
@@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
|||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Bundle args=getArguments();
|
Bundle args=getArguments();
|
||||||
accountId=args.getString("account");
|
accountID=args.getString("account");
|
||||||
setTitle(R.string.sk_hashtags_you_follow);
|
setTitle(R.string.sk_hashtags_you_follow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
|||||||
onDataLoaded(result, nextMaxID!=null);
|
onDataLoaded(result, nextMaxID!=null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountId);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,6 +77,16 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
|||||||
smoothScrollRecyclerViewToTop(list);
|
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>{
|
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@@ -109,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
|
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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 boolean isInstancePixelfed() {
|
||||||
|
return getInstance().map(Instance::isPixelfed).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
default Optional<Instance> getInstance() {
|
||||||
|
return getSession().getInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ public interface HasFab {
|
|||||||
View getFab();
|
View getFab();
|
||||||
void showFab();
|
void showFab();
|
||||||
void hideFab();
|
void hideFab();
|
||||||
|
boolean isScrolling();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -8,7 +9,6 @@ import android.view.MenuInflater;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
@@ -159,4 +159,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.PUBLIC;
|
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.Fragment;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -16,6 +17,11 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
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.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
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.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TabBar;
|
import org.joinmastodon.android.ui.views.TabBar;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import androidx.annotation.IdRes;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
|
||||||
|
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
import me.grishka.appkit.api.Callback;
|
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.utils.V;
|
||||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
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 FragmentRootLinearLayout content;
|
||||||
private HomeTabFragment homeTabFragment;
|
private HomeTabFragment homeTabFragment;
|
||||||
private NotificationsFragment notificationsFragment;
|
private NotificationsFragment notificationsFragment;
|
||||||
@@ -66,6 +69,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
private int currentTab=R.id.tab_home;
|
private int currentTab=R.id.tab_home;
|
||||||
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
|
private boolean isPleroma;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -73,16 +77,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
E.register(this);
|
E.register(this);
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
setTitle(R.string.sk_app_name);
|
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)
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
|
|
||||||
|
// TODO: clean up
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
homeTabFragment=new HomeTabFragment();
|
homeTabFragment=new HomeTabFragment();
|
||||||
homeTabFragment.setArguments(args);
|
homeTabFragment.setArguments(args);
|
||||||
args=new Bundle(args);
|
args=new Bundle(args);
|
||||||
|
args.putBoolean("disableDiscover", isPleroma);
|
||||||
args.putBoolean("noAutoLoad", true);
|
args.putBoolean("noAutoLoad", true);
|
||||||
searchFragment=new DiscoverFragment();
|
searchFragment=new DiscoverFragment();
|
||||||
searchFragment.setArguments(args);
|
searchFragment.setArguments(args);
|
||||||
@@ -217,6 +226,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCurrentTab(@IdRes int tab){
|
||||||
|
if(tab==currentTab)
|
||||||
|
return;
|
||||||
|
tabBar.selectTab(tab);
|
||||||
|
onTabSelected(tab);
|
||||||
|
}
|
||||||
|
|
||||||
private void onTabSelected(@IdRes int tab){
|
private void onTabSelected(@IdRes int tab){
|
||||||
Fragment newFragment=fragmentForTab(tab);
|
Fragment newFragment=fragmentForTab(tab);
|
||||||
if(tab==currentTab){
|
if(tab==currentTab){
|
||||||
@@ -228,9 +244,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
}
|
}
|
||||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||||
maybeTriggerLoading(newFragment);
|
maybeTriggerLoading(newFragment);
|
||||||
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
|
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
|
||||||
currentTab=tab;
|
currentTab=tab;
|
||||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||||
|
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeTriggerLoading(Fragment newFragment){
|
private void maybeTriggerLoading(Fragment newFragment){
|
||||||
@@ -257,7 +274,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||||
}
|
}
|
||||||
new AccountSwitcherSheet(getActivity()).show();
|
new AccountSwitcherSheet(getActivity(), this).show();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -290,10 +307,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
|
|
||||||
public void updateNotificationBadge() {
|
public void updateNotificationBadge() {
|
||||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
Optional<Instance> instance = session.getInstance();
|
||||||
if (instance == null) return;
|
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
|
||||||
|
|
||||||
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null)
|
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
|
||||||
.setCallback(new Callback<>() {
|
.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Notification> notifications) {
|
public void onSuccess(List<Notification> notifications) {
|
||||||
@@ -330,4 +347,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
|
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
|
||||||
setNotificationBadge(false);
|
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.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.FragmentTransaction;
|
import android.app.FragmentTransaction;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
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.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
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.CubicBezierInterpolator;
|
||||||
import me.grishka.appkit.utils.V;
|
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 static final int ANNOUNCEMENTS_RESULT = 654;
|
||||||
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
@@ -106,7 +108,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
E.register(this);
|
E.register(this);
|
||||||
accountID = getArguments().getString("account");
|
accountID = getArguments().getString("account");
|
||||||
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
|
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
|
||||||
assert timelineDefinitions != null;
|
assert timelineDefinitions != null;
|
||||||
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
|
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
|
||||||
count = timelineDefinitions.size();
|
count = timelineDefinitions.size();
|
||||||
@@ -458,6 +460,12 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.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) {
|
private void updateSwitcherIcon(int i) {
|
||||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||||
@@ -693,6 +701,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
return fab;
|
return fab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProvideAssistContent(AssistContent assistContent) {
|
||||||
|
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
|
||||||
|
}
|
||||||
|
|
||||||
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
|
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -285,4 +286,9 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.HOME;
|
return Filter.FilterContext.HOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return base.path("/").build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageButton;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -168,4 +168,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.HOME;
|
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);
|
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.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -13,6 +14,12 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.LinearLayout;
|
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.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
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.TabLayout;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
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.fragments.BaseRecyclerFragment;
|
||||||
import me.grishka.appkit.utils.V;
|
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 TabLayout tabLayout;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
@@ -47,7 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||||
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(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>{
|
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -10,17 +11,23 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
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.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
import org.joinmastodon.android.model.Filter;
|
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.Notification;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
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.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||||
@@ -151,13 +158,17 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
loadRelationships(needRelationships);
|
loadRelationships(needRelationships);
|
||||||
maxID=result.maxID;
|
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());
|
E.post(new AllNotificationsSeenEvent());
|
||||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||||
if (AccountSessionManager.getInstance().getAccount(accountID).markers != null)
|
AccountSessionManager.getInstance().getAccount(accountID).markers
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).markers
|
.notifications.lastReadId = result.items.get(0).id;
|
||||||
.notifications.lastReadId = result.items.get(0).id;
|
|
||||||
AccountSessionManager.getInstance().writeAccountsFile();
|
AccountSessionManager.getInstance().writeAccountsFile();
|
||||||
|
|
||||||
|
if (isInstanceAkkoma()) {
|
||||||
|
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -184,23 +195,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
public void onItemClick(String id){
|
public void onItemClick(String id){
|
||||||
Notification n=getNotificationByID(id);
|
Notification n=getNotificationByID(id);
|
||||||
if(n.status!=null){
|
Bundle args = new Bundle();
|
||||||
Status status=n.status;
|
if(n.status != null && n.status.inReplyToAccountId != null && knownAccounts.containsKey(n.status.inReplyToAccountId))
|
||||||
Bundle args=new Bundle();
|
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(n.status.inReplyToAccountId)));
|
||||||
args.putString("account", accountID);
|
UiUtils.showFragmentForNotification(getContext(), n, accountID, args);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -232,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
|
@Subscribe
|
||||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||||
@@ -264,4 +288,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
displayItems.subList(index, lastIndex).clear();
|
displayItems.subList(index, lastIndex).clear();
|
||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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
|
@Override
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
|
|||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -31,7 +32,6 @@ import android.view.ViewOutlineProvider;
|
|||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
@@ -45,6 +45,7 @@ import android.widget.Toolbar;
|
|||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
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.GetAccountByID;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
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.LinkedTextView;
|
||||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -91,6 +93,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
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.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 AVATAR_RESULT=722;
|
||||||
private static final int COVER_RESULT=343;
|
private static final int COVER_RESULT=343;
|
||||||
|
|
||||||
@@ -135,8 +138,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private TextView followsYouView;
|
private TextView followsYouView;
|
||||||
private ViewGroup rolesView;
|
private ViewGroup rolesView;
|
||||||
|
|
||||||
private Account account;
|
private Account account, remoteAccount;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
|
private String domain;
|
||||||
private Relationship relationship;
|
private Relationship relationship;
|
||||||
private int statusBarHeight;
|
private int statusBarHeight;
|
||||||
private boolean isOwnProfile;
|
private boolean isOwnProfile;
|
||||||
@@ -151,7 +155,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private PhotoViewer currentPhotoViewer;
|
private PhotoViewer currentPhotoViewer;
|
||||||
private boolean editModeLoading;
|
private boolean editModeLoading;
|
||||||
|
|
||||||
private static final int MAX_FIELDS=4;
|
private int maxFields = 4;
|
||||||
|
|
||||||
// from ProfileAboutFragment
|
// from ProfileAboutFragment
|
||||||
public UsableRecyclerView list;
|
public UsableRecyclerView list;
|
||||||
@@ -172,13 +176,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
|
|
||||||
accountID=getArguments().getString("account");
|
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"));
|
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||||
profileAccountID=account.id;
|
profileAccountID=account.id;
|
||||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||||
loaded=true;
|
loaded=true;
|
||||||
if(!isOwnProfile)
|
if(!isOwnProfile)
|
||||||
loadRelationship();
|
loadRelationship();
|
||||||
|
else if (isInstanceAkkoma() && getInstance().isPresent())
|
||||||
|
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
|
||||||
}else{
|
}else{
|
||||||
profileAccountID=getArguments().getString("profileAccountID");
|
profileAccountID=getArguments().getString("profileAccountID");
|
||||||
if(!getArguments().getBoolean("noAutoLoad", false))
|
if(!getArguments().getBoolean("noAutoLoad", false))
|
||||||
@@ -324,7 +335,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
username.setOnLongClickListener(v->{
|
username.setOnLongClickListener(v->{
|
||||||
String usernameString=account.acct;
|
String usernameString=account.acct;
|
||||||
if(!usernameString.contains("@")){
|
if(!usernameString.contains("@")){
|
||||||
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
usernameString+="@"+domain;
|
||||||
}
|
}
|
||||||
UiUtils.copyText(username, '@'+usernameString);
|
UiUtils.copyText(username, '@'+usernameString);
|
||||||
return true;
|
return true;
|
||||||
@@ -341,36 +352,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return sizeWrapper;
|
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
|
@Override
|
||||||
protected void doLoadData(){
|
protected void doLoadData(){
|
||||||
|
if (remoteAccount != null) {
|
||||||
|
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
|
||||||
|
if (getContext() == null) return;
|
||||||
|
if (args == null || !args.containsKey("profileAccount")) {
|
||||||
|
Toast.makeText(getContext(), getContext().getString(
|
||||||
|
R.string.sk_error_loading_profile, domain
|
||||||
|
), Toast.LENGTH_SHORT).show();
|
||||||
|
Nav.finish(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
currentRequest=new GetAccountByID(profileAccountID)
|
currentRequest=new GetAccountByID(profileAccountID)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Account result){
|
public void onSuccess(Account result){
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
account=result;
|
onAccountLoaded(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);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.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(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));
|
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||||
name.setText(ssb);
|
name.setText(ssb);
|
||||||
setTitle(ssb);
|
setTitle(ssb);
|
||||||
|
|
||||||
if (account.roles != null && !account.roles.isEmpty()) {
|
if (account.roles != null && !account.roles.isEmpty()) {
|
||||||
rolesView.setVisibility(View.VISIBLE);
|
rolesView.setVisibility(View.VISIBLE);
|
||||||
@@ -505,13 +535,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
|
|
||||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||||
|
|
||||||
|
String acct = ((isSelf || account.isRemote)
|
||||||
|
? account.getFullyQualifiedName()
|
||||||
|
: account.acct);
|
||||||
if(account.locked){
|
if(account.locked){
|
||||||
ssb=new SpannableStringBuilder("@");
|
ssb=new SpannableStringBuilder("@");
|
||||||
ssb.append(account.acct);
|
ssb.append(acct);
|
||||||
if(isSelf){
|
|
||||||
ssb.append('@');
|
|
||||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
|
||||||
}
|
|
||||||
ssb.append(" ");
|
ssb.append(" ");
|
||||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
|
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
|
||||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||||
@@ -520,7 +549,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
username.setText(ssb);
|
username.setText(ssb);
|
||||||
}else{
|
}else{
|
||||||
// noinspection SetTextI18n
|
// 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);
|
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||||
if(TextUtils.isEmpty(parsedBio)){
|
if(TextUtils.isEmpty(parsedBio)){
|
||||||
@@ -590,6 +619,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean wantsToolbarMenuIconsTinted() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
if(isOwnProfile && isInEditMode){
|
if(isOwnProfile && isInEditMode){
|
||||||
@@ -620,8 +654,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
||||||
if(isOwnProfile)
|
if(isOwnProfile) {
|
||||||
|
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MenuItem mute = menu.findItem(R.id.mute);
|
MenuItem mute = menu.findItem(R.id.mute);
|
||||||
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||||
@@ -703,7 +739,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
args.putString("profileAccount", profileAccountID);
|
args.putString("profileAccount", profileAccountID);
|
||||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||||
}
|
}
|
||||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
Nav.go(getActivity(), ListsFragment.class, args);
|
||||||
}else if(id==R.id.followed_hashtags){
|
}else if(id==R.id.followed_hashtags){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
@@ -768,6 +804,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.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){
|
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||||
if(scrollY>avatarBorder.getTop()-topBarsH){
|
if(scrollY>avatarBorder.getTop()-topBarsH){
|
||||||
@@ -1159,6 +1201,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if (adapter != null) adapter.notifyDataSetChanged();
|
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 {
|
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
|
||||||
public MetadataAdapter(){
|
public MetadataAdapter(){
|
||||||
super(imgLoader);
|
super(imgLoader);
|
||||||
@@ -1189,7 +1246,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
public int getItemCount(){
|
public int getItemCount(){
|
||||||
if(isInEditMode){
|
if(isInEditMode){
|
||||||
int size=metadataListData.size();
|
int size=metadataListData.size();
|
||||||
if(size<MAX_FIELDS)
|
if(size<maxFields)
|
||||||
size++;
|
size++;
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
@@ -1306,7 +1363,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(){
|
public void onClick(){
|
||||||
metadataListData.add(new AccountField());
|
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);
|
adapter.notifyItemChanged(metadataListData.size()-1);
|
||||||
}else{
|
}else{
|
||||||
adapter.notifyItemInserted(metadataListData.size()-1);
|
adapter.notifyItemInserted(metadataListData.size()-1);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -181,4 +182,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
|||||||
}
|
}
|
||||||
return null;
|
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 android.view.ViewTreeObserver;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.utils.V;
|
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
public interface ScrollableToTop{
|
public interface ScrollableToTop{
|
||||||
void scrollToTop();
|
void scrollToTop();
|
||||||
@@ -21,7 +22,7 @@ public interface ScrollableToTop{
|
|||||||
@Override
|
@Override
|
||||||
public boolean onPreDraw(){
|
public boolean onPreDraw(){
|
||||||
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
list.scrollBy(0, V.dp(300));
|
list.scrollBy(0, UiUtils.SCROLL_TO_TOP_DELTA);
|
||||||
list.smoothScrollToPosition(0);
|
list.smoothScrollToPosition(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import android.content.Intent;
|
|||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.LruCache;
|
import android.util.LruCache;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
@@ -37,6 +37,7 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
|
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
|
||||||
import org.joinmastodon.android.MainActivity;
|
import org.joinmastodon.android.MainActivity;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
@@ -59,9 +60,11 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
|||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
@@ -78,13 +81,13 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class SettingsFragment extends MastodonToolbarFragment{
|
public class SettingsFragment extends MastodonToolbarFragment implements ProvidesAssistContent.ProvidesWebUri {
|
||||||
private UsableRecyclerView list;
|
private UsableRecyclerView list;
|
||||||
private ArrayList<Item> items=new ArrayList<>();
|
private ArrayList<Item> items=new ArrayList<>();
|
||||||
private ThemeItem themeItem;
|
private ThemeItem themeItem;
|
||||||
private NotificationPolicyItem notificationPolicyItem;
|
private NotificationPolicyItem notificationPolicyItem;
|
||||||
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
|
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
|
||||||
private ButtonItem defaultContentTypeButtonItem;
|
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private boolean needUpdateNotificationSettings;
|
private boolean needUpdateNotificationSettings;
|
||||||
private boolean needAppRestart;
|
private boolean needAppRestart;
|
||||||
@@ -105,7 +108,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
imageCache = ImageCache.getInstance(getActivity());
|
imageCache = ImageCache.getInstance(getActivity());
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
Optional<Instance> instance = session.getInstance();
|
||||||
String instanceName = UiUtils.getInstanceName(accountID);
|
String instanceName = UiUtils.getInstanceName(accountID);
|
||||||
|
|
||||||
if(GithubSelfUpdater.needSelfUpdating()){
|
if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
@@ -187,9 +190,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
GlobalUserPreferences.showInteractionCounts=i.checked;
|
GlobalUserPreferences.showInteractionCounts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
items.add(alwaysRevealSpoilersItem = new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
||||||
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
|
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
|
if (list.findViewHolderForAdapterPosition(items.indexOf(autoRevealSpoilersItem)) instanceof ButtonViewHolder bvh) bvh.rebind();
|
||||||
|
}));
|
||||||
|
items.add(autoRevealSpoilersItem = new ButtonItem(R.string.sk_settings_auto_reveal_equal_spoilers, R.drawable.ic_fluent_eye_24_regular, b->{
|
||||||
|
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||||
|
popupMenu.inflate(R.menu.settings_auto_reveal_spoiler);
|
||||||
|
popupMenu.setOnMenuItemClickListener(i -> onAutoRevealSpoilerClick(i, b));
|
||||||
|
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||||
|
b.setOnClickListener(v->popupMenu.show());
|
||||||
|
onAutoRevealSpoilerChanged(b);
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
|
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
|
||||||
GlobalUserPreferences.disableSwipe=i.checked;
|
GlobalUserPreferences.disableSwipe=i.checked;
|
||||||
@@ -217,13 +229,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
||||||
GlobalUserPreferences.save();
|
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 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->{
|
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.showReplies=i.checked;
|
||||||
GlobalUserPreferences.save();
|
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->{
|
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 popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||||
popupMenu.inflate(R.menu.reply_visibility);
|
popupMenu.inflate(R.menu.reply_visibility);
|
||||||
@@ -269,7 +286,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
GlobalUserPreferences.collapseLongPosts=i.checked;
|
GlobalUserPreferences.collapseLongPosts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_star_off_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
||||||
GlobalUserPreferences.spectatorMode=i.checked;
|
GlobalUserPreferences.spectatorMode=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
needAppRestart=true;
|
needAppRestart=true;
|
||||||
@@ -299,7 +316,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
needAppRestart=true;
|
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 ?
|
items.add(new SmallTextItem(getString(translationAvailable ?
|
||||||
R.string.sk_settings_translation_availability_note_available :
|
R.string.sk_settings_translation_availability_note_available :
|
||||||
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
|
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
|
||||||
@@ -324,16 +343,18 @@ 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 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 HeaderItem(instanceName));
|
||||||
items.add(new TextItem(R.string.sk_settings_rules, ()->{
|
items.add(new TextItem(R.string.sk_settings_rules, instance.<Runnable>map(i -> () -> {
|
||||||
Bundle args=new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putParcelable("instance", Parcels.wrap(instance));
|
args.putParcelable("instance", Parcels.wrap(i));
|
||||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
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.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_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.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));
|
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 HeaderItem(R.string.sk_instance_features));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{
|
items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{
|
||||||
@@ -361,16 +382,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
b.setText(getContentTypeString(contentType));
|
b.setText(getContentTypeString(contentType));
|
||||||
contentTypeMenu = popupMenu.getMenu();
|
contentTypeMenu = popupMenu.getMenu();
|
||||||
contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true);
|
contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true);
|
||||||
ContentType.adaptMenuToInstance(contentTypeMenu, instance);
|
instance.ifPresent(i -> ContentType.adaptMenuToInstance(contentTypeMenu, i));
|
||||||
contentTypeMenu.findItem(R.id.content_type_null).setVisible(
|
|
||||||
!GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID));
|
|
||||||
}));
|
}));
|
||||||
items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation)));
|
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->{
|
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
|
||||||
glitchModeItem.enabled = i.checked;
|
glitchModeItem.enabled = i.checked;
|
||||||
if (i.checked) {
|
if (i.checked) {
|
||||||
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
|
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
|
||||||
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
if (!isInstanceAkkoma()) {
|
||||||
|
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
|
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
|
||||||
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
|
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
|
||||||
@@ -520,6 +541,36 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean onAutoRevealSpoilerClick(MenuItem item, Button btn) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
|
||||||
|
AutoRevealMode mode = AutoRevealMode.NEVER;
|
||||||
|
if (id == R.id.auto_reveal_threads) mode = AutoRevealMode.THREADS;
|
||||||
|
else if (id == R.id.auto_reveal_discussions) mode = AutoRevealMode.DISCUSSIONS;
|
||||||
|
|
||||||
|
GlobalUserPreferences.alwaysExpandContentWarnings = false;
|
||||||
|
GlobalUserPreferences.autoRevealEqualSpoilers = mode;
|
||||||
|
GlobalUserPreferences.save();
|
||||||
|
onAutoRevealSpoilerChanged(btn);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAutoRevealSpoilerChanged(Button b) {
|
||||||
|
if (GlobalUserPreferences.alwaysExpandContentWarnings) {
|
||||||
|
b.setText(R.string.sk_settings_auto_reveal_anyone);
|
||||||
|
} else {
|
||||||
|
b.setText(switch(GlobalUserPreferences.autoRevealEqualSpoilers){
|
||||||
|
case THREADS -> R.string.sk_settings_auto_reveal_author;
|
||||||
|
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_anyone;
|
||||||
|
default -> R.string.sk_settings_auto_reveal_nobody;
|
||||||
|
});
|
||||||
|
if (alwaysRevealSpoilersItem.checked != GlobalUserPreferences.alwaysExpandContentWarnings) {
|
||||||
|
alwaysRevealSpoilersItem.checked = GlobalUserPreferences.alwaysExpandContentWarnings;
|
||||||
|
if (list.findViewHolderForAdapterPosition(items.indexOf(alwaysRevealSpoilersItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
@@ -549,14 +600,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
|
|
||||||
private boolean onContentTypeChanged(MenuItem item, Button btn){
|
private boolean onContentTypeChanged(MenuItem item, Button btn){
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
ContentType contentType = switch (id) {
|
|
||||||
case R.id.content_type_plain -> ContentType.PLAIN;
|
ContentType contentType = null;
|
||||||
case R.id.content_type_html -> ContentType.HTML;
|
if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
|
||||||
case R.id.content_type_markdown -> ContentType.MARKDOWN;
|
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
|
||||||
case R.id.content_type_bbcode -> ContentType.BBCODE;
|
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
|
||||||
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
|
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
|
||||||
default -> null;
|
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
|
||||||
};
|
|
||||||
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
|
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
btn.setText(getContentTypeString(contentType));
|
btn.setText(getContentTypeString(contentType));
|
||||||
@@ -736,6 +787,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{
|
private static abstract class Item{
|
||||||
public abstract int getViewType();
|
public abstract int getViewType();
|
||||||
}
|
}
|
||||||
@@ -819,7 +880,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class SmallTextItem extends Item {
|
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) {
|
public SmallTextItem(String text) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
@@ -1060,7 +1125,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
private final ImageView icon;
|
private final ImageView icon;
|
||||||
private final TextView text;
|
private final TextView text;
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
public ButtonViewHolder(){
|
public ButtonViewHolder(){
|
||||||
super(getActivity(), R.layout.item_settings_button, list);
|
super(getActivity(), R.layout.item_settings_button, list);
|
||||||
text=findViewById(R.id.text);
|
text=findViewById(R.id.text);
|
||||||
@@ -1068,14 +1132,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
button=findViewById(R.id.button);
|
button=findViewById(R.id.button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public void onBind(ButtonItem item){
|
public void onBind(ButtonItem item){
|
||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
if (item.icon == 0) {
|
icon.setVisibility(item.icon == 0 ? View.GONE : View.VISIBLE);
|
||||||
icon.setVisibility(View.GONE);
|
icon.setImageResource(item.icon == 0 ? 0 : item.icon);
|
||||||
} else {
|
// reset listeners before letting the button consumer consume the button
|
||||||
icon.setImageResource(item.icon);
|
// (and potentially set some listeners, but not others)
|
||||||
}
|
button.setOnTouchListener(null);
|
||||||
|
button.setOnClickListener(null);
|
||||||
|
button.setOnLongClickListener(null);
|
||||||
item.buttonConsumer.accept(button);
|
item.buttonConsumer.accept(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -24,13 +25,13 @@ import java.util.stream.Collectors;
|
|||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||||
private String id;
|
private String id, url;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
id=getArguments().getString("id");
|
id=getArguments().getString("id");
|
||||||
|
url=getArguments().getString("url");
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,4 +163,9 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return Uri.parse(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ import java.util.stream.Stream;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
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 EventListener eventListener=new EventListener();
|
||||||
|
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
@@ -67,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
status.filterRevealed = true;
|
status.filterRevealed = true;
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
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))
|
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||||
@@ -163,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
protected void removeStatus(Status status){
|
protected void removeStatus(Status status){
|
||||||
data.remove(status);
|
data.remove(status);
|
||||||
preloadedData.remove(status);
|
preloadedData.remove(status);
|
||||||
int index=-1;
|
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
|
||||||
for(int i=0;i<displayItems.size();i++){
|
for(int i=0;i<displayItems.size();i++){
|
||||||
if(status.id.equals(displayItems.get(i).parentID)){
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
if(status.id.equals(item.parentID)){
|
||||||
index=i;
|
index=i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (item.parentID.equals(status.inReplyToId)) {
|
||||||
|
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
|
||||||
|
ancestorLastIndex = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// did we find an ancestor that is also the status' neighbor?
|
||||||
|
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) {
|
||||||
|
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) {
|
||||||
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
// update ancestor to have no descendant anymore
|
||||||
|
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
|
||||||
|
}
|
||||||
|
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
if(index==-1)
|
if(index==-1)
|
||||||
return;
|
return;
|
||||||
int lastIndex;
|
int lastIndex;
|
||||||
@@ -182,7 +199,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig){
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
|
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,58 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
|
||||||
import org.joinmastodon.android.R;
|
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.requests.statuses.GetStatusContext;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Instance;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.StatusContext;
|
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.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
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.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
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.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
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.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ThreadFragment extends StatusListFragment{
|
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||||
protected Status mainStatus;
|
protected Status mainStatus, updatedStatus;
|
||||||
|
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||||
|
protected boolean contextInitiallyRendered;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -47,57 +69,79 @@ public class ThreadFragment extends StatusListFragment{
|
|||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||||
if(s.id.equals(mainStatus.id)){
|
// "what the fuck is a deque"? yes
|
||||||
for(StatusDisplayItem item:items){
|
// (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)
|
if(item instanceof TextStatusDisplayItem text)
|
||||||
text.textSelectable=true;
|
text.textSelectable=true;
|
||||||
else if(item instanceof FooterStatusDisplayItem footer)
|
else if(item instanceof FooterStatusDisplayItem footer)
|
||||||
footer.hideCounts=true;
|
footer.hideCounts=true;
|
||||||
}
|
}
|
||||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
|
}
|
||||||
|
|
||||||
|
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||||
|
if(s.id.equals(mainStatus.id)) {
|
||||||
|
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
|
if (refreshing) loadMainStatus();
|
||||||
currentRequest=new GetStatusContext(mainStatus.id)
|
currentRequest=new GetStatusContext(mainStatus.id)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(StatusContext result){
|
public void onSuccess(StatusContext result){
|
||||||
if (getActivity() == null) return;
|
if (getContext() == null) return;
|
||||||
|
Map<String, Status> oldData = null;
|
||||||
if(refreshing){
|
if(refreshing){
|
||||||
|
oldData = new HashMap<>(data.size());
|
||||||
|
for (Status s : data) oldData.put(s.id, s);
|
||||||
data.clear();
|
data.clear();
|
||||||
|
ancestryMap.clear();
|
||||||
displayItems.clear();
|
displayItems.clear();
|
||||||
data.add(mainStatus);
|
data.add(mainStatus);
|
||||||
onAppendItems(Collections.singletonList(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());
|
// TODO: figure out how this code works
|
||||||
result.descendants=getDescendantsOrdered(mainStatus.id,
|
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
|
||||||
result.descendants.stream()
|
|
||||||
.filter(s -> threadIds.contains(s.id))
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
result.descendants=filterStatuses(result.descendants);
|
result.descendants=filterStatuses(result.descendants);
|
||||||
result.ancestors=filterStatuses(result.ancestors);
|
result.ancestors=filterStatuses(result.ancestors);
|
||||||
|
|
||||||
|
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
|
||||||
|
ancestryMap.put(i.status.id, i);
|
||||||
|
}
|
||||||
|
|
||||||
if(footerProgress!=null)
|
if(footerProgress!=null)
|
||||||
footerProgress.setVisibility(View.GONE);
|
footerProgress.setVisibility(View.GONE);
|
||||||
data.addAll(result.descendants);
|
data.addAll(result.descendants);
|
||||||
@@ -106,19 +150,141 @@ public class ThreadFragment extends StatusListFragment{
|
|||||||
int count=displayItems.size();
|
int count=displayItems.size();
|
||||||
if(!refreshing)
|
if(!refreshing)
|
||||||
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
|
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--;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Status s : data) {
|
||||||
|
Status oldStatus = oldData == null ? null : oldData.get(s.id);
|
||||||
|
// restore previous spoiler/filter revealed states when refreshing
|
||||||
|
if (oldStatus != null) {
|
||||||
|
s.spoilerRevealed = oldStatus.spoilerRevealed;
|
||||||
|
s.filterRevealed = oldStatus.filterRevealed;
|
||||||
|
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
|
||||||
|
s.spoilerText != null &&
|
||||||
|
s.spoilerText.equals(mainStatus.spoilerText) &&
|
||||||
|
mainStatus.spoilerRevealed) {
|
||||||
|
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
|
||||||
|
s.spoilerRevealed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dataLoaded();
|
dataLoaded();
|
||||||
if(refreshing){
|
if(refreshing){
|
||||||
refreshDone();
|
refreshDone();
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
list.scrollToPosition(displayItems.size()-count);
|
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);
|
.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;
|
||||||
|
|
||||||
|
// restore revealed states for main status because it gets updated after doLoadData
|
||||||
|
updatedStatus.filterRevealed = mainStatus.filterRevealed;
|
||||||
|
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
|
||||||
|
|
||||||
|
// 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<>();
|
List<Status> out=new ArrayList<>();
|
||||||
for(Status s:getDirectDescendants(id, statuses)){
|
for(Status s:getDirectDescendants(id, statuses)){
|
||||||
out.add(s);
|
out.add(s);
|
||||||
@@ -130,7 +296,7 @@ public class ThreadFragment extends StatusListFragment{
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Status> getDirectDescendants(String id, List<Status> statuses){
|
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
|
||||||
return statuses.stream()
|
return statuses.stream()
|
||||||
.filter(s -> s.inReplyToId.equals(id))
|
.filter(s -> s.inReplyToId.equals(id))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -159,17 +325,78 @@ public class ThreadFragment extends StatusListFragment{
|
|||||||
showContent();
|
showContent();
|
||||||
if(!loaded)
|
if(!loaded)
|
||||||
footerProgress.setVisibility(View.VISIBLE);
|
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){
|
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||||
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
|
if (ev.status.inReplyToId == null) return;
|
||||||
onAppendItems(Collections.singletonList(ev.status));
|
Status repliedToStatus = getStatusByID(ev.status.inReplyToId);
|
||||||
|
if (repliedToStatus == null) return;
|
||||||
|
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
|
||||||
|
|
||||||
|
int nextDisplayItemsIndex = -1, indexOfPreviousDisplayItem = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < displayItems.size(); i++) {
|
||||||
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
if (repliedToStatus.id.equals(item.parentID)) {
|
||||||
|
// saving the replied-to status' display items index to eventually reach the last one
|
||||||
|
indexOfPreviousDisplayItem = i;
|
||||||
|
item.hasDescendantNeighbor = true;
|
||||||
|
} else if (indexOfPreviousDisplayItem >= 0 && nextDisplayItemsIndex == -1) {
|
||||||
|
// previous display item was the replied-to status' display items
|
||||||
|
nextDisplayItemsIndex = i;
|
||||||
|
// nothing left to do if there's no other reply to that status
|
||||||
|
if (ancestry.descendantNeighbor == null) break;
|
||||||
|
}
|
||||||
|
if (ancestry.descendantNeighbor != null && item.parentID.equals(ancestry.descendantNeighbor.id)) {
|
||||||
|
// existing reply shall no longer have the replied-to status as its neighbor
|
||||||
|
item.hasAncestoringNeighbor = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fall back to inserting the item at the end
|
||||||
|
nextDisplayItemsIndex = nextDisplayItemsIndex >= 0 ? nextDisplayItemsIndex : displayItems.size();
|
||||||
|
int nextDataIndex = data.indexOf(repliedToStatus) + 1;
|
||||||
|
|
||||||
|
// if replied-to status already has another reply...
|
||||||
|
if (ancestry.descendantNeighbor != null) {
|
||||||
|
// update the reply's ancestry to remove its ancestoring neighbor (as we did above)
|
||||||
|
ancestryMap.get(ancestry.descendantNeighbor.id).ancestoringNeighbor = null;
|
||||||
|
// make sure the existing reply has a reply line
|
||||||
|
if (nextDataIndex < data.size() &&
|
||||||
|
!(displayItems.get(nextDisplayItemsIndex) instanceof ReblogOrReplyLineStatusDisplayItem)) {
|
||||||
|
Status nextStatus = data.get(nextDataIndex);
|
||||||
|
if (!nextStatus.account.id.equals(repliedToStatus.account.id)) {
|
||||||
|
// create reply line manually since we're not building that status' items
|
||||||
|
displayItems.add(nextDisplayItemsIndex, StatusDisplayItem.buildReplyLine(
|
||||||
|
this, nextStatus, accountID, nextStatus, repliedToStatus.account, false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update replied-to status' ancestry
|
||||||
|
ancestry.descendantNeighbor = ev.status;
|
||||||
|
|
||||||
|
// add ancestry for newly created status before building its display items
|
||||||
|
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus));
|
||||||
|
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status));
|
||||||
|
data.add(nextDataIndex, ev.status);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isItemEnabled(String id){
|
public boolean isItemEnabled(String id){
|
||||||
return !id.equals(mainStatus.id);
|
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -187,4 +414,46 @@ public class ThreadFragment extends StatusListFragment{
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.THREAD;
|
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;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.joinmastodon.android.model.Account;
|
||||||
import org.parceler.Parcels;
|
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 Account account;
|
||||||
|
protected String initialSubtitle = "";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||||
|
if (getArguments().containsKey("remoteAccount")) {
|
||||||
|
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||||
|
}
|
||||||
setTitle("@"+account.acct);
|
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;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.drawable.Animatable;
|
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.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
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.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.RecyclerFragment;
|
import org.joinmastodon.android.fragments.RecyclerFragment;
|
||||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
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.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -44,6 +48,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.APIRequest;
|
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.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 HashMap<String, Relationship> relationships=new HashMap<>();
|
||||||
protected String accountID;
|
protected String accountID;
|
||||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||||
@@ -170,6 +175,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
super.onApplyWindowInsets(insets);
|
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{
|
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
public AccountsAdapter(){
|
public AccountsAdapter(){
|
||||||
super(imgLoader);
|
super(imgLoader);
|
||||||
@@ -230,16 +245,19 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
@Override
|
@Override
|
||||||
public void onBind(AccountItem item){
|
public void onBind(AccountItem item){
|
||||||
name.setText(item.parsedName);
|
name.setText(item.parsedName);
|
||||||
username.setText("@"+item.account.acct);
|
username.setText("@"+ (item.account.isRemote
|
||||||
|
? item.account.getFullyQualifiedName()
|
||||||
|
: item.account.acct));
|
||||||
bindRelationship();
|
bindRelationship();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bindRelationship(){
|
public void bindRelationship(){
|
||||||
Relationship rel=relationships.get(item.account.id);
|
Relationship rel=relationships.get(item.account.id);
|
||||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||||
button.setVisibility(View.GONE);
|
button.setVisibility(View.GONE);
|
||||||
}else{
|
}else{
|
||||||
button.setVisibility(View.VISIBLE);
|
button.setVisibility(View.VISIBLE);
|
||||||
@@ -269,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
public void onClick(){
|
public void onClick(){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
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);
|
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +406,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putString("profileAccount", account.id);
|
args.putString("profileAccount", account.id);
|
||||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
Nav.go(getActivity(), ListsFragment.class, args);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -410,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
emojiHelper=new CustomEmojiHelper();
|
emojiHelper=new CustomEmojiHelper();
|
||||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
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;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -12,11 +13,17 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(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
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
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;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -12,11 +13,17 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(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
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
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;
|
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.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
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.api.SimpleCallback;
|
||||||
|
|
||||||
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
|
public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
|
||||||
private String nextMaxID;
|
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);
|
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
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
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){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Account> result){
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
|
||||||
|
Collection<AccountItem> d = justRefreshed ? List.of() : data;
|
||||||
|
|
||||||
if(result.nextPageUri!=null)
|
if(result.nextPageUri!=null)
|
||||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
else
|
else
|
||||||
nextMaxID=null;
|
nextMaxID=null;
|
||||||
if (getActivity() == null) return;
|
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
|
@Override
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
package org.joinmastodon.android.fragments.account_list;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
|
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(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));
|
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
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;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
|
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(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));
|
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
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;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.joinmastodon.android.model.Status;
|
||||||
import org.parceler.Parcels;
|
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 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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -16,6 +32,46 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean hasSubtitle(){
|
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.Rect;
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextUtils;
|
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.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -49,7 +51,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 String accountID;
|
||||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||||
private GetAccountRelationships relationshipsRequest;
|
private GetAccountRelationships relationshipsRequest;
|
||||||
@@ -145,6 +147,16 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
|
|||||||
return isRecyclerViewOnTop(list);
|
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{
|
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
public AccountsAdapter(){
|
public AccountsAdapter(){
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
@@ -19,12 +20,14 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.fragments.HomeFragment;
|
||||||
import org.joinmastodon.android.fragments.IsOnTop;
|
import org.joinmastodon.android.fragments.IsOnTop;
|
||||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -36,7 +39,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
|||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
import me.grishka.appkit.utils.V;
|
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 TabLayout tabLayout;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
@@ -49,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
private ProgressBar searchProgress;
|
private ProgressBar searchProgress;
|
||||||
|
|
||||||
private DiscoverPostsFragment postsFragment;
|
private DiscoverPostsFragment postsFragment;
|
||||||
private TrendingHashtagsFragment hashtagsFragment;
|
private DiscoverHashtagsFragment hashtagsFragment;
|
||||||
private DiscoverNewsFragment newsFragment;
|
private DiscoverNewsFragment newsFragment;
|
||||||
private DiscoverAccountsFragment accountsFragment;
|
private DiscoverAccountsFragment accountsFragment;
|
||||||
private SearchFragment searchFragment;
|
private SearchFragment searchFragment;
|
||||||
@@ -117,7 +120,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
postsFragment=new DiscoverPostsFragment();
|
postsFragment=new DiscoverPostsFragment();
|
||||||
postsFragment.setArguments(args);
|
postsFragment.setArguments(args);
|
||||||
|
|
||||||
hashtagsFragment=new TrendingHashtagsFragment();
|
hashtagsFragment=new DiscoverHashtagsFragment();
|
||||||
hashtagsFragment.setArguments(args);
|
hashtagsFragment.setArguments(args);
|
||||||
|
|
||||||
newsFragment=new DiscoverNewsFragment();
|
newsFragment=new DiscoverNewsFragment();
|
||||||
@@ -238,7 +241,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
else scrollToTop();
|
else scrollToTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectSearch() {
|
public void selectSearch() {
|
||||||
searchEdit.requestFocus();
|
searchEdit.requestFocus();
|
||||||
onSearchEditFocusChanged(searchEdit, true);
|
onSearchEditFocusChanged(searchEdit, true);
|
||||||
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
|
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
|
||||||
@@ -272,6 +275,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
searchBack.setEnabled(false);
|
searchBack.setEnabled(false);
|
||||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
|
||||||
|
if (getArguments().getBoolean("disableDiscover"))
|
||||||
|
((HomeFragment) getParentFragment()).onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -318,6 +323,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
|
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>{
|
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
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.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.DiscoverInfoBannerHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.HashtagChartView;
|
import org.joinmastodon.android.ui.views.HashtagChartView;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -24,11 +29,11 @@ import me.grishka.appkit.api.SimpleCallback;
|
|||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 String accountID;
|
||||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
|
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
|
||||||
|
|
||||||
public TrendingHashtagsFragment(){
|
public DiscoverHashtagsFragment(){
|
||||||
super(10);
|
super(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +78,16 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
|||||||
return isRecyclerViewOnTop(list);
|
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>{
|
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@@ -105,6 +120,14 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
|
|||||||
@Override
|
@Override
|
||||||
public void onBind(Hashtag item){
|
public void onBind(Hashtag item){
|
||||||
title.setText('#'+item.name);
|
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;
|
int numPeople=item.history.get(0).accounts;
|
||||||
if(item.history.size()>1)
|
if(item.history.size()>1)
|
||||||
numPeople+=item.history.get(1).accounts;
|
numPeople+=item.history.get(1).accounts;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
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.drawables.BlurhashCrossfadeDrawable;
|
||||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -35,7 +37,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
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 String accountID;
|
||||||
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
|
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
|
||||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
|
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
|
||||||
@@ -88,6 +90,16 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
|
|||||||
return isRecyclerViewOnTop(list);
|
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{
|
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
public LinksAdapter(){
|
public LinksAdapter(){
|
||||||
super(imgLoader);
|
super(imgLoader);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
||||||
import org.joinmastodon.android.fragments.IsOnTop;
|
|
||||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
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);
|
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,14 +38,13 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
|||||||
bannerHelper.maybeAddBanner(contentWrap);
|
bannerHelper.maybeAddBanner(contentWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOnTop() {
|
|
||||||
return isRecyclerViewOnTop(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.PUBLIC;
|
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;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -25,7 +26,6 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
||||||
@@ -52,4 +52,9 @@ public class FederatedTimelineFragment extends StatusListFragment {
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.PUBLIC;
|
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;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
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.fragments.StatusListFragment;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
@@ -24,7 +26,6 @@ public class LocalTimelineFragment extends StatusListFragment {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
||||||
@@ -51,4 +52,9 @@ public class LocalTimelineFragment extends StatusListFragment {
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.PUBLIC;
|
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;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -11,7 +12,6 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.fragments.IsOnTop;
|
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
@@ -42,7 +42,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
|||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
|
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
|
||||||
private String currentQuery;
|
private String currentQuery;
|
||||||
private List<StatusDisplayItem> prevDisplayItems;
|
private List<StatusDisplayItem> prevDisplayItems;
|
||||||
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
||||||
@@ -312,8 +312,11 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnTop() {
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
return isRecyclerViewOnTop(list);
|
Uri.Builder searchUri = base.path("/search");
|
||||||
|
return isInstanceAkkoma()
|
||||||
|
? searchUri.appendQueryParameter("query", currentQuery).build()
|
||||||
|
: searchUri.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
|
|||||||
@@ -89,13 +89,6 @@ public class AccountActivationFragment extends ToolbarFragment{
|
|||||||
return !UiUtils.isDarkTheme();
|
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
|
@Override
|
||||||
protected void onUpdateToolbar(){
|
protected void onUpdateToolbar(){
|
||||||
super.onUpdateToolbar();
|
super.onUpdateToolbar();
|
||||||
@@ -110,7 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onToolbarNavigationClick(){
|
public void onToolbarNavigationClick(){
|
||||||
new AccountSwitcherSheet(getActivity()).show();
|
new AccountSwitcherSheet(getActivity(), null).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
|
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
|
||||||
list.setItemAnimator(new BetterItemAnimator());
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
|
((UsableRecyclerView) list).setSelector(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package org.joinmastodon.android.fragments.onboarding;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.assist.AssistContent;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Html;
|
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.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||||
|
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.FragmentRootLinearLayout;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public class InstanceRulesFragment extends ToolbarFragment{
|
public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAssistContent {
|
||||||
private UsableRecyclerView list;
|
private UsableRecyclerView list;
|
||||||
private MergeRecyclerAdapter adapter;
|
private MergeRecyclerAdapter adapter;
|
||||||
private Button btn;
|
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>{
|
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
@@ -267,4 +268,11 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
|||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return null;
|
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;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
@@ -62,7 +65,6 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
/**
|
/**
|
||||||
* An image banner that is shown above the profile and in profile cards.
|
* An image banner that is shown above the profile and in profile cards.
|
||||||
*/
|
*/
|
||||||
@RequiredField
|
|
||||||
public String header;
|
public String header;
|
||||||
/**
|
/**
|
||||||
* A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.
|
* A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.
|
||||||
@@ -135,6 +137,8 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
|
|
||||||
public List<Role> roles;
|
public List<Role> roles;
|
||||||
|
|
||||||
|
public @Nullable String fqn; // akkoma has this, mastodon't
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getQuery() {
|
public String getQuery() {
|
||||||
return url;
|
return url;
|
||||||
@@ -162,6 +166,7 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
moved.postprocess();
|
moved.postprocess();
|
||||||
if(TextUtils.isEmpty(displayName))
|
if(TextUtils.isEmpty(displayName))
|
||||||
displayName=username;
|
displayName=username;
|
||||||
|
if(fqn == null) fqn = getFullyQualifiedName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLocal(){
|
public boolean isLocal(){
|
||||||
@@ -173,6 +178,10 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
return parts.length==1 ? null : parts[1];
|
return parts.length==1 ? null : parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDomainFromURL() {
|
||||||
|
return Uri.parse(url).getHost();
|
||||||
|
}
|
||||||
|
|
||||||
public String getDisplayUsername(){
|
public String getDisplayUsername(){
|
||||||
return '@'+acct;
|
return '@'+acct;
|
||||||
}
|
}
|
||||||
@@ -181,6 +190,10 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
return '@'+acct.split("@")[0];
|
return '@'+acct.split("@")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFullyQualifiedName() {
|
||||||
|
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(){
|
public String toString(){
|
||||||
return "Account{"+
|
return "Account{"+
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
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
|
@CallSuper
|
||||||
public void postprocess() throws ObjectValidationException{
|
public void postprocess() throws ObjectValidationException{
|
||||||
try{
|
try{
|
||||||
@@ -23,4 +32,14 @@ public abstract class BaseModel{
|
|||||||
}
|
}
|
||||||
}catch(IllegalAccessException ignore){}
|
}catch(IllegalAccessException ignore){}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object clone(){
|
||||||
|
try{
|
||||||
|
return super.clone();
|
||||||
|
}catch(CloneNotSupportedException x){
|
||||||
|
throw new RuntimeException(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.net.IDN;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public class Instance extends BaseModel{
|
public class Instance extends BaseModel{
|
||||||
@@ -86,6 +87,11 @@ public class Instance extends BaseModel{
|
|||||||
|
|
||||||
public Pleroma pleroma;
|
public Pleroma pleroma;
|
||||||
|
|
||||||
|
public PleromaPollLimits pollLimits;
|
||||||
|
|
||||||
|
/** like uri, but always without scheme and trailing slash */
|
||||||
|
public transient String normalizedUri;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postprocess() throws ObjectValidationException{
|
public void postprocess() throws ObjectValidationException{
|
||||||
super.postprocess();
|
super.postprocess();
|
||||||
@@ -95,6 +101,10 @@ public class Instance extends BaseModel{
|
|||||||
rules=Collections.emptyList();
|
rules=Collections.emptyList();
|
||||||
if(shortDescription==null)
|
if(shortDescription==null)
|
||||||
shortDescription="";
|
shortDescription="";
|
||||||
|
// akkoma says uri is "https://example.social" while just "example.social" on mastodon
|
||||||
|
normalizedUri = uri
|
||||||
|
.replaceFirst("^https://", "")
|
||||||
|
.replaceFirst("/$", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -134,6 +144,30 @@ public class Instance extends BaseModel{
|
|||||||
return ci;
|
return ci;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAkkoma() {
|
||||||
|
return pleroma != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPixelfed() {
|
||||||
|
return version.contains("compatible; Pixelfed");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@Parcel
|
||||||
public static class Rule{
|
public static class Rule{
|
||||||
public String id;
|
public String id;
|
||||||
@@ -198,6 +232,28 @@ public class Instance extends BaseModel{
|
|||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class Pleroma extends BaseModel {
|
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
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public PushSubscription clone(){
|
public PushSubscription clone(){
|
||||||
PushSubscription copy=null;
|
PushSubscription copy=(PushSubscription) super.clone();
|
||||||
try{
|
|
||||||
copy=(PushSubscription) super.clone();
|
|
||||||
}catch(CloneNotSupportedException ignore){}
|
|
||||||
copy.alerts=alerts.clone();
|
copy.alerts=alerts.clone();
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.model;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
import org.joinmastodon.android.model.Poll.Option;
|
import org.joinmastodon.android.model.Poll.Option;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
@@ -16,7 +17,6 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
|||||||
public Instant scheduledAt;
|
public Instant scheduledAt;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public Params params;
|
public Params params;
|
||||||
@RequiredField
|
|
||||||
public List<Attachment> mediaAttachments;
|
public List<Attachment> mediaAttachments;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -24,8 +24,17 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess() throws ObjectValidationException {
|
||||||
|
super.postprocess();
|
||||||
|
if (mediaAttachments == null) mediaAttachments = List.of();
|
||||||
|
for(Attachment a:mediaAttachments)
|
||||||
|
a.postprocess();
|
||||||
|
if (params != null) params.postprocess();
|
||||||
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class Params {
|
public static class Params extends BaseModel {
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String text;
|
public String text;
|
||||||
public String spoilerText;
|
public String spoilerText;
|
||||||
@@ -40,10 +49,16 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
|||||||
public String applicationId;
|
public String applicationId;
|
||||||
public List<String> mediaIds;
|
public List<String> mediaIds;
|
||||||
public ContentType contentType;
|
public ContentType contentType;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess() throws ObjectValidationException {
|
||||||
|
super.postprocess();
|
||||||
|
if (poll != null) poll.postprocess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class ScheduledPoll {
|
public static class ScheduledPoll extends BaseModel {
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String expiresIn;
|
public String expiresIn;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.google.gson.JsonParseException;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
@@ -37,7 +38,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
public boolean sensitive;
|
public boolean sensitive;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String spoilerText;
|
public String spoilerText;
|
||||||
@RequiredField
|
|
||||||
public List<Attachment> mediaAttachments;
|
public List<Attachment> mediaAttachments;
|
||||||
public Application application;
|
public Application application;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
@@ -50,6 +50,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
public long favouritesCount;
|
public long favouritesCount;
|
||||||
public long repliesCount;
|
public long repliesCount;
|
||||||
public Instant editedAt;
|
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 List<FilterResult> filtered;
|
||||||
|
|
||||||
public String url;
|
public String url;
|
||||||
@@ -92,6 +94,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
t.postprocess();
|
t.postprocess();
|
||||||
for(Emoji e:emojis)
|
for(Emoji e:emojis)
|
||||||
e.postprocess();
|
e.postprocess();
|
||||||
|
if (mediaAttachments == null) mediaAttachments = List.of();
|
||||||
for(Attachment a:mediaAttachments)
|
for(Attachment a:mediaAttachments)
|
||||||
a.postprocess();
|
a.postprocess();
|
||||||
account.postprocess();
|
account.postprocess();
|
||||||
@@ -169,6 +172,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
return strippedText;
|
return strippedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReblogPermitted(String accountID){
|
||||||
|
return visibility.isReblogPermitted(account.id.equals(
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).self.id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
public static Status ofFake(String id, String text, Instant createdAt) {
|
public static Status ofFake(String id, String text, Instant createdAt) {
|
||||||
Status s = new Status();
|
Status s = new Status();
|
||||||
s.id = id;
|
s.id = id;
|
||||||
@@ -180,7 +189,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
s.mentions = List.of();
|
s.mentions = List.of();
|
||||||
s.tags = List.of();
|
s.tags = List.of();
|
||||||
s.emojis = List.of();
|
s.emojis = List.of();
|
||||||
s.filtered = List.of();
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +198,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class StatusDeserializer implements JsonDeserializer<Status> {
|
public static class StatusDeserializer implements JsonDeserializer<Status> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
JsonObject obj = json.getAsJsonObject();
|
JsonObject obj = json.getAsJsonObject();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public enum StatusPrivacy{
|
|||||||
@SerializedName("local")
|
@SerializedName("local")
|
||||||
LOCAL(4); // akkoma
|
LOCAL(4); // akkoma
|
||||||
|
|
||||||
private int privacy;
|
private final int privacy;
|
||||||
|
|
||||||
StatusPrivacy(int privacy) {
|
StatusPrivacy(int privacy) {
|
||||||
this.privacy = privacy;
|
this.privacy = privacy;
|
||||||
@@ -24,6 +24,13 @@ public enum StatusPrivacy{
|
|||||||
return privacy > other.getPrivacy();
|
return privacy > other.getPrivacy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReblogPermitted(boolean isOwnStatus){
|
||||||
|
return (this == StatusPrivacy.PUBLIC ||
|
||||||
|
this == StatusPrivacy.UNLISTED ||
|
||||||
|
this == StatusPrivacy.LOCAL ||
|
||||||
|
(this == StatusPrivacy.PRIVATE && isOwnStatus));
|
||||||
|
}
|
||||||
|
|
||||||
public int getPrivacy() {
|
public int getPrivacy() {
|
||||||
return privacy;
|
return privacy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,20 @@ import androidx.annotation.DrawableRes;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
import org.joinmastodon.android.BuildConfig;
|
|
||||||
import org.joinmastodon.android.R;
|
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.HashtagTimelineFragment;
|
||||||
import org.joinmastodon.android.fragments.HomeTimelineFragment;
|
import org.joinmastodon.android.fragments.HomeTimelineFragment;
|
||||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
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.FederatedTimelineFragment;
|
||||||
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
|
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class TimelineDefinition {
|
public class TimelineDefinition {
|
||||||
private TimelineType type;
|
private TimelineType type;
|
||||||
@@ -58,6 +61,14 @@ public class TimelineDefinition {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCompatible(AccountSession session) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wantsDefault(AccountSession session) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public String getTitle(Context ctx) {
|
public String getTitle(Context ctx) {
|
||||||
return title != null ? title : getDefaultTitle(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 POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
|
||||||
case LIST -> listTitle;
|
case LIST -> listTitle;
|
||||||
case HASHTAG -> hashtagName;
|
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 POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
|
||||||
case LIST -> Icon.LIST;
|
case LIST -> Icon.LIST;
|
||||||
case HASHTAG -> Icon.HASHTAG;
|
case HASHTAG -> Icon.HASHTAG;
|
||||||
|
case BUBBLE -> Icon.BUBBLE;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +113,7 @@ public class TimelineDefinition {
|
|||||||
case LIST -> new ListTimelineFragment();
|
case LIST -> new ListTimelineFragment();
|
||||||
case HASHTAG -> new HashtagTimelineFragment();
|
case HASHTAG -> new HashtagTimelineFragment();
|
||||||
case POST_NOTIFICATIONS -> new NotificationsListFragment();
|
case POST_NOTIFICATIONS -> new NotificationsListFragment();
|
||||||
|
case BUBBLE -> new BubbleTimelineFragment();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +170,7 @@ public class TimelineDefinition {
|
|||||||
return args;
|
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 {
|
public enum Icon {
|
||||||
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
|
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),
|
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),
|
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),
|
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 int iconRes, nameRes;
|
||||||
public final boolean hidden;
|
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 LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
|
||||||
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
|
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 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")
|
@Override
|
||||||
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
|
public boolean wantsDefault(AccountSession session) {
|
||||||
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
|
return session.getInstance()
|
||||||
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
|
.map(i -> i.hasFeature(Instance.Feature.BUBBLE_TIMELINE))
|
||||||
HOME_TIMELINE.copy(),
|
.orElse(false);
|
||||||
LOCAL_TIMELINE.copy(),
|
}
|
||||||
FEDERATED_TIMELINE.copy(),
|
};
|
||||||
POSTS_TIMELINE.copy()
|
|
||||||
|
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.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.ColorStateList;
|
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
@@ -14,7 +14,7 @@ import android.view.WindowInsets;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.RadioButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
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.requests.oauth.RevokeOauthToken;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
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.fragments.onboarding.CustomWelcomeFragment;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
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.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
@@ -49,13 +57,24 @@ import me.grishka.appkit.views.UsableRecyclerView;
|
|||||||
|
|
||||||
public class AccountSwitcherSheet extends BottomSheet{
|
public class AccountSwitcherSheet extends BottomSheet{
|
||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
|
private final HomeFragment fragment;
|
||||||
|
private final boolean externalShare, openInApp;
|
||||||
|
private BiConsumer<String, Boolean> onClick;
|
||||||
private UsableRecyclerView list;
|
private UsableRecyclerView list;
|
||||||
private List<WrappedAccount> accounts;
|
private List<WrappedAccount> accounts;
|
||||||
private ListImageLoaderWrapper imgLoader;
|
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);
|
super(activity);
|
||||||
this.activity=activity;
|
this.activity=activity;
|
||||||
|
this.fragment=fragment;
|
||||||
|
this.externalShare = externalShare;
|
||||||
|
this.openInApp = openInApp;
|
||||||
|
|
||||||
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||||
|
|
||||||
@@ -67,41 +86,63 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||||
View handle=new View(activity);
|
View handle=new View(activity);
|
||||||
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
|
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 SingleViewRecyclerAdapter(handle));
|
||||||
adapter.addAdapter(new AccountsAdapter());
|
|
||||||
AccountViewHolder holder=new AccountViewHolder();
|
if (externalShare) {
|
||||||
holder.more.setVisibility(View.GONE);
|
FrameLayout shareHeading = new FrameLayout(activity);
|
||||||
holder.currentIcon.setVisibility(View.GONE);
|
activity.getLayoutInflater().inflate(R.layout.item_external_share_heading, shareHeading);
|
||||||
holder.name.setText(R.string.add_account);
|
((TextView) shareHeading.findViewById(R.id.title)).setText(openInApp
|
||||||
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
|
? R.string.sk_external_share_or_open_title
|
||||||
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled);
|
: R.string.sk_external_share_title);
|
||||||
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
|
adapter.addAdapter(new SingleViewRecyclerAdapter(shareHeading));
|
||||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
|
|
||||||
Nav.go(activity, CustomWelcomeFragment.class, null);
|
setOnDismissListener((d) -> activity.finish());
|
||||||
dismiss();
|
}
|
||||||
}));
|
|
||||||
|
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);
|
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);
|
FrameLayout content=new FrameLayout(activity);
|
||||||
content.setBackgroundResource(R.drawable.bg_bottom_sheet);
|
content.setBackgroundResource(R.drawable.bg_bottom_sheet);
|
||||||
content.addView(list);
|
content.addView(list);
|
||||||
setContentView(content);
|
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){
|
private void confirmLogOut(String accountID){
|
||||||
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
new M3AlertDialogBuilder(activity)
|
new M3AlertDialogBuilder(activity)
|
||||||
.setTitle(R.string.log_out)
|
.setMessage(activity.getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||||
.setMessage(R.string.confirm_log_out)
|
|
||||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
|
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.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){
|
private void logOut(String accountID){
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||||
@@ -120,9 +161,55 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||||||
.exec(accountID);
|
.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){
|
private void onLoggedOut(String accountID){
|
||||||
AccountSessionManager.getInstance().removeAccount(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
|
@Override
|
||||||
@@ -140,6 +227,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{
|
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
public AccountsAdapter(){
|
public AccountsAdapter(){
|
||||||
super(imgLoader);
|
super(imgLoader);
|
||||||
@@ -173,45 +267,42 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
|
||||||
private final TextView name;
|
private final TextView name, username;
|
||||||
private final ImageView avatar;
|
private final ImageView avatar;
|
||||||
private final ImageButton more;
|
private final CheckableRelativeLayout view;
|
||||||
private final View currentIcon;
|
private final View radioButton, extraBtnWrap;
|
||||||
private final PopupMenu menu;
|
private final ImageButton extraBtn;
|
||||||
|
|
||||||
public AccountViewHolder(){
|
public AccountViewHolder(){
|
||||||
super(activity, R.layout.item_account_switcher, list);
|
super(activity, R.layout.item_account_switcher, list);
|
||||||
name=findViewById(R.id.name);
|
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);
|
avatar=findViewById(R.id.avatar);
|
||||||
more=findViewById(R.id.more);
|
avatar.setOutlineProvider(OutlineProviders.roundedRect(OutlineProviders.RADIUS_MEDIUM));
|
||||||
currentIcon=findViewById(R.id.current);
|
|
||||||
|
|
||||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
|
||||||
avatar.setClipToOutline(true);
|
avatar.setClipToOutline(true);
|
||||||
|
view=(CheckableRelativeLayout) itemView;
|
||||||
menu=new PopupMenu(activity, more);
|
extraBtnWrap = findViewById(R.id.extra_btn_wrap);
|
||||||
menu.inflate(R.menu.account_switcher);
|
extraBtn = findViewById(R.id.extra_btn);
|
||||||
menu.setOnMenuItemClickListener(item1 -> {
|
extraBtn.setOnClickListener(this::onExtraBtnClick);
|
||||||
confirmLogOut(item.getID());
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
more.setOnClickListener(v->menu.show());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
@Override
|
@Override
|
||||||
public void onBind(AccountSession item){
|
public void onBind(AccountSession item){
|
||||||
name.setText("@"+item.self.username+"@"+item.domain);
|
name.setText(item.self.displayName);
|
||||||
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
|
username.setText(item.getFullUsername());
|
||||||
more.setVisibility(View.GONE);
|
radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
|
||||||
currentIcon.setVisibility(View.VISIBLE);
|
extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);
|
||||||
}else{
|
if (externalShare) view.setCheckable(false);
|
||||||
more.setVisibility(View.VISIBLE);
|
else {
|
||||||
currentIcon.setVisibility(View.GONE);
|
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
|
@Override
|
||||||
@@ -226,12 +317,32 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||||||
setImage(index, null);
|
setImage(index, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onExtraBtnClick(View view) {
|
||||||
|
setOnDismissListener(null);
|
||||||
|
dismiss();
|
||||||
|
onClick.accept(item.getID(), true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(){
|
public void onClick(){
|
||||||
|
setOnDismissListener(null);
|
||||||
|
if (onClick != null) {
|
||||||
|
dismiss();
|
||||||
|
onClick.accept(item.getID(), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||||
activity.finish();
|
activity.finish();
|
||||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(){
|
||||||
|
if (externalShare) return false;
|
||||||
|
confirmLogOut(item.getID());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WrappedAccount{
|
private static class WrappedAccount{
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ package org.joinmastodon.android.ui;
|
|||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.animation.AnimatorListenerAdapter;
|
import android.animation.AnimatorListenerAdapter;
|
||||||
import android.animation.TimeInterpolator;
|
import android.animation.TimeInterpolator;
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewPropertyAnimator;
|
import android.view.ViewPropertyAnimator;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||||
|
|
||||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -358,7 +360,14 @@ public class BetterItemAnimator extends SimpleItemAnimator{
|
|||||||
mChangeAnimations.add(changeInfo.oldHolder);
|
mChangeAnimations.add(changeInfo.oldHolder);
|
||||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
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
|
@Override
|
||||||
public void onAnimationStart(Animator animator) {
|
public void onAnimationStart(Animator animator) {
|
||||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ import android.view.ViewOutlineProvider;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class OutlineProviders{
|
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(){
|
private OutlineProviders(){
|
||||||
//no instance
|
//no instance
|
||||||
@@ -21,6 +29,12 @@ public class OutlineProviders{
|
|||||||
outline.setAlpha(view.getAlpha());
|
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){
|
public static ViewOutlineProvider roundedRect(int dp){
|
||||||
ViewOutlineProvider provider=roundedRects.get(dp);
|
ViewOutlineProvider provider=roundedRects.get(dp);
|
||||||
@@ -31,6 +45,24 @@ public class OutlineProviders{
|
|||||||
return provider;
|
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 static class RoundRectOutlineProvider extends ViewOutlineProvider{
|
||||||
private final int radius;
|
private final int radius;
|
||||||
|
|
||||||
@@ -43,4 +75,34 @@ public class OutlineProviders{
|
|||||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ import me.grishka.appkit.Nav;
|
|||||||
|
|
||||||
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||||
public final Status status;
|
public final Status status;
|
||||||
|
public final String accountID;
|
||||||
|
|
||||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||||
|
|
||||||
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Status status){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.status=status;
|
this.status=status;
|
||||||
|
this.accountID=accountID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -72,7 +74,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public void onBind(ExtendedFooterStatusDisplayItem item){
|
public void onBind(ExtendedFooterStatusDisplayItem item){
|
||||||
Status s=item.status;
|
Status s=item.status;
|
||||||
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
||||||
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int)(s.reblogsCount%1000), s.reblogsCount));
|
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
|
||||||
|
reblogs.setVisibility(s.isReblogPermitted(item.accountID) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if(s.editedAt!=null){
|
if(s.editedAt!=null){
|
||||||
editHistory.setVisibility(View.VISIBLE);
|
editHistory.setVisibility(View.VISIBLE);
|
||||||
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));
|
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));
|
||||||
@@ -131,6 +135,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", item.parentFragment.getAccountID());
|
args.putString("account", item.parentFragment.getAccountID());
|
||||||
args.putString("id", item.status.id);
|
args.putString("id", item.status.id);
|
||||||
|
args.putString("url", item.status.url);
|
||||||
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
|
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.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -17,7 +16,6 @@ import android.view.animation.AlphaAnimation;
|
|||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
@@ -56,8 +54,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||||
private final TextView reply, boost, favorite, bookmark;
|
private final TextView replies, boosts, favorites;
|
||||||
private final ImageView share;
|
private final View reply, boost, favorite, share, bookmark;
|
||||||
private static final Animation opacityOut, opacityIn;
|
private static final Animation opacityOut, opacityIn;
|
||||||
|
|
||||||
private View touchingView = null;
|
private View touchingView = null;
|
||||||
@@ -91,22 +89,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(activity, R.layout.display_item_footer, parent);
|
super(activity, R.layout.display_item_footer, parent);
|
||||||
reply=findViewById(R.id.reply);
|
replies=findViewById(R.id.reply);
|
||||||
boost=findViewById(R.id.boost);
|
boosts=findViewById(R.id.boost);
|
||||||
favorite=findViewById(R.id.favorite);
|
favorites=findViewById(R.id.favorite);
|
||||||
bookmark=findViewById(R.id.bookmark);
|
|
||||||
share=findViewById(R.id.share);
|
reply=findViewById(R.id.reply_btn);
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
boost=findViewById(R.id.boost_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
favorite=findViewById(R.id.favorite_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
share=findViewById(R.id.share_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
bookmark=findViewById(R.id.bookmark_btn);
|
||||||
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);
|
|
||||||
reply.setOnTouchListener(this::onButtonTouch);
|
reply.setOnTouchListener(this::onButtonTouch);
|
||||||
reply.setOnClickListener(this::onReplyClick);
|
reply.setOnClickListener(this::onReplyClick);
|
||||||
reply.setOnLongClickListener(this::onReplyLongClick);
|
reply.setOnLongClickListener(this::onReplyLongClick);
|
||||||
@@ -131,18 +123,32 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(FooterStatusDisplayItem item){
|
public void onBind(FooterStatusDisplayItem item){
|
||||||
bindButton(reply, item.status.repliesCount);
|
bindText(replies, item.status.repliesCount);
|
||||||
bindButton(boost, item.status.reblogsCount);
|
bindText(boosts, item.status.reblogsCount);
|
||||||
bindButton(favorite, item.status.favouritesCount);
|
bindText(favorites, item.status.favouritesCount);
|
||||||
reply.setSelected(item.status.repliesCount > 0);
|
// 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);
|
boost.setSelected(item.status.reblogged);
|
||||||
favorite.setSelected(item.status.favourited);
|
favorite.setSelected(item.status.favourited);
|
||||||
bookmark.setSelected(item.status.bookmarked);
|
bookmark.setSelected(item.status.bookmarked);
|
||||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|
boost.setEnabled(item.status.isReblogPermitted(item.accountID));
|
||||||
|| (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){
|
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
||||||
btn.setText(UiUtils.abbreviateNumber(count));
|
btn.setText(UiUtils.abbreviateNumber(count));
|
||||||
btn.setCompoundDrawablePadding(V.dp(8));
|
btn.setCompoundDrawablePadding(V.dp(8));
|
||||||
@@ -166,8 +172,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||||
longClickPerformed = false;
|
longClickPerformed = false;
|
||||||
touchingView = v;
|
touchingView = v;
|
||||||
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
|
// 28dp to center in middle of icon, because:
|
||||||
v.setPivotX(V.dp(20));
|
// (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();
|
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
|
||||||
if (disabled) return true;
|
if (disabled) return true;
|
||||||
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
||||||
@@ -211,7 +218,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
private void boostConsumer(View v, Status r) {
|
private void boostConsumer(View v, Status r) {
|
||||||
v.startAnimation(opacityIn);
|
v.startAnimation(opacityIn);
|
||||||
bindButton(boost, r.reblogsCount);
|
bindText(boosts, r.reblogsCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean onBoostLongClick(View v){
|
private boolean onBoostLongClick(View v){
|
||||||
@@ -300,7 +307,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
favorite.setSelected(!item.status.favourited);
|
favorite.setSelected(!item.status.favourited);
|
||||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
||||||
v.startAnimation(opacityIn);
|
v.startAnimation(opacityIn);
|
||||||
bindButton(favorite, r.favouritesCount);
|
bindText(favorites, r.favouritesCount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.joinmastodon.android.ui.displayitems;
|
package org.joinmastodon.android.ui.displayitems;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -25,6 +27,13 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
|
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
|
||||||
private final TextView title, subtitle;
|
private final TextView title, subtitle;
|
||||||
private final HashtagChartView chart;
|
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){
|
public Holder(Context context, ViewGroup parent){
|
||||||
super(context, R.layout.item_trending_hashtag, parent);
|
super(context, R.layout.item_trending_hashtag, parent);
|
||||||
@@ -37,6 +46,14 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public void onBind(HashtagStatusDisplayItem _item){
|
public void onBind(HashtagStatusDisplayItem _item){
|
||||||
Hashtag item=_item.tag;
|
Hashtag item=_item.tag;
|
||||||
title.setText('#'+item.name);
|
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;
|
int numPeople=item.history.get(0).accounts;
|
||||||
if(item.history.size()>1)
|
if(item.history.size()>1)
|
||||||
numPeople+=item.history.get(1).accounts;
|
numPeople+=item.history.get(1).accounts;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package org.joinmastodon.android.ui.displayitems;
|
package org.joinmastodon.android.ui.displayitems;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
@@ -24,8 +22,6 @@ import android.widget.PopupMenu;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
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.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
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.NotificationsListFragment;
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
@@ -44,12 +40,10 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
|||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Announcement;
|
import org.joinmastodon.android.model.Announcement;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.model.ContentType;
|
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.model.ScheduledStatus;
|
import org.joinmastodon.android.model.ScheduledStatus;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.StatusPrivacy;
|
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
@@ -59,7 +53,6 @@ import java.time.Instant;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.FormatStyle;
|
import java.time.format.FormatStyle;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -203,7 +196,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
args.putBoolean("navigateToStatus", true);
|
args.putBoolean("navigateToStatus", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!redraft && TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
boolean isPixelfed = item.parentFragment.isInstancePixelfed();
|
||||||
|
boolean textEmpty = TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText);
|
||||||
|
if(!redraft && (isPixelfed || textEmpty)){
|
||||||
|
// pixelfed doesn't support /statuses/:id/source :/
|
||||||
|
if (isPixelfed) {
|
||||||
|
args.putString("sourceText", HtmlParser.text(item.status.content));
|
||||||
|
args.putString("sourceSpoiler", item.status.spoilerText);
|
||||||
|
}
|
||||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||||
}else if(item.scheduledStatus!=null){
|
}else if(item.scheduledStatus!=null){
|
||||||
args.putString("sourceText", item.status.text);
|
args.putString("sourceText", item.status.text);
|
||||||
@@ -284,7 +284,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
args.putString("account", item.parentFragment.getAccountID());
|
args.putString("account", item.parentFragment.getAccountID());
|
||||||
args.putString("profileAccount", account.id);
|
args.putString("profileAccount", account.id);
|
||||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||||
Nav.go(item.parentFragment.getActivity(), ListTimelinesFragment.class, args);
|
Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.animation.AnimatorListenerAdapter;
|
|||||||
import android.animation.AnimatorSet;
|
import android.animation.AnimatorSet;
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -17,7 +18,6 @@ import android.widget.ImageButton;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
@@ -40,7 +40,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
|
|||||||
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||||
private static final String TAG="MediaGridDisplayItem";
|
private static final String TAG="MediaGridDisplayItem";
|
||||||
|
|
||||||
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||||
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
|
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
|
||||||
private final List<Attachment> attachments;
|
private final List<Attachment> attachments;
|
||||||
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
||||||
@@ -98,6 +98,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
private int altTextIndex=-1;
|
private int altTextIndex=-1;
|
||||||
private Animator altTextAnimator;
|
private Animator altTextAnimator;
|
||||||
|
|
||||||
|
private boolean sizeUpdating = false;
|
||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
|
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
|
||||||
wrapper=(FrameLayout)itemView;
|
wrapper=(FrameLayout)itemView;
|
||||||
@@ -126,6 +128,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
layout.removeAllViews();
|
layout.removeAllViews();
|
||||||
controllers.clear();
|
controllers.clear();
|
||||||
|
|
||||||
int i=0;
|
int i=0;
|
||||||
for(Attachment att:item.attachments){
|
for(Attachment att:item.attachments){
|
||||||
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
|
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
|
||||||
@@ -158,6 +161,19 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setImage(int index, Drawable drawable){
|
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);
|
controllers.get(index).setImage(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,5 +330,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
layout.setClipChildren(clip);
|
layout.setClipChildren(clip);
|
||||||
wrapper.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) {
|
public void updateVisibility(StatusPrivacy visibility) {
|
||||||
this.visibility = visibility;
|
this.visibility = visibility;
|
||||||
this.iconEnd = visibility != null ? switch (visibility) {
|
this.iconEnd = visibility != null ? switch (visibility) {
|
||||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
case PUBLIC -> R.drawable.ic_fluent_earth_20sp_regular;
|
||||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
case UNLISTED -> R.drawable.ic_fluent_lock_open_20sp_regular;
|
||||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20sp_filled;
|
||||||
default -> 0;
|
default -> 0;
|
||||||
} : 0;
|
} : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.displayitems;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -26,6 +27,7 @@ import org.joinmastodon.android.model.ScheduledStatus;
|
|||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
@@ -45,6 +47,23 @@ public abstract class StatusDisplayItem{
|
|||||||
public final BaseStatusListFragment parentFragment;
|
public final BaseStatusListFragment parentFragment;
|
||||||
public boolean inset;
|
public boolean inset;
|
||||||
public int index;
|
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){
|
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||||
this.parentID=parentID;
|
this.parentID=parentID;
|
||||||
@@ -78,6 +97,7 @@ public abstract class StatusDisplayItem{
|
|||||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||||
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||||
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
||||||
|
case FILE -> new FileStatusDisplayItem.Holder(activity, parent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +105,21 @@ public abstract class StatusDisplayItem{
|
|||||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
|
||||||
|
String parentID = parent.getID();
|
||||||
|
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
|
||||||
|
: account == null ? fragment.getString(R.string.sk_in_reply)
|
||||||
|
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
|
||||||
|
: fragment.getString(R.string.in_reply_to, account.displayName);
|
||||||
|
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
|
||||||
|
: account == null ? fragment.getString(R.string.sk_in_reply)
|
||||||
|
: fragment.getString(R.string.in_reply_to, account.displayName);
|
||||||
|
return new ReblogOrReplyLineStatusDisplayItem(
|
||||||
|
parentID, fragment, text, account == null ? List.of() : account.emojis,
|
||||||
|
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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){
|
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();
|
String parentID=parentObject.getID();
|
||||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||||
@@ -94,34 +129,20 @@ public abstract class StatusDisplayItem{
|
|||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
||||||
|
|
||||||
if (!statusForContent.filterRevealed) {
|
|
||||||
statusForContent.filterRevealed = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN).test(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
||||||
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
||||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||||
|
|
||||||
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
||||||
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
||||||
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
|
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||||
: account == null ? fragment.getString(R.string.sk_in_reply)
|
|
||||||
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
|
|
||||||
: fragment.getString(R.string.in_reply_to, account.displayName);
|
|
||||||
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
|
|
||||||
: account == null ? fragment.getString(R.string.sk_in_reply)
|
|
||||||
: 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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(status.reblog!=null){
|
if(status.reblog!=null){
|
||||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||||
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
||||||
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
|
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));
|
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||||
}, fullText));
|
}, fullText));
|
||||||
@@ -136,7 +157,7 @@ public abstract class StatusDisplayItem{
|
|||||||
// post contains a hashtag the user is following
|
// post contains a hashtag the user is following
|
||||||
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||||
parentID, fragment, hashtag.name, List.of(),
|
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 -> {
|
i -> {
|
||||||
args.putString("hashtag", hashtag.name);
|
args.putString("hashtag", hashtag.name);
|
||||||
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
||||||
@@ -165,14 +186,32 @@ public abstract class StatusDisplayItem{
|
|||||||
items.add(replyLine);
|
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))
|
if(!TextUtils.isEmpty(statusForContent.content))
|
||||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
|
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)
|
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)
|
||||||
replyLine.needBottomPadding=true;
|
replyLine.needBottomPadding=true;
|
||||||
else
|
else
|
||||||
header.needBottomPadding=true;
|
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()){
|
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);
|
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||||
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
||||||
}
|
}
|
||||||
@@ -181,6 +220,12 @@ public abstract class StatusDisplayItem{
|
|||||||
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
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){
|
if(statusForContent.poll!=null){
|
||||||
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||||
}
|
}
|
||||||
@@ -196,8 +241,15 @@ public abstract class StatusDisplayItem{
|
|||||||
item.index=i++;
|
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 :
|
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)) {
|
if (addFooter && status.hasGapAfter && !(fragment instanceof ThreadFragment)) {
|
||||||
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
|
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
|
||||||
@@ -230,7 +282,8 @@ public abstract class StatusDisplayItem{
|
|||||||
GAP,
|
GAP,
|
||||||
EXTENDED_FOOTER,
|
EXTENDED_FOOTER,
|
||||||
MEDIA_GRID,
|
MEDIA_GRID,
|
||||||
WARNING
|
WARNING,
|
||||||
|
FILE
|
||||||
}
|
}
|
||||||
|
|
||||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
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);
|
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||||
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
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) {
|
if (!GlobalUserPreferences.collapseLongPosts) {
|
||||||
textScrollView.setLayoutParams(wrapParams);
|
textScrollView.setLayoutParams(wrapParams);
|
||||||
readMore.setVisibility(View.GONE);
|
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(
|
text.measure(
|
||||||
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
|
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
||||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||||
|
|
||||||
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
|
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -15,11 +16,13 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public boolean loading;
|
public boolean loading;
|
||||||
public final Status status;
|
public final Status status;
|
||||||
public List<StatusDisplayItem> filteredItems;
|
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);
|
super(parentID, parentFragment);
|
||||||
this.status=status;
|
this.status=status;
|
||||||
this.filteredItems = filteredItems;
|
this.filteredItems = filteredItems;
|
||||||
|
this.applyingFilter = applyingFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -41,7 +44,7 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
|||||||
@Override
|
@Override
|
||||||
public void onBind(WarningFilteredStatusDisplayItem item) {
|
public void onBind(WarningFilteredStatusDisplayItem item) {
|
||||||
filteredItems = item.filteredItems;
|
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
|
@Override
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.CornerPathEffect;
|
import android.graphics.CornerPathEffect;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.Rect;
|
import android.os.Build;
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
@@ -33,7 +32,8 @@ public class ClickableLinksDelegate {
|
|||||||
hlPaint=new Paint();
|
hlPaint=new Paint();
|
||||||
hlPaint.setAntiAlias(true);
|
hlPaint.setAntiAlias(true);
|
||||||
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
||||||
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
|
hlPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
hlPaint.setStrokeWidth(V.dp(4));
|
||||||
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
|
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ public class ClickableLinksDelegate {
|
|||||||
public void onDraw(Canvas canvas){
|
public void onDraw(Canvas canvas){
|
||||||
if(hlPath!=null){
|
if(hlPath!=null){
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate(0, view.getPaddingTop());
|
canvas.translate(view.getTotalPaddingLeft(), view.getTotalPaddingTop());
|
||||||
canvas.drawPath(hlPath, hlPaint);
|
canvas.drawPath(hlPath, hlPaint);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
@@ -73,59 +73,29 @@ public class ClickableLinksDelegate {
|
|||||||
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
|
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||||
@Override
|
@Override
|
||||||
public boolean onDown(@NonNull MotionEvent event) {
|
public boolean onDown(@NonNull MotionEvent event) {
|
||||||
int line=-1;
|
int padLeft=view.getTotalPaddingLeft(), padRight=view.getTotalPaddingRight(), padTop=view.getTotalPaddingTop(), padBottom=view.getTotalPaddingBottom();
|
||||||
Rect rect=new Rect();
|
float x=event.getX(), y=event.getY();
|
||||||
Layout l=view.getLayout();
|
if(x<padLeft || y<padTop || x>view.getWidth()-padRight || y>view.getHeight()-padBottom)
|
||||||
for(int i=0;i<l.getLineCount();i++){
|
|
||||||
view.getLineBounds(i, rect);
|
|
||||||
if(rect.contains((int)event.getX(), (int)event.getY())){
|
|
||||||
line=i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(line==-1){
|
|
||||||
return false;
|
return false;
|
||||||
}
|
x-=padLeft;
|
||||||
|
y-=padTop;
|
||||||
|
Layout l=view.getLayout();
|
||||||
|
int line=l.getLineForVertical(Math.round(y));
|
||||||
|
int position=l.getOffsetForHorizontal(line, x);
|
||||||
|
|
||||||
CharSequence text=view.getText();
|
CharSequence text=view.getText();
|
||||||
if(text instanceof Spanned s){
|
if(text instanceof Spanned s){
|
||||||
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
||||||
if(spans.length>0){
|
for(LinkSpan span:spans){
|
||||||
for(LinkSpan span:spans){
|
int start=s.getSpanStart(span);
|
||||||
int start=s.getSpanStart(span);
|
int end=s.getSpanEnd(span);
|
||||||
int end=s.getSpanEnd(span);
|
if(start<=position && end>position){
|
||||||
int lstart=l.getLineForOffset(start);
|
selectedSpan=span;
|
||||||
int lend=l.getLineForOffset(end);
|
hlPath=new Path();
|
||||||
if(line>=lstart && line<=lend){
|
l.getSelectionPath(start, end, hlPath);
|
||||||
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){
|
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
|
||||||
continue;
|
view.invalidate();
|
||||||
}
|
return true;
|
||||||
if(line==lend && event.getX()-view.getPaddingLeft()>l.getPrimaryHorizontal(end)){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
hlPath=new Path();
|
|
||||||
selectedSpan=span;
|
|
||||||
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
|
|
||||||
//l.getSelectionPath(start, end, hlPath);
|
|
||||||
for(int j=lstart;j<=lend;j++){
|
|
||||||
Rect bounds=new Rect();
|
|
||||||
l.getLineBounds(j, bounds);
|
|
||||||
//bounds.left+=view.getPaddingLeft();
|
|
||||||
if(j==lstart){
|
|
||||||
bounds.left=Math.round(l.getPrimaryHorizontal(start));
|
|
||||||
}
|
|
||||||
if(j==lend){
|
|
||||||
bounds.right=Math.round(l.getPrimaryHorizontal(end));
|
|
||||||
}else{
|
|
||||||
CharSequence lineChars=view.getText().subSequence(l.getLineStart(j), l.getLineEnd(j));
|
|
||||||
bounds.right=Math.round(view.getPaint().measureText(lineChars.toString()))/*+view.getPaddingRight()*/;
|
|
||||||
}
|
|
||||||
bounds.inset(V.dp(-2), V.dp(-2));
|
|
||||||
hlPath.addRect(new RectF(bounds), Path.Direction.CW);
|
|
||||||
}
|
|
||||||
hlPath.offset(view.getPaddingLeft(), 0);
|
|
||||||
view.invalidate();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package org.joinmastodon.android.ui.text;
|
package org.joinmastodon.android.ui.text;
|
||||||
|
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.fonts.FontFamily;
|
|
||||||
import android.graphics.fonts.FontStyle;
|
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.BackgroundColorSpan;
|
|
||||||
import android.text.style.BulletSpan;
|
import android.text.style.BulletSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.text.style.LeadingMarginSpan;
|
import android.text.style.LeadingMarginSpan;
|
||||||
import android.text.style.RelativeSizeSpan;
|
import android.text.style.RelativeSizeSpan;
|
||||||
import android.text.style.StrikethroughSpan;
|
import android.text.style.StrikethroughSpan;
|
||||||
@@ -17,13 +13,10 @@ import android.text.style.SubscriptSpan;
|
|||||||
import android.text.style.SuperscriptSpan;
|
import android.text.style.SuperscriptSpan;
|
||||||
import android.text.style.TypefaceSpan;
|
import android.text.style.TypefaceSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.twitter.twittertext.Regex;
|
import com.twitter.twittertext.Regex;
|
||||||
|
|
||||||
import org.joinmastodon.android.MastodonApp;
|
|
||||||
import org.joinmastodon.android.R;
|
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Mention;
|
import org.joinmastodon.android.model.Mention;
|
||||||
@@ -251,6 +244,10 @@ public class HtmlParser{
|
|||||||
return Jsoup.clean(html, Safelist.none());
|
return Jsoup.clean(html, Safelist.none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String text(String html) {
|
||||||
|
return Jsoup.parse(html).body().wholeText();
|
||||||
|
}
|
||||||
|
|
||||||
public static CharSequence parseLinks(String text){
|
public static CharSequence parseLinks(String text){
|
||||||
Matcher matcher=URL_PATTERN.matcher(text);
|
Matcher matcher=URL_PATTERN.matcher(text);
|
||||||
if(!matcher.find()) // Return the original string if there are no URLs
|
if(!matcher.find()) // Return the original string if there are no URLs
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public class DiscoverInfoBannerHelper{
|
|||||||
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
||||||
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
|
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
|
||||||
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
|
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
|
||||||
|
case BUBBLE_TIMELINE -> R.string.sk_bubble_timeline_info_banner;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,7 @@ public class DiscoverInfoBannerHelper{
|
|||||||
LOCAL_TIMELINE,
|
LOCAL_TIMELINE,
|
||||||
FEDERATED_TIMELINE,
|
FEDERATED_TIMELINE,
|
||||||
POST_NOTIFICATIONS,
|
POST_NOTIFICATIONS,
|
||||||
// ACCOUNTS
|
// ACCOUNTS,
|
||||||
|
BUBBLE_TIMELINE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Fragment;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
@@ -16,6 +17,7 @@ import android.content.DialogInterface;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@@ -36,6 +38,7 @@ import android.text.SpannableStringBuilder;
|
|||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@@ -53,6 +56,8 @@ import org.joinmastodon.android.E;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.StatusInteractionController;
|
import org.joinmastodon.android.api.StatusInteractionController;
|
||||||
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
||||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||||
@@ -97,6 +102,7 @@ import org.parceler.Parcels;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.IDN;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -109,6 +115,7 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -137,15 +144,11 @@ public class UiUtils {
|
|||||||
private static Handler mainHandler = new Handler(Looper.getMainLooper());
|
private static Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
|
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
|
||||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||||
public static int MAX_WIDTH;
|
public static int MAX_WIDTH, SCROLL_TO_TOP_DELTA;
|
||||||
|
|
||||||
private UiUtils() {
|
private UiUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadMaxWidth(Context ctx) {
|
|
||||||
if (MAX_WIDTH == 0) MAX_WIDTH = (int) ctx.getResources().getDimension(R.dimen.layout_max_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void launchWebBrowser(Context context, String url) {
|
public static void launchWebBrowser(Context context, String url) {
|
||||||
try {
|
try {
|
||||||
if (GlobalUserPreferences.useCustomTabs) {
|
if (GlobalUserPreferences.useCustomTabs) {
|
||||||
@@ -633,9 +636,9 @@ public class UiUtils {
|
|||||||
if (relationship.blocking) {
|
if (relationship.blocking) {
|
||||||
button.setText(R.string.button_blocked);
|
button.setText(R.string.button_blocked);
|
||||||
secondaryStyle = true;
|
secondaryStyle = true;
|
||||||
} else if (relationship.blockedBy) {
|
// } else if (relationship.blockedBy) {
|
||||||
button.setText(R.string.button_follow);
|
// button.setText(R.string.button_follow);
|
||||||
secondaryStyle = false;
|
// secondaryStyle = false;
|
||||||
} else if (relationship.requested) {
|
} else if (relationship.requested) {
|
||||||
button.setText(R.string.button_follow_pending);
|
button.setText(R.string.button_follow_pending);
|
||||||
secondaryStyle = true;
|
secondaryStyle = true;
|
||||||
@@ -649,7 +652,8 @@ public class UiUtils {
|
|||||||
|
|
||||||
if (keepText) button.setText(textBefore);
|
if (keepText) button.setText(textBefore);
|
||||||
|
|
||||||
button.setEnabled(!relationship.blockedBy);
|
// https://github.com/sk22/megalodon/issues/526
|
||||||
|
// button.setEnabled(!relationship.blockedBy);
|
||||||
int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
|
int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
|
||||||
TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
|
TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
|
||||||
int styleRes = ta.getResourceId(0, 0);
|
int styleRes = ta.getResourceId(0, 0);
|
||||||
@@ -893,6 +897,10 @@ public class UiUtils {
|
|||||||
|
|
||||||
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
||||||
if (palette != null) palette.apply(context);
|
if (palette != null) palette.apply(context);
|
||||||
|
|
||||||
|
Resources res = context.getResources();
|
||||||
|
MAX_WIDTH = (int) res.getDimension(R.dimen.layout_max_width);
|
||||||
|
SCROLL_TO_TOP_DELTA = (int) res.getDimension(R.dimen.scroll_to_top_delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isDarkTheme() {
|
public static boolean isDarkTheme() {
|
||||||
@@ -901,6 +909,32 @@ public class UiUtils {
|
|||||||
return theme == GlobalUserPreferences.ThemePreference.DARK;
|
return theme == GlobalUserPreferences.ThemePreference.DARK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Optional<Pair<String, Optional<String>>> parseFediverseHandle(String maybeFediHandle) {
|
||||||
|
// https://stackoverflow.com/a/26987741, except i put a + here ... v
|
||||||
|
String domainRegex = "^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]\\.)+(xn--)?([a-z0-9][a-z0-9\\-]{0,60}|[a-z0-9-]{1,30}\\.[a-z]{2,})$";
|
||||||
|
if (maybeFediHandle.toLowerCase().startsWith("mailto:")) {
|
||||||
|
maybeFediHandle = maybeFediHandle.substring("mailto:".length());
|
||||||
|
}
|
||||||
|
List<String> parts = Arrays.stream(maybeFediHandle.split("@"))
|
||||||
|
.filter(part -> !part.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (parts.size() == 0 || !parts.get(0).matches("^[^/\\s]+$")) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (parts.size() == 2) {
|
||||||
|
try {
|
||||||
|
String domain = IDN.toASCII(parts.get(1));
|
||||||
|
if (!domain.matches(domainRegex)) return Optional.empty();
|
||||||
|
return Optional.of(Pair.create(parts.get(0), Optional.of(parts.get(1))));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
} else if (maybeFediHandle.startsWith("@")) {
|
||||||
|
return Optional.of(Pair.create(parts.get(0), Optional.empty()));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://mastodon.foo.bar/@User
|
// https://mastodon.foo.bar/@User
|
||||||
// https://mastodon.foo.bar/@User/43456787654678
|
// https://mastodon.foo.bar/@User/43456787654678
|
||||||
// https://pleroma.foo.bar/users/User
|
// https://pleroma.foo.bar/users/User
|
||||||
@@ -917,7 +951,7 @@ public class UiUtils {
|
|||||||
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
|
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
|
||||||
//
|
//
|
||||||
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
|
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
|
||||||
public static boolean looksLikeMastodonUrl(String urlString) {
|
public static boolean looksLikeFediverseUrl(String urlString) {
|
||||||
URI uri;
|
URI uri;
|
||||||
try {
|
try {
|
||||||
uri = new URI(urlString);
|
uri = new URI(urlString);
|
||||||
@@ -946,8 +980,8 @@ public class UiUtils {
|
|||||||
|
|
||||||
public static String getInstanceName(String accountID) {
|
public static String getInstanceName(String accountID) {
|
||||||
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
Optional<Instance> instance = session.getInstance();
|
||||||
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
|
return instance.isPresent() && !instance.get().title.isBlank() ? instance.get().title : session.domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
|
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
|
||||||
@@ -1019,50 +1053,46 @@ public class UiUtils {
|
|||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
|
public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
|
||||||
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
|
return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
|
||||||
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
|
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
|
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
|
||||||
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
|
return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
|
||||||
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
|
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Searchable> void lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
|
public static <T extends Searchable> Optional<MastodonAPIRequest<SearchResults>> lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
|
||||||
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
|
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
|
||||||
resultConsumer.accept(query);
|
resultConsumer.accept(query);
|
||||||
return;
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
|
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults results) {
|
public void onSuccess(SearchResults results) {
|
||||||
Optional<T> result = extractResult.apply(results);
|
Optional<T> result = extractResult.apply(results);
|
||||||
if (result.isPresent()) resultConsumer.accept(result.get());
|
if (result.isPresent()) resultConsumer.accept(result.get());
|
||||||
else {
|
else {
|
||||||
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||||
resultConsumer.accept(null);
|
resultConsumer.accept(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(context);
|
error.showToast(context);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress((Activity) context, R.string.loading, true,
|
.wrapProgress((Activity) context, R.string.loading, true,
|
||||||
d -> transformDialogForLookup(context, targetAccountID, null, d))
|
d -> transformDialogForLookup(context, targetAccountID, null, d))
|
||||||
.exec(targetAccountID);
|
.exec(targetAccountID));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void openURL(Context context, String accountID, String url) {
|
public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
|
||||||
openURL(context, accountID, url, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
|
|
||||||
if (accountID != null) {
|
if (accountID != null) {
|
||||||
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
|
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
|
||||||
} else {
|
} else {
|
||||||
@@ -1077,33 +1107,114 @@ public class UiUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Bundle bundleError(String error) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("error", error);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bundle bundleError(ErrorResponse error) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
if (error instanceof MastodonErrorResponse e) {
|
||||||
|
args.putString("error", e.error);
|
||||||
|
args.putInt("httpStatus", e.httpStatus);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openURL(Context context, String accountID, String url) {
|
||||||
|
openURL(context, accountID, url, true);
|
||||||
|
}
|
||||||
|
|
||||||
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
|
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
|
||||||
|
lookupURL(context, accountID, url, (clazz, args) -> {
|
||||||
|
if (clazz == null) {
|
||||||
|
if (args != null && args.containsKey("error")) Toast.makeText(context, args.getString("error"), Toast.LENGTH_SHORT).show();
|
||||||
|
if (launchBrowser) launchWebBrowser(context, url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Nav.go((Activity) context, clazz, args);
|
||||||
|
}).map(req -> req.wrapProgress((Activity) context, R.string.loading, true, d ->
|
||||||
|
transformDialogForLookup(context, accountID, url, d)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean acctMatches(String accountID, String acct, String queriedUsername, @Nullable String queriedDomain) {
|
||||||
|
// check if the username matches
|
||||||
|
if (!acct.split("@")[0].equalsIgnoreCase(queriedUsername)) return false;
|
||||||
|
|
||||||
|
boolean resultOnHomeInstance = !acct.contains("@");
|
||||||
|
if (resultOnHomeInstance) {
|
||||||
|
// acct is formatted like 'someone'
|
||||||
|
// only allow home instance result if query didn't specify a domain,
|
||||||
|
// or the specified domain does, in fact, match the account session's domain
|
||||||
|
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
|
return queriedDomain == null || session.domain.equalsIgnoreCase(queriedDomain);
|
||||||
|
} else if (queriedDomain == null) {
|
||||||
|
// accept whatever result we have as there's no queried domain to compare to
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// acct is formatted like 'someone@somewhere'
|
||||||
|
return acct.split("@")[1].equalsIgnoreCase(queriedDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
||||||
|
return parseFediverseHandle(query).map(
|
||||||
|
handle -> lookupAccountHandle(context, accountID, handle, go))
|
||||||
|
.or(() -> {
|
||||||
|
go.accept(null, null);
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public static MastodonAPIRequest<SearchResults> lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
||||||
|
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
|
||||||
|
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
|
||||||
|
.setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SearchResults results) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Optional<Account> account = results.accounts.stream()
|
||||||
|
.filter(a -> acctMatches(accountID, a.acct, queryHandle.first, queryHandle.second.orElse(null)))
|
||||||
|
.findAny();
|
||||||
|
if (account.isPresent()) {
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
|
||||||
|
go.accept(ProfileFragment.class, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
go.accept(null, bundleError(error));
|
||||||
|
}
|
||||||
|
}).exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<MastodonAPIRequest<?>> lookupURL(Context context, String accountID, String url, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
List<String> path = uri.getPathSegments();
|
List<String> path = uri.getPathSegments();
|
||||||
if (accountID != null && "https".equals(uri.getScheme())) {
|
if (accountID != null && "https".equals(uri.getScheme())) {
|
||||||
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
|
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
|
||||||
new GetStatusByID(path.get(1))
|
return Optional.of(new GetStatusByID(path.get(1))
|
||||||
.setCallback(new Callback<>() {
|
.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result) {
|
public void onSuccess(Status result) {
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putParcelable("status", Parcels.wrap(result));
|
args.putParcelable("status", Parcels.wrap(result));
|
||||||
Nav.go((Activity) context, ThreadFragment.class, args);
|
go.accept(ThreadFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(context);
|
go.accept(null, bundleError(error));
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress((Activity) context, R.string.loading, true,
|
.exec(accountID));
|
||||||
d -> transformDialogForLookup(context, accountID, url, d))
|
} else if (looksLikeFediverseUrl(url)) {
|
||||||
.exec(accountID);
|
return Optional.of(new GetSearchResults(url, null, true)
|
||||||
return;
|
|
||||||
} else if (looksLikeMastodonUrl(url)) {
|
|
||||||
new GetSearchResults(url, null, true)
|
|
||||||
.setCallback(new Callback<>() {
|
.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults results) {
|
public void onSuccess(SearchResults results) {
|
||||||
@@ -1111,30 +1222,29 @@ public class UiUtils {
|
|||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
if (!results.statuses.isEmpty()) {
|
if (!results.statuses.isEmpty()) {
|
||||||
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
|
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
|
||||||
Nav.go((Activity) context, ThreadFragment.class, args);
|
go.accept(ThreadFragment.class, args);
|
||||||
} else if (!results.accounts.isEmpty()) {
|
return;
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
|
|
||||||
Nav.go((Activity) context, ProfileFragment.class, args);
|
|
||||||
} else {
|
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
else
|
|
||||||
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
}
|
||||||
|
Optional<Account> account = results.accounts.stream()
|
||||||
|
.filter(a -> uri.equals(Uri.parse(a.url))).findAny();
|
||||||
|
if (account.isPresent()) {
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
|
||||||
|
go.accept(ProfileFragment.class, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(context);
|
go.accept(null, bundleError(error));
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress((Activity) context, R.string.loading, true,
|
.exec(accountID));
|
||||||
d -> transformDialogForLookup(context, accountID, url, d))
|
|
||||||
.exec(accountID);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launchWebBrowser(context, url);
|
go.accept(null, null);
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void copyText(View v, String text) {
|
public static void copyText(View v, String text) {
|
||||||
@@ -1297,6 +1407,33 @@ public class UiUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showFragmentForNotification(Context context, Notification n, String accountID, Bundle extras) {
|
||||||
|
if (extras == null) extras = new Bundle();
|
||||||
|
extras.putString("account", accountID);
|
||||||
|
if (n.status!=null) {
|
||||||
|
Status status=n.status;
|
||||||
|
extras.putParcelable("status", Parcels.wrap(status.clone()));
|
||||||
|
Nav.go((Activity) context, ThreadFragment.class, extras);
|
||||||
|
} else if (n.report != null) {
|
||||||
|
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||||
|
UiUtils.launchWebBrowser(context, "https://"+domain+"/admin/reports/"+n.report.id);
|
||||||
|
} else if (n.account != null) {
|
||||||
|
extras.putString("account", accountID);
|
||||||
|
extras.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||||
|
Nav.go((Activity) context, ProfileFragment.class, extras);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the input value according to the device's scaled display density
|
||||||
|
* @param sp Input value in scale-independent pixels (sp)
|
||||||
|
* @return Scaled value in physical pixels (px)
|
||||||
|
*/
|
||||||
|
public static int sp(Context context, float sp){
|
||||||
|
// TODO: replace with V.sp in next AppKit version
|
||||||
|
return Math.round(sp*context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a View.OnClickListener to filter multiple clicks in succession.
|
* Wraps a View.OnClickListener to filter multiple clicks in succession.
|
||||||
* Useful for buttons that perform some action that changes their state asynchronously.
|
* Useful for buttons that perform some action that changes their state asynchronously.
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package org.joinmastodon.android.ui.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
|
import android.widget.Checkable;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
public class CheckableRelativeLayout extends RelativeLayout implements Checkable{
|
||||||
|
private boolean checked, checkable = true;
|
||||||
|
private static final int[] CHECKED_STATE_SET = {
|
||||||
|
android.R.attr.state_checked
|
||||||
|
};
|
||||||
|
|
||||||
|
public CheckableRelativeLayout(Context context){
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheckableRelativeLayout(Context context, AttributeSet attrs){
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle){
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChecked(boolean checked){
|
||||||
|
this.checked=checked;
|
||||||
|
refreshDrawableState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCheckable(boolean checkable) {
|
||||||
|
this.checkable = checkable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isChecked(){
|
||||||
|
return checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toggle(){
|
||||||
|
setChecked(!checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int[] onCreateDrawableState(int extraSpace) {
|
||||||
|
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||||
|
if (isChecked()) {
|
||||||
|
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
|
||||||
|
}
|
||||||
|
return drawableState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
|
||||||
|
super.onInitializeAccessibilityNodeInfo(info);
|
||||||
|
info.setCheckable(checkable);
|
||||||
|
info.setChecked(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ public class ComposeMediaLayout extends ViewGroup{
|
|||||||
|
|
||||||
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
|
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
UiUtils.loadMaxWidth(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package org.joinmastodon.android.ui.views;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
|
||||||
public class MaxWidthFrameLayout extends FrameLayout{
|
public class MaxWidthFrameLayout extends FrameLayout{
|
||||||
private int maxWidth;
|
private int maxWidth, defaultWidth;
|
||||||
|
|
||||||
public MaxWidthFrameLayout(Context context){
|
public MaxWidthFrameLayout(Context context){
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@@ -22,6 +23,7 @@ public class MaxWidthFrameLayout extends FrameLayout{
|
|||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
|
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
|
||||||
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
|
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
|
||||||
|
defaultWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_defaultWidth, -1);
|
||||||
ta.recycle();
|
ta.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,10 +35,19 @@ public class MaxWidthFrameLayout extends FrameLayout{
|
|||||||
this.maxWidth=maxWidth;
|
this.maxWidth=maxWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getDefaultWidth() {
|
||||||
|
return defaultWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultWidth(int defaultWidth) {
|
||||||
|
this.defaultWidth = defaultWidth;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||||
if(MeasureSpec.getSize(widthMeasureSpec)>maxWidth){
|
if(MeasureSpec.getSize(widthMeasureSpec)>maxWidth){
|
||||||
widthMeasureSpec=maxWidth | MeasureSpec.getMode(widthMeasureSpec);
|
int width = defaultWidth >= 0 ? defaultWidth : maxWidth;
|
||||||
|
widthMeasureSpec=width | MeasureSpec.getMode(widthMeasureSpec);
|
||||||
}
|
}
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user