Compare commits

..

128 Commits

Author SHA1 Message Date
sk
b94741feae boop version 2023-06-10 22:10:24 +02:00
sk
e43d6c35d8 update languages 2023-06-10 22:10:13 +02:00
sk
4a6f9e80b1 Merge remote-tracking branch 'weblate/main' 2023-06-10 22:03:41 +02:00
gallegonovato
ec02680507 Translated using Weblate (Spanish)
Currently translated at 99.6% (300 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-10 20:03:34 +00:00
sk22
5fc569a45a Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 20:03:34 +00:00
sk
4bc9c5691d Merge remote-tracking branch 'upstream/l10n_master' 2023-06-10 22:03:19 +02:00
sk
19b68855ac move permission definition to status privacy 2023-06-10 21:54:47 +02:00
sk
70fdfb612e fix issue on re-bind 2023-06-10 21:54:18 +02:00
sk
0a32c217d8 Merge branch 'main' into pr/FineFindus/557 2023-06-10 21:47:26 +02:00
Eugen Rochko
5dfa9237ad New translations short_description.txt (Persian) 2023-06-10 21:44:47 +02:00
sk
573ff75498 update ancestor when deleting post 2023-06-10 21:32:41 +02:00
sk
87c37df370 insert replied directly below status
closes sk22#558
2023-06-10 20:57:01 +02:00
sk22
7fb0944e66 Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 16:59:16 +00:00
sk
35c8a3d121 change strings 2023-06-10 18:59:05 +02:00
sk
9e58413d1a Merge remote-tracking branch 'weblate/main' 2023-06-10 18:54:32 +02:00
sk
90e60aef84 change auto-reveal cw wording
closes sk22#562
2023-06-10 18:53:38 +02:00
sk
8547ce05ed change auto-reveal cw wording
closes sk22#562
2023-06-10 18:52:01 +02:00
gicorada
0825faee5c Translated using Weblate (Italian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-06-10 16:42:23 +00:00
Linerly
d43a697df7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-10 16:42:23 +00:00
Choukajohn
3b742c4391 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
ihor_ck
a43a396043 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-10 16:42:23 +00:00
Choukajohn
bcb4fac553 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
sk
35bf858a83 auto-reveal equal spoilers in threads 2023-06-09 14:54:03 +02:00
sk
870bfaf08c don't use switch for android ids 2023-06-09 14:50:21 +02:00
sk
c4238fb19b keep revealed states when reloading
closes sk22#561
2023-06-09 13:27:25 +02:00
sk
ba7aeb358b fix thread status not clickable if filter revealed
closes sk22#554
2023-06-09 12:50:10 +02:00
sk
6f3fd4d454 fix opening browser twice
closes sk22#559
2023-06-09 12:47:32 +02:00
Eugen Rochko
c890195567 New translations strings.xml (Swedish) 2023-06-08 22:57:52 +02:00
Eugen Rochko
b50a327b17 New translations strings.xml (Swedish) 2023-06-08 17:47:06 +02:00
sk
97547f334f returning optionals for the optional god
closes sk22#555
2023-06-08 15:31:58 +02:00
sk
1ab953d819 fix spoiler button being hidden while editing
closes sk22#553
2023-06-08 15:03:04 +02:00
FineFindus
dbe7eb25ff Merge branch 'main' into feat/hide-non-boostable-boosts 2023-06-08 10:35:12 +02:00
FineFindus
45ecec09f5 feat: hide boost count on non-boostable statuses 2023-06-08 10:33:27 +02:00
FineFindus
9b4556d293 refactor: move boostable check to status 2023-06-08 10:33:27 +02:00
sk
307d483a56 add comment 2023-06-07 21:59:56 +02:00
sk
9612248695 remove unused imports 2023-06-07 21:51:17 +02:00
sk
1f63401e5b fix pixelfed post editing 2023-06-07 21:50:50 +02:00
sk
d35ec18a88 increase akkoma scheduled posts compatibility 2023-06-07 21:12:38 +02:00
sk
b93b1847c3 increase pixelfed compatibility 2023-06-07 21:12:30 +02:00
sk
cd46ed565f open browser if login redirects to website 2023-06-07 21:11:20 +02:00
sk
4a0e4edef8 load more if screen isn't full yet 2023-06-07 20:28:56 +02:00
sk
2ea7333daa only reload main status when refreshing
closes sk22#552
2023-06-07 18:45:50 +02:00
sk
fa7a66809d change profile loading error behavior 2023-06-07 16:09:41 +02:00
sk
71884ab760 don't show wrong relationship with remote accounts 2023-06-07 15:50:30 +02:00
sk
f31205c670 generalize lookup error handling 2023-06-07 15:45:22 +02:00
sk
0091ae87ce fix issues with external share
closes sk22#550
2023-06-07 15:17:44 +02:00
sk
ad13b1e927 bump version 2023-06-06 17:13:30 +02:00
sk
a354ea80ab Merge remote-tracking branch 'upstream/l10n_master' 2023-06-06 17:09:06 +02:00
sk22
9f65b8112a Translated using Weblate (German)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-06 15:08:53 +00:00
Choukajohn
6ac5d957fe Translated using Weblate (French)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-06 15:05:05 +00:00
sk
4258c55b88 implement fetching listings from remote instances 2023-06-06 17:04:29 +02:00
sk
969f29e2e9 support string res for small text item 2023-06-06 16:55:20 +02:00
sk
68921d0f0b fix footer being too close to next header 2023-06-06 14:23:34 +02:00
Eugen Rochko
c4ac4ee173 New translations strings.xml (Swedish) 2023-06-06 13:23:46 +02:00
Eugen Rochko
659b4e2fcd New translations strings.xml (Swedish) 2023-06-06 11:55:59 +02:00
sk
24e5bda8d3 fix profile options icons 2023-06-06 10:30:32 +02:00
Eugen Rochko
02b1ad8d7a New translations strings.xml (Swedish) 2023-06-06 00:56:52 +02:00
Eugen Rochko
47eeb01b75 New translations strings.xml (Turkish) 2023-06-05 21:08:28 +02:00
ihor_ck
4288814138 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-05 16:09:08 +00:00
sk
ac4458e106 fix badged toolbar icons with new appkit version 2023-06-05 18:07:22 +02:00
sk
3d24b2de10 add status counters event listener to notifications 2023-06-05 16:19:55 +02:00
sk
ed994b23e9 fix wrong views' states being modified
closes sk22#549
2023-06-05 16:12:07 +02:00
sk
8c4678aba5 fix updated main status not being applied 2023-06-05 16:05:58 +02:00
Eugen Rochko
3d5fb2dfea New translations strings.xml (Swedish) 2023-06-05 15:00:47 +02:00
Eugen Rochko
ef6238b593 New translations strings.xml (Swedish) 2023-06-05 13:45:02 +02:00
Eugen Rochko
bc9bec3d66 New translations strings.xml (Swedish) 2023-06-05 12:47:38 +02:00
sk
d16e199dd1 scroll up when posting on profile fragment
closes sk22#546
2023-06-05 11:48:40 +02:00
sk
a9c2df2e83 do copy spoilerRevealed on clone
closes sk22#547
2023-06-05 11:26:36 +02:00
sk
4673a4b9f7 add missing database column in post notification table 2023-06-05 11:22:01 +02:00
Grishka
d4a5286895 Fix #553 2023-06-04 23:47:08 +02:00
Grishka
1b4579346b Fix #548 2023-06-04 23:39:06 +02:00
sk
0665b8dd3b fix incompatibility with upstream bugfix 2023-06-04 23:33:04 +02:00
Grishka
853124e2ce Fix it again 2023-06-04 23:32:13 +02:00
Grishka
5dcd6e5a0d Fix it again 2023-06-04 23:31:12 +02:00
Grishka
6f25c8be0f Fix #583 2023-06-04 23:26:48 +02:00
sk
1db4b1319e use latest appkit version 2023-06-04 23:26:39 +02:00
Grishka
76a97fcb47 Fix #591 2023-06-04 23:20:29 +02:00
sk
4baaa39f35 bump version 2023-06-04 23:16:32 +02:00
sk
52f025ae5a Merge remote-tracking branch 'upstream/l10n_master' 2023-06-04 23:14:49 +02:00
sk22
14b805e883 Translated using Weblate (German)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-04 21:13:53 +00:00
sk
433a7b15fe change bubble string 2023-06-04 23:03:29 +02:00
sk
6c8cbbc34a Merge remote-tracking branch 'weblate/main' 2023-06-04 22:58:56 +02:00
sk
d4fbb298c1 use sp for reply line inline icons 2023-06-04 22:57:06 +02:00
sk
2aeb5f03d6 remove unused sp drawables 2023-06-04 22:32:54 +02:00
sk
6522403c37 fix footer text margins 2023-06-04 22:12:45 +02:00
sk
f090ca7f75 use sp for scaled footer 2023-06-04 21:08:45 +02:00
sk
2f02a238df refresh updated main status 2023-06-04 20:56:44 +02:00
sk
0d5fa97800 fix wrong index 2023-06-04 20:40:27 +02:00
sk
b102deaee1 don't let interaction counts go negative 2023-06-04 19:08:18 +02:00
Eryk Michalak
968b2ee460 Translated using Weblate (Polish)
Currently translated at 97.9% (286 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-06-04 10:37:37 +00:00
Andrewblasco
890340de94 Translated using Weblate (Spanish)
Currently translated at 99.6% (291 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-04 10:37:37 +00:00
sk
4ca1a7b29e fix index out of bounds exception 2023-06-04 11:45:12 +02:00
sk
5432f2590c fine-tune footer layout 2023-06-04 05:00:48 +02:00
sk
60ccf5cf0a only shift selection box if footer is present 2023-06-04 04:15:15 +02:00
sk
bc717f5b10 tweak footer margins and hitboxes 2023-06-04 04:08:38 +02:00
sk
486eef21dd responsive footer width 2023-06-04 02:16:47 +02:00
sk
44a4d02815 remove redundant suppress annotation 2023-06-04 01:36:38 +02:00
sk
336a8194bd fix settings button binding not reset visibility and events 2023-06-04 01:36:05 +02:00
sk
7859f4cd05 support parsing mailto links
i mean, why not - if github decided every @username@example.social is actually
an email address, might as well support sharing that mailto link to megalodon
2023-06-03 23:39:43 +02:00
sk
37622ba9ce generalize notification handling, open reports in browser 2023-06-03 22:47:20 +02:00
sk
7a6af89375 fix unwanted fab animation when scrolling and switching tab
closes sk22#528
2023-06-03 22:07:58 +02:00
sk
056bfaacfe fix fab being hidden when scrolling to top
closes sk22#528
2023-06-03 21:54:57 +02:00
sk
6684311ec5 fix button state/char counter not updating when empty
closes sk22#537
2023-06-03 21:24:40 +02:00
sk
11943571ad fix thread replies not added to data
closes sk22#543
2023-06-03 21:10:45 +02:00
sk
f696fcd412 simplify ancestry code 2023-06-03 21:03:47 +02:00
sk
2919e109ca remove unused member 2023-06-03 20:40:29 +02:00
sk
995f478708 allow sharing @-handles with megalodon
closes sk22#540
2023-06-03 20:31:00 +02:00
sk
fb8764bcd7 refactor ancestry, fix case regarding reply line
fix case where reply line was removed despite having no direct ancestor
2023-06-02 22:08:03 +02:00
Eugen Rochko
d7f73e02c5 New translations strings.xml (German) 2023-06-02 20:22:32 +02:00
Eugen Rochko
e897b3af57 New translations strings.xml (German) 2023-06-02 19:23:23 +02:00
sk
e04fd8a004 bump version 2023-06-02 19:10:08 +02:00
sk
ada70ae1b5 Merge remote-tracking branch 'upstream/l10n_master' 2023-06-02 19:09:44 +02:00
Espasant3
5fdec0900e Translated using Weblate (Galician)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-06-02 17:09:12 +00:00
sk
56a93288c4 reimplement thread ancestry 2023-06-02 19:05:18 +02:00
sk
02e3421f98 fix null pointer exception 2023-06-02 19:03:29 +02:00
Eugen Rochko
fdbf331432 New translations strings.xml (Bengali) 2023-06-02 18:01:03 +02:00
Eugen Rochko
aed86ac6f0 New translations strings.xml (Bengali) 2023-06-02 16:50:10 +02:00
Eugen Rochko
3a13d4d6c0 New translations strings.xml (Bengali) 2023-06-02 05:45:51 +02:00
Eugen Rochko
f5336564d0 New translations strings.xml (Bengali) 2023-06-02 04:39:21 +02:00
sk
1ce49c68fe bump version 2023-06-02 01:45:55 +02:00
sk
d37e880993 don't close sheet after logging out 2023-06-02 01:45:30 +02:00
sk
6fdb81a01f Merge remote-tracking branch 'upstream/l10n_master' 2023-06-02 01:37:14 +02:00
ihor_ck
f9d6827572 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-01 23:36:24 +00:00
Linerly
10bf72b9ff Translated using Weblate (Indonesian)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-01 23:36:24 +00:00
Choukajohn
800f929a15 Translated using Weblate (French)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-01 23:36:24 +00:00
Eugen Rochko
07ca5a8b77 New translations strings.xml (Bengali) 2023-06-01 19:07:10 +02:00
Eugen Rochko
6926a212f4 New translations strings.xml (Bengali) 2023-06-01 17:46:40 +02:00
Eugen Rochko
936f39161b New translations strings.xml (German) 2023-05-31 20:11:48 +02:00
89 changed files with 1957 additions and 619 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 86
versionName "1.2.3+fork.86"
versionCode 92
versionName "1.2.3+fork.92"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
}
@@ -75,7 +75,7 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.7'
implementation 'me.grishka.appkit:appkit:1.2.8'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -2,14 +2,14 @@ package org.joinmastodon.android.fragments;
import static org.junit.Assert.*;
import android.util.Pair;
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;
import java.util.stream.Collectors;
public class ThreadFragmentTest {
@@ -19,41 +19,64 @@ public class ThreadFragmentTest {
return status;
}
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
return new ThreadFragment.NeighborAncestryInfo(s, d, a);
}
@Test
public void countAncestryLevels() {
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<Pair<String, Integer>> actual =
ThreadFragment.countAncestryLevels("main status", context);
List<Pair<String, Integer>> expected = List.of(
Pair.create("oldest ancestor", -2),
Pair.create("younger ancestor", -1),
Pair.create("main status", 0),
Pair.create("first reply", 1),
Pair.create("reply to first reply", 2),
Pair.create("third level reply", 3),
Pair.create("another reply", 1)
);
assertEquals(
"status ids are in the right order",
expected.stream().map(p -> p.first).collect(Collectors.toList()),
actual.stream().map(p -> p.first).collect(Collectors.toList())
);
assertEquals(
"counted levels match",
expected.stream().map(p -> p.second).collect(Collectors.toList()),
actual.stream().map(p -> p.second).collect(Collectors.toList())
);
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

View File

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

View File

@@ -6,8 +6,10 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import android.widget.Toast;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
@@ -18,6 +20,8 @@ import org.jsoup.internal.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
@@ -29,20 +33,24 @@ public class ExternalShareActivity extends FragmentStackActivity{
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text);
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);
boolean isOpenable = isFediUrl || fediHandle.isPresent();
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){
if (sessions.isEmpty()){
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
finish();
}else if(sessions.size()==1 && !isMastodonURL){
openComposeFragment(sessions.get(0).getID());
}else{
new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> {
if (open) {
UiUtils.lookupURL(this, accountId, text, false, (clazz, args) -> {
} 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;
}
@@ -51,11 +59,25 @@ public class ExternalShareActivity extends FragmentStackActivity{
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);
}
}).show();
});
sheet.show();
} else if (sessions.size() == 1) {
openComposeFragment(sessions.get(0).getID());
}
}
}

View File

@@ -48,6 +48,8 @@ public class GlobalUserPreferences{
public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static boolean allowRemoteLoading;
public static AutoRevealMode autoRevealEqualSpoilers;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
@@ -127,6 +129,8 @@ public class GlobalUserPreferences{
replyVisibility=prefs.getString("replyVisibility", null);
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -176,6 +180,8 @@ public class GlobalUserPreferences{
.putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.apply();
}
@@ -195,4 +201,10 @@ public class GlobalUserPreferences{
LIGHT,
DARK
}
public enum AutoRevealMode {
NEVER,
THREADS,
DISCUSSIONS
}
}

View File

@@ -117,30 +117,18 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
}
private void showFragmentForNotification(Notification notification, String accountID){
Fragment fragment;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("_can_go_back", true);
try{
notification.postprocess();
}catch(ObjectValidationException x){
Log.w("MainActivity", x);
return;
}
if(notification.status!=null){
fragment=new ThreadFragment();
args.putParcelable("status", Parcels.wrap(notification.status));
}else{
fragment=new ProfileFragment();
args.putParcelable("profileAccount", Parcels.wrap(notification.account));
}
fragment.setArguments(args);
showFragment(fragment);
UiUtils.showFragmentForNotification(this, notification, accountID, null);
}
private void showFragmentForExternalShare(Bundle args) {
String clazz = args.getString("fromExternalShare");
Fragment fragment = switch (clazz) {
String className = args.getString("fromExternalShare");
Fragment fragment = switch (className) {
case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment();
default -> null;

View File

@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
@Override
public void onSuccess(Token token){
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<>(){
@Override
public void onSuccess(Account account){

View File

@@ -36,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=3;
private static final int DB_VERSION=4;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
@@ -61,7 +61,7 @@ public class CacheController{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
@@ -112,7 +112,7 @@ public class CacheController{
runOnDbThread((db)->{
if(clear)
db.delete("home_timeline", null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Status s:posts){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
@@ -120,6 +120,7 @@ public class CacheController{
if(s.hasGapAfter)
flags|=POST_FLAG_GAP_AFTER;
values.put("flags", flags);
values.put("time", s.createdAt.getEpochSecond());
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -134,7 +135,7 @@ public class CacheController{
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
@@ -192,7 +193,7 @@ public class CacheController{
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
db.delete(table, null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Notification n:notifications){
if(n.type==null){
continue;
@@ -200,6 +201,7 @@ public class CacheController{
values.put("id", n.id);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
values.put("time", n.createdAt.getEpochSecond());
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -296,21 +298,24 @@ public class CacheController{
CREATE TABLE `home_timeline` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0
`flags` INTEGER NOT NULL DEFAULT 0,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_all` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_mentions` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
createRecentSearchesTable(db);
createPostsNotificationsTable(db);
@@ -318,12 +323,16 @@ public class CacheController{
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
if(oldVersion==1){
if(oldVersion<2){
createRecentSearchesTable(db);
}
if(oldVersion==2){
if(oldVersion<3){
// MEGALODON-SPECIFIC
createPostsNotificationsTable(db);
}
if(oldVersion<4){
addTimeColumns(db);
}
}
private void createRecentSearchesTable(SQLiteDatabase db){
@@ -341,9 +350,21 @@ public class CacheController{
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
}
private void addTimeColumns(SQLiteDatabase db){
db.execSQL("DELETE FROM `home_timeline`");
db.execSQL("DELETE FROM `notifications_all`");
db.execSQL("DELETE FROM `notifications_mentions`");
db.execSQL("DELETE FROM `notifications_posts`");
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_posts` ADD `time` INTEGER NOT NULL DEFAULT 0");
}
}
@FunctionalInterface

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -161,6 +162,11 @@ public class MastodonAPIController{
respObj=gson.fromJson(reader, req.respClass);
}
}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)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
@@ -20,9 +21,11 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
@@ -44,10 +47,11 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
TypeToken<T> respTypeToken;
Call okhttpCall;
Token token;
boolean canceled;
boolean canceled, isRemote;
Map<String, String> headers;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@Nullable Context context;
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
this.path=path;
@@ -101,6 +105,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> execRemote(String domain) {
return execRemote(domain, null);
}
public MastodonAPIRequest<T> execRemote(String domain, @Nullable AccountSession remoteSession) {
this.isRemote = true;
return Optional.ofNullable(remoteSession)
.or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream()
.filter(acc -> acc.domain.equals(domain))
.findAny())
.map(AccountSession::getID)
.map(this::exec)
.orElse(this.execNoAuth(domain));
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null);
}
@@ -164,9 +183,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> setContext(Context context) {
this.context = context;
return this;
}
@Nullable
public Context getContext() {
return context;
}
@CallSuper
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){
((BaseModel) respObj).isRemote = isRemote;
((BaseModel) respObj).postprocess();
}else if(respObj instanceof List){
if(removeUnsupportedItems){
@@ -175,6 +205,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Object item=itr.next();
if(item instanceof BaseModel){
try{
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}catch(ObjectValidationException x){
Log.w(TAG, "Removing invalid object from list", x);
@@ -182,15 +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)){
if(item instanceof BaseModel){
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}
}
}else{
for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel)
if(item instanceof BaseModel) {
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}
}
}
}

View File

@@ -46,7 +46,7 @@ public class StatusInteractionController{
@Override
public void onSuccess(Status result){
runningFavoriteRequests.remove(status.id);
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@@ -80,7 +80,7 @@ public class StatusInteractionController{
public void onSuccess(Status reblog){
Status result = reblog.getContentStatus();
runningReblogRequests.remove(status.id);
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}

View File

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

View File

@@ -160,6 +160,11 @@ public class AccountSessionManager{
return sessions.get(id);
}
@Nullable
public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id);
}
@Nullable
public AccountSession getLastActiveAccount(){
if(sessions.isEmpty() || lastActiveAccountID==null)

View File

@@ -20,6 +20,7 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
@@ -95,10 +96,11 @@ public class AccountTimelineFragment extends StatusListFragment{
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
return;
}else if(filter==GetAccountStatuses.Filter.MEDIA){
if(ev.status.mediaAttachments.isEmpty())
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
return;
}
prependItems(Collections.singletonList(ev.status), true);
if (isOnTop()) scrollToTop();
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){

View File

@@ -26,7 +26,6 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent;
@@ -72,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@@ -83,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
protected boolean currentlyScrolling;
public BaseStatusListFragment(){
super(20);
@@ -96,7 +96,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
UiUtils.loadMaxWidth(getContext());
if(GlobalUserPreferences.disableMarquee){
setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false);
@@ -207,7 +206,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){
if(holder!=null && list!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
int[] pos={0, 0};
@@ -293,6 +292,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
fab.startAnimation(animate);
}
public boolean isScrolling() {
return currentlyScrolling;
}
@Override
public void hideFab() {
View fab = getFab();
@@ -320,7 +323,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer.offsetView(-dx, -dy);
View fab = getFab();
if (fab!=null && GlobalUserPreferences.autoHideFab) {
if (fab!=null && GlobalUserPreferences.autoHideFab && dy != UiUtils.SCROLL_TO_TOP_DELTA) {
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
hideFab();
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
@@ -333,12 +336,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
currentlyScrolling = newState != RecyclerView.SCROLL_STATE_IDLE;
}
});
list.addItemDecoration(new StatusListItemDecoration());
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
@@ -350,23 +361,42 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
if(holder instanceof StatusDisplayItem.Holder<?> h){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(otherID.equals(id)){
if (firstIndex < 0) firstIndex = i;
lastIndex = i;
StatusDisplayItem item = h.getItem();
hasDescendant = item.hasDescendantNeighbor;
// no for direct descendants because main status (right above) is
// being displayed with an extended footer - no connected layout
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left);
outRect.top=Math.min(outRect.top, tmpRect.top);
outRect.right=Math.max(outRect.right, tmpRect.right);
int bottom = tmpRect.bottom;
if (holder instanceof FooterStatusDisplayItem.Holder fh
&& fh.getItem().hasDescendantSibling) {
bottom += V.dp(8);
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
isWarning = true;
}
outRect.bottom=Math.max(outRect.bottom, bottom);
}
}
}
}
// 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());
@@ -650,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
protected int getListWidthForMediaLayout(){
return list.getWidth();
}
@@ -719,6 +754,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
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{
public DisplayItemsAdapter(){
@@ -779,7 +821,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (ih.getItem().descendantLevel != 0 && ih.getItem().hasDescendantSibling) continue;
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}

View File

@@ -149,7 +149,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
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 IMAGE_DESCRIPTION_RESULT=363;
@@ -355,6 +355,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} else {
mediaBtn.setOnClickListener(v -> openFilePicker(false));
}
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -579,8 +580,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void afterTextChanged(Editable s){
if(s.length()==0)
if(s.length()==0){
updateCharCounter();
return;
}
int start=lastChangeStart;
int count=lastChangeCount;
// offset one char back to catch an already typed '@' or '#' or ':'
@@ -845,12 +848,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) {
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
// editing an already published post
draftsBtn.setVisibility(View.GONE);
}
}
@Override
public String getAccountID() {
return accountID;
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -1007,7 +1016,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.state!=AttachmentUploadState.DONE)
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);
}
@@ -1149,7 +1158,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
sendProgress.setVisibility(View.VISIBLE);
sendError.setVisibility(View.GONE);
Callback<Status> resCallback=new Callback<>(){
Callback<Status> resCallback = new Callback<>(){
@Override
public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> {
@@ -1162,7 +1171,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}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()) {
Nav.finish(ComposeFragment.this);
@@ -1897,9 +1916,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
if (isInstancePixelfed()) {
m.findItem(R.id.vis_private).setVisible(false);
}
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.isAkkoma()) {
if (isInstanceAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@@ -17,6 +17,10 @@ public interface HasAccountID {
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default boolean isInstancePixelfed() {
return getInstance().map(Instance::isPixelfed).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}

View File

@@ -6,4 +6,5 @@ public interface HasFab {
View getFab();
void showFab();
void hideFab();
boolean isScrolling();
}

View File

@@ -244,7 +244,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment);
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();

View File

@@ -460,6 +460,12 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
}
@Override
public boolean isScrolling() {
return (fragments[pager.getCurrentItem()] instanceof HasFab fabulous)
&& fabulous.isScrolling();
}
private void updateSwitcherIcon(int i) {
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext()));

View File

@@ -30,4 +30,9 @@ public abstract class MastodonToolbarFragment extends ToolbarFragment{
toolbar.setNavigationContentDescription(R.string.back);
}
}
@Override
protected boolean wantsToolbarMenuIconsTinted() {
return false; // else, badged icons don't work :(
}
}

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
@@ -25,6 +26,8 @@ import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
@@ -192,23 +195,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else if(n.report != null){
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
Bundle args = new Bundle();
if(n.status != null && n.status.inReplyToAccountId != null && knownAccounts.containsKey(n.status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(n.status.inReplyToAccountId)));
UiUtils.showFragmentForNotification(getContext(), n, accountID, args);
}
@Override
@@ -240,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
// copied from StatusListFragment.EventListener (just like the method above)
// (which assumes this.data to be a list of statuses...)
@Subscribe
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
for(Notification n:data){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}
}
}
}
for(Notification n:preloadedData){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
}
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow)

View File

@@ -45,6 +45,7 @@ import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -58,7 +59,6 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
@@ -138,7 +138,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private TextView followsYouView;
private ViewGroup rolesView;
private Account account;
private Account account, remoteAccount;
private String accountID;
private String domain;
private Relationship relationship;
@@ -177,7 +177,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(getArguments().containsKey("profileAccount")){
if (getArguments().containsKey("remoteAccount")) {
remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
if(!getArguments().getBoolean("noAutoLoad", false))
loadData();
} else if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
@@ -348,36 +352,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return sizeWrapper;
}
private void onAccountLoaded(Account result) {
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
@Override
protected void doLoadData(){
if (remoteAccount != null) {
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
if (getContext() == null) return;
if (args == null || !args.containsKey("profileAccount")) {
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)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(Account result){
if (getActivity() == null) return;
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
onAccountLoaded(result);
}
})
.exec(accountID);
@@ -491,9 +514,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
if (account.roles != null && !account.roles.isEmpty()) {
rolesView.setVisibility(View.VISIBLE);
@@ -512,13 +535,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
String acct = ((isSelf || account.isRemote)
? account.getFullyQualifiedName()
: account.acct);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(domain);
}
ssb.append(acct);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
@@ -527,7 +549,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
username.setText('@'+acct);
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
@@ -597,6 +619,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return false;
}
@Override
protected boolean wantsToolbarMenuIconsTinted() {
return false;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){
@@ -627,8 +654,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
));
}
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;
}
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
@@ -775,6 +804,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
}
@Override
public boolean isScrolling() {
return getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous
&& fabulous.isScrolling();
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatarBorder.getTop()-topBarsH){

View File

@@ -3,7 +3,8 @@ package org.joinmastodon.android.fragments;
import android.view.ViewTreeObserver;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.V;
import org.joinmastodon.android.ui.utils.UiUtils;
public interface ScrollableToTop{
void scrollToTop();
@@ -21,7 +22,7 @@ public interface ScrollableToTop{
@Override
public boolean onPreDraw(){
list.getViewTreeObserver().removeOnPreDrawListener(this);
list.scrollBy(0, V.dp(300));
list.scrollBy(0, UiUtils.SCROLL_TO_TOP_DELTA);
list.smoothScrollToPosition(0);
return true;
}

View File

@@ -37,6 +37,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
@@ -85,8 +86,8 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
private ButtonItem defaultContentTypeButtonItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
@@ -189,9 +190,18 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.showInteractionCounts=i.checked;
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.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->{
GlobalUserPreferences.disableSwipe=i.checked;
@@ -219,6 +229,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.confirmBeforeReblog=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
GlobalUserPreferences.allowRemoteLoading=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
items.add(new HeaderItem(R.string.sk_timelines));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
@@ -271,7 +286,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.collapseLongPosts=i.checked;
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.save();
needAppRestart=true;
@@ -526,6 +541,36 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
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){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
@@ -555,14 +600,14 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private boolean onContentTypeChanged(MenuItem item, Button btn){
int id = item.getItemId();
ContentType contentType = switch (id) {
case R.id.content_type_plain -> ContentType.PLAIN;
case R.id.content_type_html -> ContentType.HTML;
case R.id.content_type_markdown -> ContentType.MARKDOWN;
case R.id.content_type_bbcode -> ContentType.BBCODE;
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
default -> null;
};
ContentType contentType = null;
if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
GlobalUserPreferences.save();
btn.setText(getContentTypeString(contentType));
@@ -835,7 +880,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
}
private class SmallTextItem extends Item {
private String text;
private final String text;
public SmallTextItem(@StringRes int text) {
this.text = getString(text);
}
public SmallTextItem(String text) {
this.text = text;
@@ -1076,7 +1125,6 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private final ImageView icon;
private final TextView text;
@SuppressLint("ClickableViewAccessibility")
public ButtonViewHolder(){
super(getActivity(), R.layout.item_settings_button, list);
text=findViewById(R.id.text);
@@ -1084,14 +1132,17 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
button=findViewById(R.id.button);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBind(ButtonItem item){
text.setText(item.text);
if (item.icon == 0) {
icon.setVisibility(View.GONE);
} else {
icon.setImageResource(item.icon);
}
icon.setVisibility(item.icon == 0 ? View.GONE : View.VISIBLE);
icon.setImageResource(item.icon == 0 ? 0 : item.icon);
// reset listeners before letting the button consumer consume the button
// (and potentially set some listeners, but not others)
button.setOnTouchListener(null);
button.setOnClickListener(null);
button.setOnLongClickListener(null);
item.buttonConsumer.accept(button);
}
}

View File

@@ -68,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
status.filterRevealed = true;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
@@ -164,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
protected void removeStatus(Status status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
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;
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)
return;
int lastIndex;

View File

@@ -2,21 +2,31 @@ package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
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.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
@@ -30,31 +40,19 @@ import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus;
/**
* lists the hierarchy of ancestors and descendants in a thread. level 0 = the main status.
* e.g.
* <pre>
* [0] ancestor: -2 ↰
* [1] ancestor: -1 ↰
* [2] main status: 0 ↰
* [3] descendant: 1 ↰
* [4] descendant: 2 ↰
* [5] descendant: 3
* [6] descendant: 1
* [7] descendant: 1 ↰
* [8] descendant: 2
* </pre>
* confused? good. /j
*/
private final List<Pair<String, Integer>> levels = new ArrayList<>();
protected Status mainStatus, updatedStatus;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
protected boolean contextInitiallyRendered;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -74,23 +72,29 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
for(int i = 0; i < items.size(); i++){
StatusDisplayItem item = items.get(i);
Optional<Pair<String, Integer>> levelForStatus =
levels.stream().filter(p -> p.first.equals(s.id)).findAny();
item.descendantLevel = levelForStatus.map(p -> p.second).orElse(0);
if (levelForStatus.isPresent()) {
int idx = levels.indexOf(levelForStatus.get());
item.hasDescendantSibling = (levels.size() > idx + 1)
&& levels.get(idx + 1).second > levelForStatus.get().second;
item.isDescendantSibling = (idx - 1 >= 0)
&& levels.get(idx - 1).second < levelForStatus.get().second;
// 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.isDescendantSibling
&& item.descendantLevel != 1) {
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
deleteTheseItems.add(i);
}
@@ -101,23 +105,28 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
footer.hideCounts=true;
}
}
for (int deleteThisItem : deleteTheseItems) items.remove(deleteThisItem);
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
}
return items;
}
@Override
protected void doLoadData(int offset, int count){
if (refreshing) loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if (getActivity() == null) return;
if (getContext() == null) return;
Map<String, Status> oldData = null;
if(refreshing){
oldData = new HashMap<>(data.size());
for (Status s : data) oldData.put(s.id, s);
data.clear();
levels.clear();
ancestryMap.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
@@ -129,7 +138,9 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
levels.addAll(countAncestryLevels(mainStatus.id, result));
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
}
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
@@ -145,37 +156,109 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
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();
if(refreshing){
refreshDone();
adapter.notifyDataSetChanged();
}
list.scrollToPosition(displayItems.size()-count);
// no animation is going to happen, so proceeding to apply right now
if (data.size() == 1) {
contextInitiallyRendered = true;
// for the case that the main status has already finished loading
maybeApplyMainStatus();
}
}
})
.exec(accountID);
}
public static List<Pair<String, Integer>> countAncestryLevels(String mainStatusID, StatusContext context) {
List<Pair<String, Integer>> levels = new ArrayList<>();
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();
}
for (int i = 0; i < context.ancestors.size(); i++) {
levels.add(Pair.create(
context.ancestors.get(i).id,
-context.ancestors.size() + i // -3, -2, -1
@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)
));
}
levels.add(Pair.create(mainStatusID, 0));
Map<String, Integer> levelPerStatus = new HashMap<>();
// sum up the amounts of descendants per status
context.descendants.forEach(s -> levelPerStatus.put(s.id,
levelPerStatus.getOrDefault(s.inReplyToId, 0) + 1));
context.descendants.forEach(s ->
levels.add(Pair.create(s.id, levelPerStatus.get(s.id))));
return levels;
return ancestry;
}
public static void sortStatusContext(Status mainStatus, StatusContext context) {
@@ -242,17 +325,78 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
showContent();
if(!loaded)
footerProgress.setVisibility(View.VISIBLE);
list.setItemAnimator(new BetterItemAnimator() {
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder);
contextInitiallyRendered = true;
// for the case that both requests are already done (and thus won't apply it)
maybeApplyMainStatus();
}
});
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
onAppendItems(Collections.singletonList(ev.status));
if (ev.status.inReplyToId == null) return;
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
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id);
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
}
@Override
@@ -275,4 +419,41 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
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();
}
}

View File

@@ -3,16 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
import java.util.Optional;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
protected Account account;
protected String initialSubtitle = "";
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
if (getArguments().containsKey("remoteAccount")) {
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
}
setTitle("@"+account.acct);
}
@@ -22,4 +33,36 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
? "/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);
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent;
@@ -47,6 +48,7 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
@@ -243,16 +245,19 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship();
}
public void bindRelationship(){
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);
}else{
button.setVisibility(View.VISIBLE);
@@ -282,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@@ -423,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
}
}

View File

@@ -13,12 +13,12 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowers(account.id, maxID, count);
return new GetAccountFollowers(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -13,12 +13,12 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowing(account.id, maxID, count);
return new GetAccountFollowing(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -1,33 +1,173 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
private String nextMaxID;
private MastodonAPIRequest<T> remoteInfoRequest;
protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled;
protected int localOffset;
protected T remoteInfo;
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
protected abstract MastodonAPIRequest<T> loadRemoteInfo();
public abstract T getCurrentInfo();
public abstract String getRemoteDomain();
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// already have remote info (e.g. from arguments), so no need to fetch it again
if (remoteInfo != null) {
onRemoteInfoLoaded(remoteInfo);
return;
}
remoteDisabled = !GlobalUserPreferences.allowRemoteLoading
|| getSession().domain.equals(getRemoteDomain());
if (!remoteDisabled) {
remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() {
@Override
public void onSuccess(T result) {
if (getContext() == null) return;
onRemoteInfoLoaded(result);
}
@Override
public void onError(ErrorResponse error) {
if (getContext() == null) return;
onRemoteLoadingFailed();
}
});
remoteInfoRequest.execRemote(getRemoteDomain(), getRemoteSession());
}
}
/**
* override to provide an ideal account session (e.g. if you're logged into the author's remote
* account) to make the remote request from. if null is provided, will try to get any session
* on the remote domain, or tries the request without authentication.
*/
protected AccountSession getRemoteSession() {
return null;
}
protected void onRemoteInfoLoaded(T info) {
this.remoteInfo = info;
this.remoteInfoRequest = null;
maybeStartLoadingRemote();
}
protected void onRemoteLoadingFailed() {
this.remoteRequestFailed = true;
this.remoteInfo = null;
this.remoteInfoRequest = null;
if (doneWithHomeInstance) dataLoaded();
}
@Override
public void dataLoaded() {
super.dataLoaded();
footerProgress.setVisibility(View.GONE);
}
private void maybeStartLoadingRemote() {
if (startedRemoteLoading || remoteDisabled) return;
if (!remoteRequestFailed) {
if (data.size() == 0) showProgress();
else footerProgress.setVisibility(View.VISIBLE);
}
if (doneWithHomeInstance && remoteInfo != null) {
startedRemoteLoading = true;
loadData(localOffset, itemsPerPage * 2);
}
}
@Override
public void onRefresh() {
localOffset = 0;
doneWithHomeInstance = false;
startedRemoteLoading = false;
super.onRefresh();
}
@Override
public void loadData(int offset, int count) {
// always subtract the amount loaded through the home instance once loading from remote
// since loadData gets called with data.size() (data includes both local and remote)
if (doneWithHomeInstance) offset -= localOffset;
super.loadData(offset, count);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count)
MastodonAPIRequest<?> request = onCreateRequest(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
Collection<AccountItem> d = justRefreshed ? List.of() : data;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
if (getActivity() == null) return;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
List<AccountItem> items = result.stream()
.filter(a -> d.size() > 1000 || d.stream()
.noneMatch(i -> i.account.url.equals(a.url)))
.map(AccountItem::new)
.collect(Collectors.toList());
boolean hasMore = nextMaxID != null;
if (!hasMore && !doneWithHomeInstance) {
// only runs last time data was fetched from the home instance
localOffset = d.size() + items.size();
doneWithHomeInstance = true;
}
onDataLoaded(items, hasMore);
if (doneWithHomeInstance) maybeStartLoadingRemote();
}
})
.exec(accountID);
@Override
public void onError(ErrorResponse error) {
if (doneWithHomeInstance) {
onRemoteLoadingFailed();
onDataLoaded(Collections.emptyList(), false);
return;
}
super.onError(error);
}
});
if (doneWithHomeInstance && remoteInfo == null) return; // we are waiting
if (doneWithHomeInstance && remoteInfo != null) {
request.execRemote(getRemoteDomain(), getRemoteSession());
} else {
request.exec(accountID);
}
currentRequest = request;
}
@Override

View File

@@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
updateTitle(status);
}
@Override
protected void updateTitle(Status status) {
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusFavorites(status.id, maxID, count);
return new GetStatusFavorites(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
updateTitle(status);
}
@Override
protected void updateTitle(Status status) {
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusReblogs(status.id, maxID, count);
return new GetStatusReblogs(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -3,12 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{
import java.util.Optional;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
protected Status status;
protected abstract void updateTitle(Status status);
protected MastodonAPIRequest<Status> loadRemoteInfo() {
String[] parts = status.url.split("/");
if (parts.length == 0) return null;
return new GetStatusByID(parts[parts.length - 1]);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -17,7 +32,7 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
@Override
protected boolean hasSubtitle(){
return false;
return remoteRequestFailed;
}
@Override
@@ -28,4 +43,35 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
: '@' + 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();
}
}

View File

@@ -5,7 +5,6 @@ import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -17,7 +16,7 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop {
public class DiscoverPostsFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
@Override
@@ -39,11 +38,6 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;

View File

@@ -12,7 +12,6 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
@@ -43,7 +42,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
@@ -312,11 +311,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
}
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri.Builder searchUri = base.path("/search");

View File

@@ -1,7 +1,10 @@
package org.joinmastodon.android.model;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@@ -62,7 +65,6 @@ public class Account extends BaseModel implements Searchable{
/**
* An image banner that is shown above the profile and in profile cards.
*/
@RequiredField
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.
@@ -135,6 +137,8 @@ public class Account extends BaseModel implements Searchable{
public List<Role> roles;
public @Nullable String fqn; // akkoma has this, mastodon't
@Override
public String getQuery() {
return url;
@@ -162,6 +166,7 @@ public class Account extends BaseModel implements Searchable{
moved.postprocess();
if(TextUtils.isEmpty(displayName))
displayName=username;
if(fqn == null) fqn = getFullyQualifiedName();
}
public boolean isLocal(){
@@ -173,6 +178,10 @@ public class Account extends BaseModel implements Searchable{
return parts.length==1 ? null : parts[1];
}
public String getDomainFromURL() {
return Uri.parse(url).getHost();
}
public String getDisplayUsername(){
return '@'+acct;
}
@@ -181,6 +190,10 @@ public class Account extends BaseModel implements Searchable{
return '@'+acct.split("@")[0];
}
public String getFullyQualifiedName() {
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
}
@Override
public String toString(){
return "Account{"+

View File

@@ -8,8 +8,17 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
public abstract class BaseModel implements Cloneable{
/**
* indicates the profile has been fetched from a foreign instance.
*
* @see MastodonAPIRequest#execRemote
*/
public transient boolean isRemote;
public abstract class BaseModel{
@CallSuper
public void postprocess() throws ObjectValidationException{
try{
@@ -23,4 +32,14 @@ public abstract class BaseModel{
}
}catch(IllegalAccessException ignore){}
}
@NonNull
@Override
public Object clone(){
try{
return super.clone();
}catch(CloneNotSupportedException x){
throw new RuntimeException(x);
}
}
}

View File

@@ -148,6 +148,10 @@ public class Instance extends BaseModel{
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)

View File

@@ -30,10 +30,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
@NonNull
@Override
public PushSubscription clone(){
PushSubscription copy=null;
try{
copy=(PushSubscription) super.clone();
}catch(CloneNotSupportedException ignore){}
PushSubscription copy=(PushSubscription) super.clone();
copy.alerts=alerts.clone();
return copy;
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
@@ -16,7 +17,6 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public Instant scheduledAt;
@RequiredField
public Params params;
@RequiredField
public List<Attachment> mediaAttachments;
@Override
@@ -24,8 +24,17 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
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
public static class Params {
public static class Params extends BaseModel {
@RequiredField
public String text;
public String spoilerText;
@@ -40,10 +49,16 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public String applicationId;
public List<String> mediaIds;
public ContentType contentType;
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if (poll != null) poll.postprocess();
}
}
@Parcel
public static class ScheduledPoll {
public static class ScheduledPoll extends BaseModel {
@RequiredField
public String expiresIn;
@RequiredField

View File

@@ -12,6 +12,7 @@ import com.google.gson.JsonParseException;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcel;
@@ -37,7 +38,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public boolean sensitive;
@RequiredField
public String spoilerText;
@RequiredField
public List<Attachment> mediaAttachments;
public Application application;
@RequiredField
@@ -94,6 +94,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
t.postprocess();
for(Emoji e:emojis)
e.postprocess();
if (mediaAttachments == null) mediaAttachments = List.of();
for(Attachment a:mediaAttachments)
a.postprocess();
account.postprocess();
@@ -171,6 +172,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
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) {
Status s = new Status();
s.id = id;
@@ -191,7 +198,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
}
public static class StatusDeserializer implements JsonDeserializer<Status> {
@Override
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();

View File

@@ -14,7 +14,7 @@ public enum StatusPrivacy{
@SerializedName("local")
LOCAL(4); // akkoma
private int privacy;
private final int privacy;
StatusPrivacy(int privacy) {
this.privacy = privacy;
@@ -24,6 +24,13 @@ public enum StatusPrivacy{
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() {
return privacy;
}

View File

@@ -58,24 +58,24 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountSwitcherSheet extends BottomSheet{
private final Activity activity;
private final HomeFragment fragment;
private final BiConsumer<String, Boolean> onClick;
private final boolean externalShare, openInApp;
private BiConsumer<String, Boolean> onClick;
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader;
private AccountsAdapter accountsAdapter;
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
this(activity, fragment, false, false, null);
this(activity, fragment, false, false);
}
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp, BiConsumer<String, Boolean> onClick){
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp){
super(activity);
this.activity=activity;
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);
@@ -101,7 +101,7 @@ public class AccountSwitcherSheet extends BottomSheet{
setOnDismissListener((d) -> activity.finish());
}
adapter.addAdapter(new AccountsAdapter());
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
if (!externalShare) {
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
@@ -122,6 +122,10 @@ public class AccountSwitcherSheet extends BottomSheet{
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
}
public void setOnClick(BiConsumer<String, Boolean> onClick) {
this.onClick = onClick;
}
private void confirmLogOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(activity)
@@ -201,7 +205,10 @@ public class AccountSwitcherSheet extends BottomSheet{
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
} else {
dismiss();
accounts.stream().filter(w -> accountID.equals(w.session.getID())).findAny().ifPresent(w -> {
accountsAdapter.notifyItemRemoved(accounts.indexOf(w));
accounts.remove(w);
});
}
}

View File

@@ -33,12 +33,14 @@ import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final String accountID;
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);
this.status=status;
this.accountID=accountID;
}
@Override
@@ -72,7 +74,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status;
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){
editHistory.setVisibility(View.VISIBLE);
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));

View File

@@ -5,7 +5,6 @@ import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -17,7 +16,6 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -56,8 +54,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView reply, boost, favorite, bookmark;
private final ImageView share;
private final TextView replies, boosts, favorites;
private final View reply, boost, favorite, share, bookmark;
private static final Animation opacityOut, opacityIn;
private View touchingView = null;
@@ -91,22 +89,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_footer, parent);
reply=findViewById(R.id.reply);
boost=findViewById(R.id.boost);
favorite=findViewById(R.id.favorite);
bookmark=findViewById(R.id.bookmark);
share=findViewById(R.id.share);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
}
View reply=findViewById(R.id.reply_btn);
View boost=findViewById(R.id.boost_btn);
View favorite=findViewById(R.id.favorite_btn);
View share=findViewById(R.id.share_btn);
View bookmark=findViewById(R.id.bookmark_btn);
replies=findViewById(R.id.reply);
boosts=findViewById(R.id.boost);
favorites=findViewById(R.id.favorite);
reply=findViewById(R.id.reply_btn);
boost=findViewById(R.id.boost_btn);
favorite=findViewById(R.id.favorite_btn);
share=findViewById(R.id.share_btn);
bookmark=findViewById(R.id.bookmark_btn);
reply.setOnTouchListener(this::onButtonTouch);
reply.setOnClickListener(this::onReplyClick);
reply.setOnLongClickListener(this::onReplyLongClick);
@@ -131,26 +123,32 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(FooterStatusDisplayItem item){
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount);
bindText(replies, item.status.repliesCount);
bindText(boosts, item.status.reblogsCount);
bindText(favorites, item.status.favouritesCount);
// in thread view, direct descendant posts display one direct reply to themselves,
// hence in that case displaying whether there is another reply
reply.setSelected(item.status.repliesCount > (item.descendantLevel > 0 ? 1 : 0));
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
reply.setSelected(item.status.repliesCount > compareTo);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
boost.setEnabled(item.status.isReblogPermitted(item.accountID));
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,
item.descendantLevel != 0 && item.hasDescendantSibling ? V.dp(-6) : 0);
condenseBottom ? V.dp(-5) : 0);
itemView.requestLayout();
}
private void bindButton(TextView btn, long count){
private void bindText(TextView btn, long count){
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
btn.setText(UiUtils.abbreviateNumber(count));
btn.setCompoundDrawablePadding(V.dp(8));
@@ -174,8 +172,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
v.setPivotX(V.dp(20));
// 28dp to center in middle of icon, because:
// (icon width = 24dp) / 2 + (paddingStart = 8dp) + (paddingHorizontal = 8dp)
v.setPivotX(UiUtils.sp(v.getContext(), 28));
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
@@ -219,7 +218,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
bindText(boosts, r.reblogsCount);
}
private boolean onBoostLongClick(View v){
@@ -308,7 +307,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn);
bindButton(favorite, r.favouritesCount);
bindText(favorites, r.favouritesCount);
});
}

View File

@@ -196,7 +196,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
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);
}else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text);

View File

@@ -73,9 +73,9 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public void updateVisibility(StatusPrivacy visibility) {
this.visibility = visibility;
this.iconEnd = visibility != null ? switch (visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
case PUBLIC -> R.drawable.ic_fluent_earth_20sp_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20sp_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20sp_filled;
default -> 0;
} : 0;
}

View File

@@ -10,7 +10,6 @@ import android.view.ViewGroup;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
@@ -22,7 +21,6 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
@@ -49,8 +47,23 @@ public abstract class StatusDisplayItem{
public final BaseStatusListFragment parentFragment;
public boolean inset;
public int index;
public int descendantLevel;
public boolean hasDescendantSibling, isDescendantSibling;
public boolean
hasDescendantNeighbor = false,
hasAncestoringNeighbor = false,
isMainStatus = true,
isDirectDescendant = false;
public void setAncestryInfo(
boolean hasDescendantNeighbor,
boolean hasAncestoringNeighbor,
boolean isMainStatus,
boolean isDirectDescendant
) {
this.hasDescendantNeighbor = hasDescendantNeighbor;
this.hasAncestoringNeighbor = hasAncestoringNeighbor;
this.isMainStatus = isMainStatus;
this.isDirectDescendant = isDirectDescendant;
}
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID;
@@ -92,6 +105,21 @@ public abstract class StatusDisplayItem{
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){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
@@ -107,24 +135,14 @@ public abstract class StatusDisplayItem{
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
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);
replyLine = new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20_filled, null, null, fullText
);
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
}
if(status.reblog!=null){
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, fullText));
@@ -139,7 +157,7 @@ public abstract class StatusDisplayItem{
// post contains a hashtag the user is following
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, hashtag.name, List.of(),
R.drawable.ic_fluent_number_symbol_20_filled, null,
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
i -> {
args.putString("hashtag", hashtag.name);
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);

View File

@@ -65,7 +65,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
UiUtils.loadMaxWidth(parentFragment.getContext());
}
public void setTranslationShown(boolean translationShown) {
@@ -238,13 +237,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
// remove additional padding when (transparently padded) translate button is visible
int pos = getAbsoluteAdapterPosition();
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(),
(translateVisible &&
item.parentFragment.getDisplayItems().size() >= pos + 1 &&
item.parentFragment.getDisplayItems().get(pos + 1) instanceof FooterStatusDisplayItem)
? 0 : V.dp(12)
);
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
int bottomPadding = (translateVisible && nextIsFooter) ? 0
: nextIsFooter ? V.dp(6)
: V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
if (!GlobalUserPreferences.collapseLongPosts) {
textScrollView.setLayoutParams(wrapParams);

View File

@@ -4,8 +4,7 @@ import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.view.GestureDetector;
@@ -33,7 +32,8 @@ public class ClickableLinksDelegate {
hlPaint=new Paint();
hlPaint.setAntiAlias(true);
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());
}
@@ -58,7 +58,7 @@ public class ClickableLinksDelegate {
public void onDraw(Canvas canvas){
if(hlPath!=null){
canvas.save();
canvas.translate(0, view.getPaddingTop());
canvas.translate(view.getTotalPaddingLeft(), view.getTotalPaddingTop());
canvas.drawPath(hlPath, hlPaint);
canvas.restore();
}
@@ -73,59 +73,29 @@ public class ClickableLinksDelegate {
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent event) {
int line=-1;
Rect rect=new Rect();
Layout l=view.getLayout();
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){
int padLeft=view.getTotalPaddingLeft(), padRight=view.getTotalPaddingRight(), padTop=view.getTotalPaddingTop(), padBottom=view.getTotalPaddingBottom();
float x=event.getX(), y=event.getY();
if(x<padLeft || y<padTop || x>view.getWidth()-padRight || y>view.getHeight()-padBottom)
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();
if(text instanceof Spanned s){
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
if(spans.length>0){
for(LinkSpan span:spans){
int start=s.getSpanStart(span);
int end=s.getSpanEnd(span);
int lstart=l.getLineForOffset(start);
int lend=l.getLineForOffset(end);
if(line>=lstart && line<=lend){
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){
continue;
}
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;
}
for(LinkSpan span:spans){
int start=s.getSpanStart(span);
int end=s.getSpanEnd(span);
if(start<=position && end>position){
selectedSpan=span;
hlPath=new Path();
l.getSelectionPath(start, end, hlPath);
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
view.invalidate();
return true;
}
}
}

View File

@@ -1,14 +1,10 @@
package org.joinmastodon.android.ui.text;
import android.graphics.Typeface;
import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontStyle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
@@ -17,13 +13,10 @@ import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import android.util.TypedValue;
import android.widget.TextView;
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.Hashtag;
import org.joinmastodon.android.model.Mention;
@@ -251,6 +244,10 @@ public class HtmlParser{
return Jsoup.clean(html, Safelist.none());
}
public static String text(String html) {
return Jsoup.parse(html).body().wholeText();
}
public static CharSequence parseLinks(String text){
Matcher matcher=URL_PATTERN.matcher(text);
if(!matcher.find()) // Return the original string if there are no URLs

View File

@@ -17,6 +17,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -37,6 +38,7 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuItem;
@@ -54,6 +56,8 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
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.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
@@ -98,9 +102,9 @@ import org.parceler.Parcels;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -140,15 +144,11 @@ public class UiUtils {
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");
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() {
}
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) {
try {
if (GlobalUserPreferences.useCustomTabs) {
@@ -897,6 +897,10 @@ public class UiUtils {
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
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() {
@@ -905,6 +909,32 @@ public class UiUtils {
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/43456787654678
// https://pleroma.foo.bar/users/User
@@ -921,7 +951,7 @@ public class UiUtils {
// 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
public static boolean looksLikeMastodonUrl(String urlString) {
public static boolean looksLikeFediverseUrl(String urlString) {
URI uri;
try {
uri = new URI(urlString);
@@ -1023,50 +1053,46 @@ public class UiUtils {
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
!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) {
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
!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('_')))) {
resultConsumer.accept(query);
return;
return Optional.empty();
}
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
.exec(targetAccountID));
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
@@ -1081,19 +1107,97 @@ public class UiUtils {
}
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, launchBrowser, (clazz, args) -> {
if (clazz == null) return;
Nav.go((Activity) context, clazz, args);
});
private static Bundle bundleError(String error) {
Bundle args = new Bundle();
args.putString("error", error);
return args;
}
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) {
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) {
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);
List<String> path = uri.getPathSegments();
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())) {
new GetStatusByID(path.get(1))
return Optional.of(new GetStatusByID(path.get(1))
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
@@ -1105,17 +1209,12 @@ public class UiUtils {
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} else if (looksLikeMastodonUrl(url)) {
new GetSearchResults(url, null, true)
.exec(accountID));
} else if (looksLikeFediverseUrl(url)) {
return Optional.of(new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1133,26 +1232,19 @@ public class UiUtils {
go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) launchWebBrowser(context, url);
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
.exec(accountID));
}
}
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
return Optional.empty();
}
public static void copyText(View v, String text) {
@@ -1315,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.
* Useful for buttons that perform some action that changes their state asynchronously.

View File

@@ -23,7 +23,6 @@ public class ComposeMediaLayout extends ViewGroup{
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
}
@Override

View File

@@ -3,12 +3,13 @@ package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.joinmastodon.android.R;
public class MaxWidthFrameLayout extends FrameLayout{
private int maxWidth;
private int maxWidth, defaultWidth;
public MaxWidthFrameLayout(Context context){
this(context, null);
@@ -22,6 +23,7 @@ public class MaxWidthFrameLayout extends FrameLayout{
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
defaultWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_defaultWidth, -1);
ta.recycle();
}
@@ -33,10 +35,19 @@ public class MaxWidthFrameLayout extends FrameLayout{
this.maxWidth=maxWidth;
}
public int getDefaultWidth() {
return defaultWidth;
}
public void setDefaultWidth(int defaultWidth) {
this.defaultWidth = defaultWidth;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
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);
}

View File

@@ -27,7 +27,6 @@ public class MediaGridLayout extends ViewGroup{
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
}
@Override

File diff suppressed because one or more lines are too long

View File

@@ -2,5 +2,5 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?android:colorAccent" android:state_selected="true"/>
<item android:color="?android:textColorSecondary" android:state_enabled="true"/>
<item android:color="?android:textColorSecondary" android:alpha="0.3"/>
<item android:color="?colorIconDisabled" />
</selector>

View File

@@ -3,7 +3,7 @@
<item android:drawable="@drawable/ic_fluent_megaphone_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20sp"
android:height="20sp"
android:drawable="@drawable/ic_fluent_arrow_repeat_all_20_filled" />
</layer-list>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20sp"
android:height="20sp"
android:drawable="@drawable/ic_fluent_arrow_reply_20_filled" />
</layer-list>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 4.5c-4.694 0-8.5 3.806-8.5 8.5 0 2.345 0.948 4.466 2.484 6.005 0.293 0.293 0.293 0.768 0 1.06-0.294 0.293-0.769 0.293-1.061 0C3.118 18.256 2 15.758 2 13 2 7.477 6.477 3 12 3s10 4.477 10 10c0 2.758-1.118 5.256-2.923 7.065-0.292 0.293-0.767 0.293-1.06 0-0.293-0.292-0.294-0.767-0.001-1.06C19.552 17.467 20.5 15.345 20.5 13c0-4.694-3.806-8.5-8.5-8.5zM12 8c-2.761 0-5 2.239-5 5 0 1.382 0.56 2.632 1.466 3.537 0.293 0.293 0.293 0.768 0 1.06-0.292 0.294-0.767 0.294-1.06 0.001C6.229 16.423 5.5 14.796 5.5 13c0-3.59 2.91-6.5 6.5-6.5s6.5 2.91 6.5 6.5c0 1.796-0.73 3.423-1.906 4.598-0.293 0.293-0.768 0.293-1.06 0-0.293-0.293-0.293-0.768 0-1.06C16.44 15.631 17 14.381 17 13c0-2.761-2.239-5-5-5zm0 2.5c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5 2.5-1.12 2.5-2.5-1.12-2.5-2.5-2.5zM11 13c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20sp"
android:height="20sp"
android:drawable="@drawable/ic_fluent_earth_20_regular" />
</layer-list>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20sp"
android:height="20sp"
android:drawable="@drawable/ic_fluent_lock_closed_20_filled" />
</layer-list>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20sp"
android:height="20sp"
android:drawable="@drawable/ic_fluent_lock_open_20_regular" />
</layer-list>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20sp"
android:height="20sp"
android:drawable="@drawable/ic_fluent_number_symbol_20_filled" />
</layer-list>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M3.28 2.22c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.767 0 1.06l4.804 4.805-3.867 0.561C2.05 8.807 1.607 10.168 2.41 10.95l3.815 3.719-0.9 5.251c-0.19 1.103 0.968 1.944 1.958 1.423l4.716-2.479 4.716 2.48c0.99 0.52 2.148-0.32 1.96-1.424l-0.04-0.223 2.085 2.084c0.293 0.293 0.768 0.293 1.061 0 0.293-0.292 0.293-0.767 0-1.06L3.28 2.22zm13.518 15.639l0.345 2.014-4.516-2.374c-0.394-0.207-0.864-0.207-1.257 0l-4.516 2.374 0.862-5.03c0.075-0.437-0.07-0.884-0.388-1.194l-3.654-3.562 4.673-0.679 8.45 8.45zm3.525-7.772l-3.572 3.482 1.06 1.06 3.777-3.68c0.8-0.781 0.359-2.142-0.748-2.303L15.567 7.88l-2.358-4.777c-0.495-1.004-1.926-1.004-2.421 0L9.3 6.117l1.12 1.12 1.578-3.2 2.259 4.577c0.196 0.398 0.577 0.674 1.016 0.738l5.05 0.734z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -3,7 +3,7 @@
<item android:drawable="@drawable/ic_fluent_person_add_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>

View File

@@ -3,7 +3,7 @@
<item android:drawable="@drawable/ic_fluent_settings_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>

View File

@@ -1,120 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:maxWidth="600sp"
app:defaultWidth="450sp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingHorizontal="16dp">
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/reply_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/reply"
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="11sp">
<!-- avatar width (46sp) / 2 - button width (24sp) / 2 -->
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/reply_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_fluent_chat_multiple_24_selector_text"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical" />
<TextView
android:id="@+id/reply"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/boost_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:foregroundTint="@color/boost_icon"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_boost"
android:tint="@color/boost_icon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/boost"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:textColor="@color/boost_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/favorite_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_fluent_star_24_selector"
android:tint="@color/favorite_icon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/favorite"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:textColor="@color/favorite_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:id="@+id/bookmark"
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="16dp"
android:src="@drawable/ic_fluent_bookmark_24_selector"
android:tint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
</FrameLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/share_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:drawableStart="@drawable/ic_fluent_chat_multiple_24_selector_text"
android:drawablePadding="8dp"
android:paddingHorizontal="8dp"
android:drawableTint="?android:textColorSecondary"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
tools:text="123"/>
</FrameLayout>
android:paddingVertical="12dp">
<ImageView
android:id="@+id/share"
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="16dp"
android:src="@drawable/ic_fluent_share_24_regular"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical"/>
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/boost_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/boost"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:drawableStart="@drawable/ic_boost"
android:drawablePadding="8dp"
android:paddingHorizontal="8dp"
android:drawableTint="@color/boost_icon"
android:textColor="@color/boost_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
tools:text="123"/>
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/favorite_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/favorite"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:drawableStart="@drawable/ic_fluent_star_24_selector"
android:drawablePadding="8dp"
android:paddingHorizontal="8dp"
android:drawableTint="@color/favorite_icon"
android:textColor="@color/favorite_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
tools:text="123"/>
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/bookmark"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:drawableStart="@drawable/ic_fluent_bookmark_24_selector"
android:paddingHorizontal="8dp"
android:drawableTint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/share_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<ImageView
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_fluent_share_24_regular"
android:paddingHorizontal="8dp"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>

View File

@@ -14,7 +14,7 @@
android:paddingTop="16dp"
android:paddingBottom="6dp"
android:textAppearance="@style/m3_title_small"
android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled"
android:drawableStart="@drawable/ic_fluent_arrow_reply_20sp_filled"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="6dp"
android:singleLine="true"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/auto_reveal_never"
android:title="@string/sk_settings_auto_reveal_nobody" />
<item
android:id="@+id/auto_reveal_threads"
android:title="@string/sk_settings_auto_reveal_author" />
<item
android:id="@+id/auto_reveal_discussions"
android:title="@string/sk_settings_auto_reveal_anyone" />
</menu>

View File

@@ -20,11 +20,20 @@
<string name="share_toot_title">শেয়ার করুন</string>
<string name="settings">সেটিংস</string>
<string name="cancel">বাতিল করুন</string>
<plurals name="followers">
<item quantity="one">জন ফলোয়ার</item>
<item quantity="other">জন ফলোয়ারস</item>
</plurals>
<plurals name="posts">
<item quantity="one">পোস্ট</item>
<item quantity="other">পোস্টগুলো</item>
</plurals>
<string name="posts">পোস্টগুলো</string>
<string name="media">মিডিয়া</string>
<string name="button_follow">ফলো করুন</string>
<string name="button_following">ফলো করছেন</string>
<string name="edit_profile">প্রোফাইল সংশোধন করুন</string>
<string name="mention_user">%s -কে পিং করুন</string>
<string name="share_user">%s -কে শেয়ার করুন</string>
<string name="mute_user">%s -কে মিউট করুন</string>
<string name="unmute_user">%s -কে আনমিউট করুন</string>
@@ -53,6 +62,22 @@
<item quantity="one">%d দিন</item>
<item quantity="other">%d দিন</item>
</plurals>
<plurals name="x_seconds_left">
<item quantity="one">%d সেকেন্ড বাকি</item>
<item quantity="other">%d সেকেন্ড বাকি</item>
</plurals>
<plurals name="x_minutes_left">
<item quantity="one">%d মিনিট বাকি</item>
<item quantity="other">%d মিনিট বাকি</item>
</plurals>
<plurals name="x_hours_left">
<item quantity="one">%d ঘণ্টা বাকি</item>
<item quantity="other">%d ঘণ্টা বাকি</item>
</plurals>
<plurals name="x_days_left">
<item quantity="one">%d দিন বাকি</item>
<item quantity="other">%d দিন বাকি</item>
</plurals>
<string name="poll_closed">বন্ধ</string>
<string name="confirm_mute_title">অ্যাকাউন্টটি মিউট করুন</string>
<string name="do_mute">মিউট করুন</string>
@@ -88,6 +113,18 @@
<item quantity="one">%d জন ব্যক্তি বলছেন</item>
<item quantity="other">%d jon ব্যক্তিরা বলছেন</item>
</plurals>
<string name="sending_report">রিপোর্ট পাঠানো হচ্ছে…</string>
<string name="report_sent_title">রিপোর্ট করার জন্য আপনাকে ধন্যবাদ, আমরা এটি শীঘ্রই দেখব.</string>
<string name="report_sent_subtitle">আমরা যতক্ষণে আপনার রিপোর্ট পুনর্বিবেচনা করছি, আপনি %s এর বিরুদ্ধে ব্যবস্থা নিতে পারেন.</string>
<string name="back">ফিরে যান</string>
<string name="search_communities">সার্ভারের নাম বা লিঙ্ক</string>
<string name="instance_rules_title">সার্ভারের নিয়মাবলী</string>
<string name="signup_title">অ্যাকাউন্ট তৈরি করুন</string>
<string name="display_name">নাম</string>
<string name="username">ইউজারনেম</string>
<string name="email">ই-মেইল</string>
<string name="password">পাসওয়ার্ড</string>
<string name="confirm_password">পাসওয়ার্ড নিশ্চিত করুন</string>
<!-- %s is the email address -->
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
<!-- %s is version like 1.2.3 -->
@@ -96,4 +133,8 @@
<!-- %s is server domain -->
<!-- Shown in a progress dialog when you tap "follow all" -->
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="welcome_to_mastodon">Mastodon - এ আপনাকে স্বাগত জানাই</string>
<string name="welcome_paragraph1">Mastodon হল একটি বিকেন্দ্রীভূত সামাজিক নেটওয়ার্ক, যার মানে কোনো একক কোম্পানি এটিকে নিয়ন্ত্রণ করে না। এটি অনেকগুলি স্বাধীনভাবে চালিত সার্ভারের সমন্বয়ে গঠিত, যেখানে সব সার্ভারগুলি একসাথে সংযুক্ত৷</string>
<string name="what_are_servers">সার্ভার কি?</string>
<string name="welcome_paragraph2"><![CDATA[প্রতিটি Mastodon অ্যাকাউন্টকে একটি সার্ভারে হোস্ট করা হয় — প্রত্যেকটির নিজস্ব মান, নিয়ম এবং প্রশাসক (অ্যাডমিন) রয়েছে। আপনি যে কোনো সার্ভারই বেছে নিন না কেন তা বিবেচ্য নয়, আপনি যেকোনো সার্ভারের লোকেদের সাথে যোগাযোগ করতে এবং তাদের ফলো করতে পারেন।]]></string>
</resources>

View File

@@ -166,7 +166,7 @@
<string name="report_sent_subtitle">Während wir den Vorfall überprüfen, kannst du gegen %s weitere Maßnahmen ergreifen.</string>
<string name="unfollow_user">%s entfolgen</string>
<string name="unfollow">Entfolgen</string>
<string name="mute_user_explain">Du wirst die eigenen und geteilten Beiträge des Kontos nicht mehr sehen können. Dass du das Profil stummgeschaltet hast, erfährt die Person nicht.</string>
<string name="mute_user_explain">Du wirst deren (geteilte) Beiträge auf deiner Startseite nicht mehr sehen können. Sie werden nicht erfahren, dass sie stummgeschaltet sind.</string>
<string name="block_user_explain">Dir wird es nicht länger möglich sein, die Beiträge dieses Konto zu sehen. Das blockierte Profil wird nicht mehr in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird mitbekommen, dass du ihr Konto gesperrt hast.</string>
<string name="report_personal_title">Möchtest du das nicht mehr sehen?</string>
<string name="report_personal_subtitle">Wenn du etwas auf Mastodon siehst, das dir nicht gefällt, kannst du die Person aus deinem Umfeld entfernen.</string>
@@ -175,7 +175,7 @@
<string name="instance_catalog_subtitle">Wähle einen Server basierend auf deinen Interessen oder deiner Region oder einfach einen allgemeinen. Du kannst trotzdem mit jedem interagieren, egal auf welchem Server.</string>
<string name="search_communities">Servername oder -adresse</string>
<string name="instance_rules_title">Server-Regeln</string>
<string name="instance_rules_subtitle">Mit dem Fortfahren erklärst du dich damit einverstanden, die folgenden Regeln zu befolgen, die von den %s-Moderatoren aufgestellt und umgesetzt werden.</string>
<string name="instance_rules_subtitle">Solltest du fortfahren, erklärst du dich mit den Serverregeln, die die Moderator*innen von %s aufgestellt haben und durchsetzen werden, einverstanden.</string>
<string name="signup_title">Konto erstellen</string>
<string name="edit_photo">bearbeiten</string>
<string name="display_name">Name</string>
@@ -200,7 +200,7 @@
<string name="confirm_email_title">Überprüfe deinen Posteingang</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">Klicke auf den Link, den wir dir geschickt haben, um %s zu bestätigen. Wir warten hier auf dich.</string>
<string name="confirm_email_didnt_get">Kein Link erhalten?</string>
<string name="confirm_email_didnt_get">Keinen Link erhalten?</string>
<string name="resend">Erneut abschicken</string>
<string name="open_email_app">E-Mail-App öffnen</string>
<string name="resent_email">Bestätigung per E-Mail zugeschickt</string>
@@ -438,8 +438,11 @@
<string name="show">Anzeigen</string>
<string name="hide">Ausblenden</string>
<string name="join_default_server">%s beitreten</string>
<string name="pick_server">Wähle einen anderen Server</string>
<string name="signup_or_login">oder</string>
<string name="learn_more">Mehr erfahren</string>
<string name="welcome_to_mastodon">Willkommen auf Mastodon</string>
<string name="welcome_paragraph1">Mastodon ist ein dezentrales, soziales Netzwerk. Das bedeutet, dass es nicht von einem einzigen Unternehmen kontrolliert wird. Das Netzwerk besteht aus unabhängig voneinander betriebenen Servern, die miteinander verbunden sind.</string>
<string name="what_are_servers">Was sind Server?</string>
<string name="welcome_paragraph2"><![CDATA[Jedes Mastodon-Konto wird auf einem Server gehostet. Jeder Server hat dabei seine eigenen Werte, Regeln und Administrator*innen. Aber egal, für welchen Server Du Dich entscheidest: Du kannst mit Leuten von anderen Servern interagieren und ihnen folgen.]]></string>
</resources>

View File

@@ -274,4 +274,30 @@
<string name="sk_settings_confirm_before_reblog">Vor dem Teilen bestätigen</string>
<string name="sk_reacted">hat reagiert</string>
<string name="sk_reacted_with">hat mit %s reagiert</string>
<string name="sk_external_share_title">Mit Konto teilen</string>
<string name="sk_external_share_or_open_title">Mit Konto teilen oder öffnen</string>
<string name="sk_timeline_bubble">Bubble</string>
<string name="sk_settings_default_content_type_explanation">Vorausgewählter Inhaltstyp für neue Beiträge überschreibt den Wert, der unter „Einstellungen für Beiträge“ gesetzt ist.</string>
<string name="sk_open_in_app_failed">Konnte nicht in der App öffnen</string>
<string name="sk_content_type_unspecified">Nicht angegeben</string>
<string name="sk_content_type_plain">Nur Text</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_content_type">Inhaltstyp</string>
<string name="sk_bubble_timeline_info_banner">Das sind die neuesten Beiträge aus dem Netzwerk, das deine Instanz-Admins kuratiert haben.</string>
<string name="sk_settings_content_types">Formatierung aktivieren</string>
<string name="sk_settings_default_content_type">Standard-Inhaltstyp</string>
<string name="sk_instance_info_unavailable">Informationen zur Instanz momentan nicht verfügbar</string>
<string name="sk_open_in_app">In App öffnen</string>
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string>
<string name="sk_settings_allow_remote_loading">Infos von Remote-Instanzen laden</string>
<string name="sk_no_remote_info_hint">keine Remote-Infos abrufbar</string>
<string name="sk_error_loading_profile">Konnte das Profil via %s nicht laden</string>
<string name="sk_settings_allow_remote_loading_explanation">Für vollständigere Auflistung von Follower*innen, Likes und Boosts können die Informationen von der Ursprungs-Instanz geladen werden.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Zeigen von gleichen CWs in Antworten von</string>
<string name="sk_settings_auto_reveal_nobody">niemandem</string>
<string name="sk_settings_auto_reveal_author">Autor*in</string>
<string name="sk_settings_auto_reveal_anyone">allen</string>
</resources>

View File

@@ -286,4 +286,19 @@
<string name="sk_settings_default_content_type">Contenido por defecto</string>
<string name="sk_settings_content_types_explanation">Permite establecer un tipo de contenido como Markdown al crear una entrada. Ten en cuenta que no todas las instancias lo admiten.</string>
<string name="sk_settings_default_content_type_explanation">Permite preseleccionar un tipo de contenido al crear nuevas entradas, anulando el valor establecido en \"Preferencias de publicación\".</string>
<string name="sk_bubble_timeline_info_banner">Estas son las publicaciones más recientes de la red seleccionadas por los administradores de tu instancia.</string>
<string name="sk_timeline_bubble">Burbuja</string>
<string name="sk_instance_info_unavailable">Información de la instancia temporalmente no disponible</string>
<string name="sk_external_share_or_open_title">Compartir o abrir con una cuenta</string>
<string name="sk_open_in_app">Abrir en la app</string>
<string name="sk_external_share_title">Compartir con una cuenta</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Mostrar CW iguales en las respuestas de</string>
<string name="sk_settings_auto_reveal_nobody">nadie</string>
<string name="sk_settings_auto_reveal_author">autor</string>
<string name="sk_settings_auto_reveal_anyone">todos</string>
<string name="sk_open_in_app_failed">No se pudo abrir en la aplicación</string>
<string name="sk_no_remote_info_hint">no hay información remota disponible</string>
<string name="sk_error_loading_profile">No se pudo cargar el perfil a través de %s</string>
<string name="sk_settings_allow_remote_loading">Cargar la información desde las instancias remotas</string>
<string name="sk_settings_allow_remote_loading_explanation">Intenta obtener listas más precisas de seguidores, Me gusta y promociones cargando la información desde la instancia de origen.</string>
</resources>

View File

@@ -287,4 +287,15 @@
<string name="sk_content_type_unspecified">Non spécifié</string>
<string name="sk_settings_content_types_explanation">Permet de définir un type de contenu comme Markdown lors de la création d\'un message. Gardez à l\'esprit que toutes les instances ne le prennent pas en charge.</string>
<string name="sk_settings_default_content_type">Type de contenu par défaut</string>
<string name="sk_open_in_app">Ouvrir dans l\'application</string>
<string name="sk_external_share_title">Partager avec le compte</string>
<string name="sk_external_share_or_open_title">Partager ou ouvrir avec le compte</string>
<string name="sk_bubble_timeline_info_banner">Ce sont les messages les plus récents du réseau organisés par vos administrateurs d\'instance.</string>
<string name="sk_timeline_bubble">Bulle</string>
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
<string name="sk_open_in_app_failed">Impossible de l\'ouvrir dans l\'application</string>
<string name="sk_settings_allow_remote_loading">Charger des informations à partir d\'instances distantes</string>
<string name="sk_no_remote_info_hint">informations distantes indisponibles</string>
<string name="sk_error_loading_profile">Échec du chargement du profil via %s</string>
<string name="sk_settings_allow_remote_loading_explanation">Essayez de récupérer des listes plus précises pour les abonnés, les likes et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
</resources>

View File

@@ -90,7 +90,7 @@
<string name="sk_loading_fediverse_resource_title">Buscando no Fediverso</string>
<string name="sk_reblog_with_visibility">Impulsar con visibilidade</string>
<string name="sk_quote_post">Publicar acerca disto</string>
<string name="sk_undo_reblog">Desfacer o impulso</string>
<string name="sk_undo_reblog">Desfacer impulso</string>
<string name="sk_copy_link_to_post">Copiar ligazón á publicación</string>
<string name="sk_loading_resource_on_instance_title">Buscando en %s</string>
<string name="sk_open_with_account">Abrir con outra conta</string>
@@ -286,4 +286,10 @@
<string name="sk_content_type">Tipo de contido</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_settings_content_types_explanation">Permite configurar un tipo de contido como Markdown ao crear unha publicación. Teña en conta que non tódalas instancias soportan isto.</string>
<string name="sk_bubble_timeline_info_banner">Estas son as publicacións máis recentes da xente na burbulla do seu servidor Akkoma.</string>
<string name="sk_timeline_bubble">Burbulla</string>
<string name="sk_instance_info_unavailable">Información da instancia temporalmente non dispoñible</string>
<string name="sk_open_in_app">Abrir na aplicación</string>
<string name="sk_external_share_title">Compartir coa conta</string>
<string name="sk_external_share_or_open_title">Compartir ou abrir coa conta</string>
</resources>

View File

@@ -287,4 +287,15 @@
<string name="sk_content_type">Jenis konten</string>
<string name="sk_content_type_unspecified">Tidak ditentukan</string>
<string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string>
<string name="sk_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yang paling terkini dari jaringan dikurasikan oleh admin server Anda.</string>
<string name="sk_timeline_bubble">Gelembung</string>
<string name="sk_instance_info_unavailable">Info server sementara tidak tersedia</string>
<string name="sk_external_share_or_open_title">Bagikan atau buka dengan akun</string>
<string name="sk_no_remote_info_hint">info jarak jauh tidak tersedia</string>
<string name="sk_settings_allow_remote_loading">Muat info dari server jarak jauh</string>
<string name="sk_settings_allow_remote_loading_explanation">Coba mendapatkan pendaftaran akurat untuk pengikut cr</string>
<string name="sk_error_loading_profile">Gagal memuat profil melalui %s</string>
<string name="sk_open_in_app_failed">Tidak dapat buka dalam aplikasi</string>
</resources>

View File

@@ -274,4 +274,26 @@
<string name="sk_compact_reblog_reply_line">Linea boost/risposta compatta</string>
<string name="sk_reacted_with">ha reagito con %s</string>
<string name="sk_reply_line_above_avatar">Linea \"In risposta a\" sopra l\'avatar</string>
<string name="sk_bubble_timeline_info_banner">Questi sono i post più recenti della rete, curati dagli amministratori della tua istanza.</string>
<string name="sk_settings_content_types_explanation">Permette di impostare un tipo di contenuto, come Markdown, quando si crea un post. Tieni presente che non tutte le istanze lo supportano.</string>
<string name="sk_open_in_app_failed">Impossibile aprire nell\'app</string>
<string name="sk_external_share_title">Condividi con l\'account</string>
<string name="sk_external_share_or_open_title">Condividi o apri con l\'account</string>
<string name="sk_no_remote_info_hint">Informazioni remote non disponibili</string>
<string name="sk_error_loading_profile">Impossibile caricare il profilo tramite %s</string>
<string name="sk_settings_allow_remote_loading">Carica informazioni da istanze remote</string>
<string name="sk_settings_allow_remote_loading_explanation">Cerca di ottenere elenchi più accurati di follower, like e boost caricando le informazioni dall\'istanza di origine.</string>
<string name="sk_content_type">Tipo di contenuto</string>
<string name="sk_timeline_bubble">Bolla</string>
<string name="sk_content_type_unspecified">Non specificato</string>
<string name="sk_content_type_plain">Testo semplice</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Abilita la formattazione dei post</string>
<string name="sk_settings_default_content_type">Tipo di contenuto predefinito</string>
<string name="sk_settings_default_content_type_explanation">Ti permette di preselezionare un tipo di contenuto quando si creano nuovi post, sovrascrivendo il valore impostato in \"Preferenze di pubblicazione\".</string>
<string name="sk_instance_info_unavailable">Informazioni sull\'istanza temporaneamente non disponibili</string>
<string name="sk_open_in_app">Apri nell\'app</string>
</resources>

View File

@@ -274,4 +274,15 @@
<string name="sk_settings_confirm_before_reblog">Potwierdź przed podbiciem</string>
<string name="sk_reacted_with">zareagował(a) z %s</string>
<string name="sk_reacted">zareagował(a)</string>
<string name="sk_settings_default_content_type">Domyślny rodzaj treści</string>
<string name="sk_instance_info_unavailable">Informacje o instancji są tymczasowo niedostępne</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_content_type">Rodzaj treści</string>
<string name="sk_content_type_unspecified">Nie określono</string>
<string name="sk_content_type_plain">Czysty tekst</string>
<string name="sk_settings_content_types">Włącz formatowanie wpisu</string>
<string name="sk_open_in_app">Otwórz w aplikacji</string>
<string name="sk_external_share_title">Udostępnij z kontem</string>
</resources>

View File

@@ -10,11 +10,13 @@
<string name="ok">OK</string>
<string name="preparing_auth">Förbereder för autentisering…</string>
<string name="finishing_auth">Slutför autentisering…</string>
<string name="user_boosted">%s boostade</string>
<string name="in_reply_to">Som svar på %s</string>
<string name="notifications">Notiser</string>
<string name="user_followed_you">följde dig</string>
<string name="user_sent_follow_request">skickade en förfrågning om att följa till dig</string>
<string name="user_favorited">favoritmarkerade dit inlägg</string>
<string name="notification_boosted">boostade ditt inlägg</string>
<string name="poll_ended">omröstning avslutad</string>
<string name="time_seconds">%ds</string>
<string name="time_minutes">%dm</string>
@@ -164,6 +166,7 @@
<string name="report_sent_subtitle">Medan vi granskar detta kan du vidta åtgärder mot %s.</string>
<string name="unfollow_user">Avfölj %s</string>
<string name="unfollow">Avfölj</string>
<string name="mute_user_explain">Du kommer inte att se deras inlägg eller boosts i ditt hemflöde. De kommer inte veta att de har blivit tystade.</string>
<string name="block_user_explain">De kommer inte längre att kunna följa eller se dina inlägg, men de kan se om de har blockerats.</string>
<string name="report_personal_title">Vill du inte se det här?</string>
<string name="report_personal_subtitle">När du ser något som du inte gillar på Mastodon kan du ta bort personen från din upplevelse.</string>
@@ -266,6 +269,7 @@
<string name="hide_content">Dölj innehåll</string>
<string name="new_post">Nytt inlägg</string>
<string name="button_reply">Svara</string>
<string name="button_reblog">Boosta</string>
<string name="button_favorite">Favoritmarkera</string>
<string name="button_share">Dela</string>
<string name="media_no_description">Media utan beskrivning</string>
@@ -412,7 +416,20 @@
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="signup_email_domain_blocked">%1$s tillåter inte registrering från %2$s. Prova en annan eller &lt;a&gt;välj en annan server&lt;/a&gt;.</string>
<string name="signup_username_taken">Det här användarnamnet är redan taget.</string>
<string name="spoiler_show">Visa ändå</string>
<string name="poll_multiple_choice">Välj en eller flera</string>
<string name="save_changes">Spara ändringar</string>
<string name="profile_timeline">Tidslinje</string>
<string name="view_all">Visa alla</string>
<string name="profile_endorsed_accounts">Konton</string>
<string name="verified_link">Verifierad länk</string>
<string name="show">Visa</string>
<string name="hide">Dölj</string>
<string name="join_default_server">Gå med %s</string>
<string name="signup_or_login">eller</string>
<string name="learn_more">Läs mer</string>
<string name="welcome_to_mastodon">Välkommen till Mastodon</string>
<string name="welcome_paragraph1">Mastodon är ett decentraliserat socialt nätverk, vilket innebär att inget enskilt företag kontrollerar det. Det består av många oberoende servrar, alla sammankopplade.</string>
<string name="what_are_servers">Vad är servrar?</string>
<string name="welcome_paragraph2"><![CDATA[Varje Mastodon-konto finns på en server — var och en med sina värderingar, regler och administratörer. Oavsett vilken du väljer kan du följa och interagera med människor på vilken server som helst.]]></string>
</resources>

View File

@@ -6,17 +6,17 @@
<string name="next">Sonraki</string>
<string name="loading_instance">Sunucu bilgisi alınıyor…</string>
<string name="error">Hata</string>
<string name="not_a_mastodon_instance">%s bir Mastodon sunucusu gibi görünmüyor.</string>
<string name="not_a_mastodon_instance">%s bir Mastodon sunucusu gibi görükmüyor.</string>
<string name="ok">Tamam</string>
<string name="preparing_auth">Kimlik doğrulama için hazırlanıyor…</string>
<string name="finishing_auth">Kimlik doğrulama tamamlanıyor…</string>
<string name="user_boosted">%s yineledi</string>
<string name="user_boosted">%s paylaştı</string>
<string name="in_reply_to">%s için yanıt</string>
<string name="notifications">Bildirimler</string>
<string name="user_followed_you">sizi takip etti</string>
<string name="user_sent_follow_request">sana bir takip isteği gönderdi</string>
<string name="user_favorited">gönderinizi favorilerine ekledi</string>
<string name="notification_boosted">gönderinizi yineledi</string>
<string name="notification_boosted">gönderinizi paylaştı</string>
<string name="poll_ended">oylama sona erdi</string>
<string name="time_seconds">%ds</string>
<string name="time_minutes">%ddk</string>
@@ -123,7 +123,7 @@
<string name="delete">Sil</string>
<string name="confirm_delete_title">Gönderiyi sil</string>
<string name="confirm_delete">Bu gönderiyi silmek istediğinizden emin misiniz?</string>
<string name="deleting">Siliniyor...</string>
<string name="deleting">Siliniyor</string>
<string name="notification_channel_audio_player">Ses çal</string>
<string name="play">Oynat</string>
<string name="pause">Durdur</string>
@@ -149,7 +149,7 @@
<string name="report_choose_reason_subtitle">En iyi eşleşmeyi seçin</string>
<string name="report_reason_personal">Hoşuma gitmiyor</string>
<string name="report_reason_personal_subtitle">Görmek isteyeceğin bir şey değil</string>
<string name="report_reason_spam">Bu spam</string>
<string name="report_reason_spam">Spam</string>
<string name="report_reason_spam_subtitle">Kötü amaçlı bağlantılar, sahte etkileşim veya tekrarlayan yanıtlar</string>
<string name="report_reason_violation">Sunucu kurallarını ihlal ediyor</string>
<string name="report_reason_violation_subtitle">Belirli kuralları çiğnediğinin farkındasınız</string>
@@ -166,7 +166,7 @@
<string name="report_sent_subtitle">Biz bunu incelerken siz %s karşı önlem alabilirsiniz.</string>
<string name="unfollow_user">Takipten çık %s</string>
<string name="unfollow">Takipten çık</string>
<string name="mute_user_explain">Ana sayfa akışınızda kişinin gönderilerini görmeyeceksiniz. Sessize alındıklarını bilemeyecekler.</string>
<string name="mute_user_explain">Anasayfa akışınızda kişinin gönderilerini görmeyeceksiniz. Sessize alındıklarını bilemeyecekler.</string>
<string name="block_user_explain">Artık sizi takip edemez ve gönderilerinizi göremezler ama engellendiklerini görebilirler.</string>
<string name="report_personal_title">Bunu görmek istemiyor musun?</string>
<string name="report_personal_subtitle">Mastodon\'da beğenmediğiniz bir şey gördüğünüzde, o kişiyi deneyiminizden çıkarabilirsiniz.</string>
@@ -200,10 +200,10 @@
<string name="confirm_email_title">E-posta Kutunuzu Kontrol Edin</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">%s işleminizi doğrulamak için size gönderdiğimiz linke tıklayınız. Biz burada bekleyeceğiz.</string>
<string name="confirm_email_didnt_get">Link size ulaşmadı mı?</string>
<string name="confirm_email_didnt_get">Bağlantı size ulaşmadı mı?</string>
<string name="resend">Yeniden gönder</string>
<string name="open_email_app">E-posta uygulamasını</string>
<string name="resent_email">Onay e-postası gönderildi</string>
<string name="open_email_app">Eposta uygulamasını</string>
<string name="resent_email">Onay epostası gönderildi</string>
<string name="compose_hint">Aklınızdan geçenleri yazın veya yapıştırın</string>
<string name="content_warning">İçerik Uyarısı</string>
<string name="add_image_description">Resim açıklaması ekle…</string>
@@ -243,18 +243,18 @@
<string name="settings_gif">Animasyonlu avatarları ve emojileri oynat</string>
<string name="settings_custom_tabs">Uygulama içi tarayıcıyı kullan</string>
<string name="settings_notifications">Bildirimler</string>
<string name="notify_me_when">Beni şu durumda bilgilendir: </string>
<string name="notify_anyone">Herhangi biri</string>
<string name="notify_me_when">Beni şu durumda bilgilendir</string>
<string name="notify_anyone">Herhangibiri</string>
<string name="notify_follower">Bir takipçim</string>
<string name="notify_followed">Takip ettiğim biri</string>
<string name="notify_none">Bilgilendirme</string>
<string name="notify_favorites">Gönderimi favorilerine eklediğinde</string>
<string name="notify_follow">Beni takip ettiğinde</string>
<string name="notify_reblog">Gönderimi yinelediğinde</string>
<string name="notify_reblog">Gönderimi paylaştığında</string>
<string name="notify_mention">Benden bahsettiğinde</string>
<string name="settings_boring">Sıkıcı bölge</string>
<string name="settings_account">Hesap ayarları</string>
<string name="settings_contribute">Mastodon\'a katkıda bulunun</string>
<string name="settings_contribute">Mastodona katkıda bulunun</string>
<string name="settings_tos">Kullanım Şartları</string>
<string name="settings_privacy_policy">Gizlilik Politikası</string>
<string name="settings_spicy">Tehlikeli bölge</string>
@@ -271,7 +271,7 @@
<string name="hide_content">İçeriği gizle</string>
<string name="new_post">Yeni gönderi</string>
<string name="button_reply">Cevapla</string>
<string name="button_reblog">Yinele</string>
<string name="button_reblog">Yeniden Paylaş</string>
<string name="button_favorite">Favorile</string>
<string name="button_share">Paylaş</string>
<string name="media_no_description">ıklamasız medya</string>
@@ -282,7 +282,7 @@
<string name="home_timeline">Anasayfa</string>
<string name="my_profile">Profilim</string>
<string name="media_viewer">Medya görüntüleyici</string>
<string name="follow_user">%s\'yi takip et</string>
<string name="follow_user">%s \'yi takip et</string>
<string name="unfollowed_user">%s takip edilmedi</string>
<string name="followed_user">%s kişisini takip ediyorsunuz</string>
<string name="following_user_requested">%s takip isteği gönderdi</string>
@@ -310,11 +310,11 @@
<string name="local_timeline_info_banner">Bu gönderiler seninle aynı Mastodon sunucusunda olan kişilerin paylaştığı son gönderilerdir.</string>
<string name="dismiss">Yoksay</string>
<string name="see_new_posts">Yeni gönderileri gör</string>
<string name="load_missing_posts">Daha fazla gönderi yükle</string>
<string name="follow_back">Geri Takip Et</string>
<string name="load_missing_posts">Daha fazlası</string>
<string name="follow_back">Takip edeni Takip Et</string>
<string name="button_follow_pending">Bekliyor</string>
<string name="follows_you">Seni takip ediyor</string>
<string name="manually_approves_followers">Takipçileri manuel kabul eder</string>
<string name="manually_approves_followers">Takipçileri elle kabul et</string>
<string name="current_account">Kullanılan hesap</string>
<string name="log_out_account">%s oturumunu kapat</string>
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
@@ -331,8 +331,8 @@
<item quantity="other">%,d favori</item>
</plurals>
<plurals name="x_reblogs">
<item quantity="one">%,d yineleme</item>
<item quantity="other">%,d yineleme</item>
<item quantity="one">%,d paylaşma</item>
<item quantity="other">%,d paylaşma</item>
</plurals>
<string name="timestamp_via_app">%1$s tarihinde %2$s uygulamasıyla</string>
<string name="time_now">şimdi</string>
@@ -361,7 +361,7 @@
<string name="edit_media_added">Medya eklendi</string>
<string name="edit_media_removed">Medya kaldırıldı</string>
<string name="edit_media_reordered">Medya düzenlendi</string>
<string name="edit_marked_sensitive">Hassas olarak işarlendi</string>
<string name="edit_marked_sensitive">Hassas olarak işaretlendi</string>
<string name="edit_marked_not_sensitive">Hassas değil olarak işaretlendi</string>
<string name="edit_multiple_changed">Gönderi düzenlendi</string>
<string name="edit">Düzenle</string>
@@ -373,16 +373,16 @@
<string name="file_size_gb">%.2f GB</string>
<string name="file_upload_progress">%2$s dosyadan %1$s</string>
<string name="file_upload_time_remaining">%s kaldı</string>
<string name="upload_error_connection_lost">Cihazınızın internet bağlantısı koptu</string>
<string name="upload_error_connection_lost">Cihazınızın bağlantısı koptu</string>
<string name="upload_processing">İşleniyor…</string>
<!-- %s is version like 1.2.3 -->
<string name="update_available">Mastodon Android uygulamasının %s versiyonu indirmeye hazır.</string>
<string name="update_available">Mastodon, Android uygulamasının %s versiyonu indirmeye hazır.</string>
<!-- %s is version like 1.2.3 -->
<string name="update_ready">Mastodon Android uygulamasının %s versiyonu indirildi ve kurulmaya hazır.</string>
<string name="update_ready">Mastodon , Android uygulamasının %s versiyonu indirildi ve kurulmaya hazır.</string>
<!-- %s is file size -->
<string name="download_update">İndir (%s)</string>
<string name="install_update">Kur</string>
<string name="privacy_policy_title">Gizliliğiniz</string>
<string name="privacy_policy_title">Gizliliğiniz.</string>
<string name="privacy_policy_subtitle">Mastodon uygulaması herhangi bir veri toplama dahi katıldığınız sunucunun farklı bir politikası olabilir. \n\n %s politikası sizi tatmin etmiyorsa geri giderek farklı bir sunucu seçebilirsiniz.</string>
<string name="i_agree">Kabul ediyorum</string>
<string name="empty_list">Bu liste boş</string>
@@ -392,9 +392,9 @@
<string name="remove_bookmark">Yer İmi Kaldır</string>
<string name="bookmarks">Yer İmleri</string>
<string name="your_favorites">Favorilerin</string>
<string name="login_title">Hoşgeldin</string>
<string name="login_title">Hoşgeldiniz</string>
<string name="login_subtitle">Hesabınızı oluşturduğunuz sunucu ile giriş yapın.</string>
<string name="server_url">Sunucu URL\'si</string>
<string name="server_url">Sunucu bağlantısı</string>
<string name="signup_random_server_explain">Herhangi bir seçim yapmadan devam ederseniz dilinize göre bir sunucu seçeceğiz.</string>
<string name="server_filter_any_language">Herhangi Bir Dil</string>
<string name="server_filter_instant_signup">Koşulsuz Kayıt</string>
@@ -419,7 +419,7 @@
<string name="server_rules_disagree">Reddet</string>
<string name="privacy_policy_explanation">Kısacası: Hiçbir veri işlemiyor ya da toplamıyoruz.</string>
<!-- %s is server domain -->
<string name="server_policy_disagree">%s\'yi reddet</string>
<string name="server_policy_disagree">%s \'yi reddet</string>
<string name="profile_bio">Hakkımda</string>
<!-- Shown in a progress dialog when you tap "follow all" -->
<string name="sending_follows">Hesaplar takip ediliyor…</string>
@@ -437,12 +437,12 @@
<string name="verified_link">Onaylanmış bağlantı</string>
<string name="show">Göster</string>
<string name="hide">Gizle</string>
<string name="join_default_server">%s\'e katıl</string>
<string name="join_default_server">%s katıl</string>
<string name="pick_server">Başka sunucu seç</string>
<string name="signup_or_login">veya</string>
<string name="learn_more">Daha fazla bilgi edinin</string>
<string name="learn_more">Daha fazlası</string>
<string name="welcome_to_mastodon">Mastodon\'a hoş geldiniz</string>
<string name="welcome_paragraph1">Mastodon merkezi olmayan bir sosyal ağdır, yani tek bir şirket tarafından kontrol edilmemektedir. Hepsi birbirine bağlı, bağımsız olarak işletilen birçok sunucudan oluşur.</string>
<string name="welcome_paragraph1">Mastodon , Merkezi olmayan bir sosyal ağdır, yani tek bir şirket tarafından kontrol edilmemektedir. Hepsi birbirine bağlı, bağımsız olarak işletilen birçok sunucudan oluşur.</string>
<string name="what_are_servers">Sunucular nelerdir?</string>
<string name="welcome_paragraph2"><![CDATA[Her Mastodon hesabı bir sunucuda barındırılır - her birinin kendi değerleri, kuralları ve yöneticileri vardır. Hangisini seçerseniz seçin, herhangi bir sunucudaki insanları takip edebilir ve onlarla etkileşime geçebilirsiniz.]]></string>
<string name="welcome_paragraph2"><![CDATA[Her Mastodon , hesabı bir sunucuda barındırılır - her birinin kendi değerleri, kuralları ve yöneticileri vardır. Hangisini seçerseniz seçin, herhangi bir sunucudaki insanları takip edebilir ve onlarla etkileşime geçebilirsiniz.]]></string>
</resources>

View File

@@ -286,4 +286,15 @@
<string name="sk_settings_default_content_type_explanation">Це дозволяє вам попередньо вибрати тип вмісту під час написання нових дописів, замінивши значення, встановлене в «Налаштуваннях постингу».</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_settings_content_types_explanation">Дозволяє налаштувати тип вмісту, наприклад, Markdown, під час написання допису. Зауважте, що не всі сервери підтримують цю функцію.</string>
<string name="sk_open_in_app">Відкрити у застосунку</string>
<string name="sk_external_share_title">Поділитися через обліковий запис</string>
<string name="sk_bubble_timeline_info_banner">Це найновіші дописи з мережі керованої адміністраторами вашого сервера.</string>
<string name="sk_timeline_bubble">Бульбашка</string>
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</string>
<string name="sk_open_in_app_failed">Не вдалося відкрити в застосунку</string>
<string name="sk_no_remote_info_hint">віддалена інформація недоступна</string>
<string name="sk_settings_allow_remote_loading">Завантажити інформацію з віддалених серверів</string>
<string name="sk_settings_allow_remote_loading_explanation">Спробуйте отримати точніші списки підписників, вподобань і поширень, завантаживши інформацію з джерела.</string>
<string name="sk_error_loading_profile">Не вдалося завантажити профіль на ваш домашній сервер.</string>
</resources>

View File

@@ -21,6 +21,7 @@
<attr name="colorAccentLightest" format="color"/>
<attr name="profileHeaderBackground" format="color"/>
<attr name="toolbarBackground" format="color"/>
<attr name="colorIconDisabled" format="color"/>
<attr name="colorButtonBackgroundPrimaryDarkOnLight" format="color"/>
<attr name="colorButtonBackgroundPrimaryDarkOnLightDisabled" format="color"/>
@@ -73,6 +74,7 @@
<declare-styleable name="MaxWidthFrameLayout">
<attr name="android:maxWidth" format="dimension"/>
<attr name="defaultWidth" format="dimension" />
</declare-styleable>
<declare-styleable name="FloatingHintEditTextLayout">

View File

@@ -3,4 +3,5 @@
<dimen name="text_max_height">220dp</dimen>
<dimen name="text_collapsed_height">145dp</dimen>
<dimen name="layout_max_width">450dp</dimen>
<dimen name="scroll_to_top_delta">300dp</dimen>
</resources>

View File

@@ -30,7 +30,7 @@
<string name="sk_user_post_notifications_off">Turned off post notifications for %s</string>
<string name="sk_federated_timeline">Federation</string>
<string name="sk_federated_timeline_info_banner">These are the most recent posts by the people in your federation.</string>
<string name="sk_bubble_timeline_info_banner">These are the most recent posts by the people in your Akkoma server\'s bubble.</string>
<string name="sk_bubble_timeline_info_banner">These are the most recent posts from the network curated by your instance admins.</string>
<string name="sk_update_available">Megalodon %s is ready to download.</string>
<string name="sk_update_ready">Megalodon %s is downloaded and ready to install.</string>
<string name="sk_check_for_update">Check for update</string>
@@ -290,6 +290,15 @@
<string name="sk_settings_default_content_type_explanation">This lets you have a content type be pre-selected when creating new posts, overriding the value set in “Posting preferences”.</string>
<string name="sk_instance_info_unavailable">Instance info temporarily unavailable</string>
<string name="sk_open_in_app">Open in app</string>
<string name="sk_open_in_app_failed">Could not open in app</string>
<string name="sk_external_share_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open with account</string>
<string name="sk_no_remote_info_hint">remote info unavailable</string>
<string name="sk_error_loading_profile">Failed loading the profile via %s</string>
<string name="sk_settings_allow_remote_loading">Load info from remote instances</string>
<string name="sk_settings_allow_remote_loading_explanation">Try fetching more accurate listings for followers, likes and boosts by loading the information from the instance of origin.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Reveal same CWs in replies from</string>
<string name="sk_settings_auto_reveal_nobody">nobody</string>
<string name="sk_settings_auto_reveal_author">author</string>
<string name="sk_settings_auto_reveal_anyone">everyone</string>
</resources>

View File

@@ -28,6 +28,7 @@
<item name="android:colorBackground">?colorGray100</item>
<item name="android:textColorPrimary">?colorGray800</item>
<item name="android:textColorSecondary">?colorGray500</item>
<item name="colorIconDisabled">?colorGray300</item>
<item name="colorButtonText">?colorGray50</item>
<item name="colorSecondary">#E9EDF2</item>
<item name="colorBackgroundLight">?colorGray50</item>
@@ -127,6 +128,7 @@
<item name="android:colorBackground">?colorGray700</item>
<item name="android:textColorPrimary">?colorGray50</item>
<item name="android:textColorSecondary">?colorGray400</item>
<item name="colorIconDisabled">?colorGray500</item>
<item name="colorButtonText">?colorGray800</item>
<item name="colorSecondary">#E9EDF2</item>
<item name="colorBackgroundLight">?colorGray700</item>