Compare commits

..

77 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
63 changed files with 1104 additions and 331 deletions

View File

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

View File

@@ -53,24 +53,30 @@ public class ThreadFragmentTest {
} }
@Test @Test
public void updateMainStatus() { public void maybeApplyMainStatus() {
ThreadFragment fragment = new ThreadFragment(); ThreadFragment fragment = new ThreadFragment();
fragment.contextInitiallyRendered = true;
fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH); fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH);
Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH); Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH);
update1.editedAt = Instant.ofEpochSecond(1); update1.editedAt = Instant.ofEpochSecond(1);
fragment.updatedStatus = update1; fragment.updatedStatus = update1;
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.updateMainStatus(); StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.maybeApplyMainStatus();
assertEquals("fired update event", update1, event1.status); assertEquals("fired update event", update1, event1.status);
assertEquals("updated main status", update1, fragment.mainStatus); assertEquals("updated main status", update1, fragment.mainStatus);
Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH); Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH);
update2.favouritesCount = 123; update2.favouritesCount = 123;
fragment.updatedStatus = update2; fragment.updatedStatus = update2;
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.updateMainStatus(); StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.maybeApplyMainStatus();
assertEquals("only fired counter update event", update2.id, event2.id); assertEquals("only fired counter update event", update2.id, event2.id);
assertEquals("updated counter is correct", 123, event2.favorites); assertEquals("updated counter is correct", 123, event2.favorites);
assertEquals("updated main status", update2, fragment.mainStatus); 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 @Test

View File

@@ -9,6 +9,7 @@ import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ComposeFragment;
@@ -43,7 +44,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
finish(); finish();
} else if (isOpenable || sessions.size() > 1) { } else if (isOpenable || sessions.size() > 1) {
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable); AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
if (isOpenable) sheet.setOnClick((accountId, open) -> { sheet.setOnClick((accountId, open) -> {
if (open && text.isPresent()) { if (open && text.isPresent()) {
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> { BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
if (clazz == null) { if (clazz == null) {
@@ -59,8 +60,17 @@ public class ExternalShareActivity extends FragmentStackActivity{
finish(); finish();
startActivity(intent); startActivity(intent);
}; };
if (isFediUrl) UiUtils.lookupURL(this, accountId, text.get(), false, callback);
else UiUtils.lookupAccountHandle(this, accountId, fediHandle.get(), callback); 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 { } else {
openComposeFragment(accountId); openComposeFragment(accountId);
} }

View File

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

View File

@@ -127,8 +127,8 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
} }
private void showFragmentForExternalShare(Bundle args) { private void showFragmentForExternalShare(Bundle args) {
String clazz = args.getString("fromExternalShare"); String className = args.getString("fromExternalShare");
Fragment fragment = switch (clazz) { Fragment fragment = switch (className) {
case "ThreadFragment" -> new ThreadFragment(); case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment(); case "ProfileFragment" -> new ProfileFragment();
default -> null; default -> null;

View File

@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
@Override @Override
public void onSuccess(Token token){ public void onSuccess(Token token){
new GetOwnAccount() new GetOwnAccount()
// in case the instance (looking at pixelfed) wants to redirect to a
// website, we need to pass a context so we can launch a browser
.setContext(OAuthActivity.this)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Account account){ public void onSuccess(Account account){

View File

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

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.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@@ -161,6 +162,11 @@ public class MastodonAPIController{
respObj=gson.fromJson(reader, req.respClass); respObj=gson.fromJson(reader, req.respClass);
} }
}catch(JsonIOException|JsonSyntaxException x){ }catch(JsonIOException|JsonSyntaxException x){
if (req.context != null && response.body().contentType().subtype().equals("html")) {
UiUtils.launchWebBrowser(req.context, response.request().url().toString());
req.cancel();
return;
}
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x); req.onError(x.getLocalizedMessage(), response.code(), x);

View File

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

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); return sessions.get(id);
} }
@Nullable
public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id);
}
@Nullable @Nullable
public AccountSession getLastActiveAccount(){ public AccountSession getLastActiveAccount(){
if(sessions.isEmpty() || lastActiveAccountID==null) if(sessions.isEmpty() || lastActiveAccountID==null)

View File

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

View File

@@ -71,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, 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 ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter; protected DisplayItemsAdapter adapter;
protected String accountID; protected String accountID;
@@ -680,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
protected int getListWidthForMediaLayout(){ protected int getListWidthForMediaLayout(){
return list.getWidth(); return list.getWidth();
} }
@@ -749,6 +754,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon())); assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
} }
@Override
protected void onDataLoaded(List<T> d, boolean more) {
super.onDataLoaded(d, more);
// more available, but the page isn't even full yet? seems wrong, let's load some more
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){ public DisplayItemsAdapter(){

View File

@@ -149,7 +149,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
private static final int MEDIA_RESULT=717; private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363; private static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -355,6 +355,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} else { } else {
mediaBtn.setOnClickListener(v -> openFilePicker(false)); mediaBtn.setOnClickListener(v -> openFilePicker(false));
} }
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
pollBtn.setOnClickListener(v->togglePoll()); pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText)); emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler()); spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -847,12 +848,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null); updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton); buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) { if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
// editing an already published post // editing an already published post
draftsBtn.setVisibility(View.GONE); draftsBtn.setVisibility(View.GONE);
} }
} }
@Override
public String getAccountID() {
return accountID;
}
private void navigateToUnsentPosts() { private void navigateToUnsentPosts() {
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
@@ -1009,7 +1016,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.state!=AttachmentUploadState.DONE) if(att.state!=AttachmentUploadState.DONE)
nonDoneAttachmentCount++; nonDoneAttachmentCount++;
} }
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1)); publishButton.setEnabled((!isInstancePixelfed() || attachments.size() > 0) && (trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
sendError.setVisibility(View.GONE); sendError.setVisibility(View.GONE);
} }
@@ -1151,7 +1158,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
sendProgress.setVisibility(View.VISIBLE); sendProgress.setVisibility(View.VISIBLE);
sendError.setVisibility(View.GONE); sendError.setVisibility(View.GONE);
Callback<Status> resCallback=new Callback<>(){ Callback<Status> resCallback = new Callback<>(){
@Override @Override
public void onSuccess(Status result){ public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> { maybeDeleteScheduledPost(() -> {
@@ -1164,7 +1171,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
E.post(new StatusCountersUpdatedEvent(replyTo)); E.post(new StatusCountersUpdatedEvent(replyTo));
} }
}else{ }else{
E.post(new StatusUpdatedEvent(result)); // pixelfed doesn't return the edited status :/
Status editedStatus = result == null ? editingStatus : result;
if (result == null) {
editedStatus.text = req.status;
editedStatus.spoilerText = req.spoilerText;
editedStatus.sensitive = req.sensitive;
editedStatus.language = req.language;
// user will have to reload to see html
editedStatus.content = req.status;
}
E.post(new StatusUpdatedEvent(editedStatus));
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
Nav.finish(ComposeFragment.this); Nav.finish(ComposeFragment.this);
@@ -1899,9 +1916,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityPopup=new PopupMenu(getActivity(), v); visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility); visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu(); Menu m=visibilityPopup.getMenu();
if (isInstancePixelfed()) {
m.findItem(R.id.vis_private).setVisible(false);
}
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only); MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID); boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.isAkkoma()) { if (isInstanceAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true); m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) { } else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true); localOnlyItem.setVisible(true);

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent; import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
@@ -25,6 +26,8 @@ import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
@@ -227,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
} }
} }
// copied from StatusListFragment.EventListener (just like the method above)
// (which assumes this.data to be a list of statuses...)
@Subscribe
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
for(Notification n:data){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}
}
}
}
for(Notification n:preloadedData){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
}
}
}
@Subscribe @Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){ public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow) if(!ev.accountID.equals(accountID) || ev.isUnfollow)

View File

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

View File

@@ -37,6 +37,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference; import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import org.joinmastodon.android.MainActivity; import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
@@ -85,8 +86,8 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private ArrayList<Item> items=new ArrayList<>(); private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem; private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem; private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem; private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
private ButtonItem defaultContentTypeButtonItem; private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
private String accountID; private String accountID;
private boolean needUpdateNotificationSettings; private boolean needUpdateNotificationSettings;
private boolean needAppRestart; private boolean needAppRestart;
@@ -189,9 +190,18 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.showInteractionCounts=i.checked; GlobalUserPreferences.showInteractionCounts=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
})); }));
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{ items.add(alwaysRevealSpoilersItem = new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked; GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
if (list.findViewHolderForAdapterPosition(items.indexOf(autoRevealSpoilersItem)) instanceof ButtonViewHolder bvh) bvh.rebind();
}));
items.add(autoRevealSpoilersItem = new ButtonItem(R.string.sk_settings_auto_reveal_equal_spoilers, R.drawable.ic_fluent_eye_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.settings_auto_reveal_spoiler);
popupMenu.setOnMenuItemClickListener(i -> onAutoRevealSpoilerClick(i, b));
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
onAutoRevealSpoilerChanged(b);
})); }));
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{ items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
GlobalUserPreferences.disableSwipe=i.checked; GlobalUserPreferences.disableSwipe=i.checked;
@@ -219,6 +229,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.confirmBeforeReblog=i.checked; GlobalUserPreferences.confirmBeforeReblog=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
})); }));
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
GlobalUserPreferences.allowRemoteLoading=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
items.add(new HeaderItem(R.string.sk_timelines)); items.add(new HeaderItem(R.string.sk_timelines));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{ items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
@@ -271,7 +286,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.collapseLongPosts=i.checked; GlobalUserPreferences.collapseLongPosts=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
})); }));
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{ items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_star_off_24_regular, GlobalUserPreferences.spectatorMode, i->{
GlobalUserPreferences.spectatorMode=i.checked; GlobalUserPreferences.spectatorMode=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
needAppRestart=true; needAppRestart=true;
@@ -526,6 +541,36 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
return true; return true;
} }
private boolean onAutoRevealSpoilerClick(MenuItem item, Button btn) {
int id = item.getItemId();
AutoRevealMode mode = AutoRevealMode.NEVER;
if (id == R.id.auto_reveal_threads) mode = AutoRevealMode.THREADS;
else if (id == R.id.auto_reveal_discussions) mode = AutoRevealMode.DISCUSSIONS;
GlobalUserPreferences.alwaysExpandContentWarnings = false;
GlobalUserPreferences.autoRevealEqualSpoilers = mode;
GlobalUserPreferences.save();
onAutoRevealSpoilerChanged(btn);
return true;
}
private void onAutoRevealSpoilerChanged(Button b) {
if (GlobalUserPreferences.alwaysExpandContentWarnings) {
b.setText(R.string.sk_settings_auto_reveal_anyone);
} else {
b.setText(switch(GlobalUserPreferences.autoRevealEqualSpoilers){
case THREADS -> R.string.sk_settings_auto_reveal_author;
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_anyone;
default -> R.string.sk_settings_auto_reveal_nobody;
});
if (alwaysRevealSpoilersItem.checked != GlobalUserPreferences.alwaysExpandContentWarnings) {
alwaysRevealSpoilersItem.checked = GlobalUserPreferences.alwaysExpandContentWarnings;
if (list.findViewHolderForAdapterPosition(items.indexOf(alwaysRevealSpoilersItem)) instanceof SwitchViewHolder svh) svh.rebind();
}
}
}
private void onTrueBlackThemeChanged(SwitchItem item){ private void onTrueBlackThemeChanged(SwitchItem item){
GlobalUserPreferences.trueBlackTheme=item.checked; GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
@@ -555,14 +600,14 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private boolean onContentTypeChanged(MenuItem item, Button btn){ private boolean onContentTypeChanged(MenuItem item, Button btn){
int id = item.getItemId(); int id = item.getItemId();
ContentType contentType = switch (id) {
case R.id.content_type_plain -> ContentType.PLAIN; ContentType contentType = null;
case R.id.content_type_html -> ContentType.HTML; if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
case R.id.content_type_markdown -> ContentType.MARKDOWN; else if (id == R.id.content_type_html) contentType = ContentType.HTML;
case R.id.content_type_bbcode -> ContentType.BBCODE; else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN; else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
default -> null; else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
};
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType); GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
GlobalUserPreferences.save(); GlobalUserPreferences.save();
btn.setText(getContentTypeString(contentType)); btn.setText(getContentTypeString(contentType));
@@ -835,7 +880,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
} }
private class SmallTextItem extends Item { private class SmallTextItem extends Item {
private String text; private final String text;
public SmallTextItem(@StringRes int text) {
this.text = getString(text);
}
public SmallTextItem(String text) { public SmallTextItem(String text) {
this.text = text; this.text = text;

View File

@@ -68,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
status.filterRevealed = true; status.filterRevealed = true;
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status)); args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)) if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId))); args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args); Nav.go(getActivity(), ThreadFragment.class, args);
@@ -164,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
protected void removeStatus(Status status){ protected void removeStatus(Status status){
data.remove(status); data.remove(status);
preloadedData.remove(status); preloadedData.remove(status);
int index=-1; int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
for(int i=0;i<displayItems.size();i++){ for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){ StatusDisplayItem item = displayItems.get(i);
if(status.id.equals(item.parentID)){
index=i; index=i;
break; break;
} }
if (item.parentID.equals(status.inReplyToId)) {
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
ancestorLastIndex = i;
}
} }
// did we find an ancestor that is also the status' neighbor?
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) {
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) {
StatusDisplayItem item = displayItems.get(i);
// update ancestor to have no descendant anymore
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
}
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
}
if(index==-1) if(index==-1)
return; return;
int lastIndex; int lastIndex;

View File

@@ -8,6 +8,8 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID; import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext; import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
@@ -37,6 +39,7 @@ import java.util.Collections;
import java.util.Deque; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -44,11 +47,12 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent { public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus, updatedStatus; protected Status mainStatus, updatedStatus;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>(); private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
private boolean initialAnimationFinished; protected boolean contextInitiallyRendered;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -101,22 +105,26 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
footer.hideCounts=true; footer.hideCounts=true;
} }
} }
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem); for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) { 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; return items;
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
refreshMainStatus(); if (refreshing) loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id) currentRequest=new GetStatusContext(mainStatus.id)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(StatusContext result){ public void onSuccess(StatusContext result){
if (getContext() == null) return; if (getContext() == null) return;
Map<String, Status> oldData = null;
if(refreshing){ if(refreshing){
oldData = new HashMap<>(data.size());
for (Status s : data) oldData.put(s.id, s);
data.clear(); data.clear();
ancestryMap.clear(); ancestryMap.clear();
displayItems.clear(); displayItems.clear();
@@ -148,27 +156,51 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
adapter.notifyItemRemoved(prependedCount); adapter.notifyItemRemoved(prependedCount);
count--; count--;
} }
for (Status s : data) {
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.filterRevealed = oldStatus.filterRevealed;
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText) &&
mainStatus.spoilerRevealed) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = true;
}
}
}
dataLoaded(); dataLoaded();
if(refreshing){ if(refreshing){
refreshDone(); refreshDone();
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
list.scrollToPosition(displayItems.size()-count); list.scrollToPosition(displayItems.size()-count);
// no animation is going to happen, so proceeding to apply right now
if (data.size() == 1) {
contextInitiallyRendered = true;
// for the case that the main status has already finished loading
maybeApplyMainStatus();
}
} }
}) })
.exec(accountID); .exec(accountID);
} }
private void refreshMainStatus() { private void loadMainStatus() {
new GetStatusByID(mainStatus.id) new GetStatusByID(mainStatus.id)
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Status status) { public void onSuccess(Status status) {
if (getContext() == null || status == null) return; if (getContext() == null || status == null) return;
updatedStatus = status; updatedStatus = status;
// only update main status if the initial animation is already finished. // for the case that the context has already loaded (and the animation has
// otherwise, the animator will call it in onAnimationFinished // already finished), falling back to applying it ourselves:
if (initialAnimationFinished || data.size() == 1) updateMainStatus(); maybeApplyMainStatus();
} }
@Override @Override
@@ -176,7 +208,13 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}).exec(accountID); }).exec(accountID);
} }
protected Object updateMainStatus() { 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 // returning fired event object to facilitate testing
Object event; Object event;
if (updatedStatus.editedAt != null && if (updatedStatus.editedAt != null &&
@@ -287,29 +325,78 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
showContent(); showContent();
if(!loaded) if(!loaded)
footerProgress.setVisibility(View.VISIBLE); footerProgress.setVisibility(View.VISIBLE);
list.setItemAnimator(new BetterItemAnimator() { list.setItemAnimator(new BetterItemAnimator() {
@Override @Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) { public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder); super.onAnimationFinished(viewHolder);
// in case someone else is about to call updateMainStatus faster... contextInitiallyRendered = true;
initialAnimationFinished = true; // for the case that both requests are already done (and thus won't apply it)
// ...if not (someone did fetch it but the animation wasn't finished yet), maybeApplyMainStatus();
// call it now
if (updatedStatus != null) updateMainStatus();
} }
}); });
} }
protected void onStatusCreated(StatusCreatedEvent ev){ protected void onStatusCreated(StatusCreatedEvent ev){
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){ if (ev.status.inReplyToId == null) return;
data.add(ev.status); Status repliedToStatus = getStatusByID(ev.status.inReplyToId);
onAppendItems(Collections.singletonList(ev.status)); if (repliedToStatus == null) return;
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
int nextDisplayItemsIndex = -1, indexOfPreviousDisplayItem = -1;
for (int i = 0; i < displayItems.size(); i++) {
StatusDisplayItem item = displayItems.get(i);
if (repliedToStatus.id.equals(item.parentID)) {
// saving the replied-to status' display items index to eventually reach the last one
indexOfPreviousDisplayItem = i;
item.hasDescendantNeighbor = true;
} else if (indexOfPreviousDisplayItem >= 0 && nextDisplayItemsIndex == -1) {
// previous display item was the replied-to status' display items
nextDisplayItemsIndex = i;
// nothing left to do if there's no other reply to that status
if (ancestry.descendantNeighbor == null) break;
}
if (ancestry.descendantNeighbor != null && item.parentID.equals(ancestry.descendantNeighbor.id)) {
// existing reply shall no longer have the replied-to status as its neighbor
item.hasAncestoringNeighbor = false;
}
} }
// fall back to inserting the item at the end
nextDisplayItemsIndex = nextDisplayItemsIndex >= 0 ? nextDisplayItemsIndex : displayItems.size();
int nextDataIndex = data.indexOf(repliedToStatus) + 1;
// if replied-to status already has another reply...
if (ancestry.descendantNeighbor != null) {
// update the reply's ancestry to remove its ancestoring neighbor (as we did above)
ancestryMap.get(ancestry.descendantNeighbor.id).ancestoringNeighbor = null;
// make sure the existing reply has a reply line
if (nextDataIndex < data.size() &&
!(displayItems.get(nextDisplayItemsIndex) instanceof ReblogOrReplyLineStatusDisplayItem)) {
Status nextStatus = data.get(nextDataIndex);
if (!nextStatus.account.id.equals(repliedToStatus.account.id)) {
// create reply line manually since we're not building that status' items
displayItems.add(nextDisplayItemsIndex, StatusDisplayItem.buildReplyLine(
this, nextStatus, accountID, nextStatus, repliedToStatus.account, false
));
}
}
}
// update replied-to status' ancestry
ancestry.descendantNeighbor = ev.status;
// add ancestry for newly created status before building its display items
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus));
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status));
data.add(nextDataIndex, ev.status);
adapter.notifyDataSetChanged();
} }
@Override @Override
public boolean isItemEnabled(String id){ public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id); return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
} }
@Override @Override
@@ -357,4 +444,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor); 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.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.parceler.Parcels; import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{ import java.util.Optional;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
protected Account account; protected Account account;
protected String initialSubtitle = "";
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount")); account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
if (getArguments().containsKey("remoteAccount")) {
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
}
setTitle("@"+account.acct); setTitle("@"+account.acct);
} }
@@ -22,4 +33,36 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
? "/users/" + account.id ? "/users/" + account.id
: '@' + account.acct).build(); : '@' + 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; package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.app.assist.AssistContent; import android.app.assist.AssistContent;
import android.content.Intent; import android.content.Intent;
@@ -47,6 +48,7 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.APIRequest;
@@ -243,16 +245,19 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu); UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
} }
@SuppressLint("SetTextI18n")
@Override @Override
public void onBind(AccountItem item){ public void onBind(AccountItem item){
name.setText(item.parsedName); name.setText(item.parsedName);
username.setText("@"+item.account.acct); username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship(); bindRelationship();
} }
public void bindRelationship(){ public void bindRelationship(){
Relationship rel=relationships.get(item.account.id); Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){ if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE); button.setVisibility(View.GONE);
}else{ }else{
button.setVisibility(View.VISIBLE); button.setVisibility(View.VISIBLE);
@@ -282,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onClick(){ public void onClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account)); if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args); Nav.go(getActivity(), ProfileFragment.class, args);
} }
@@ -423,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
emojiHelper=new CustomEmojiHelper(); emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis)); emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
} }
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.parceler.Parcels; import org.parceler.Parcels;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{ import java.util.Optional;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
protected Status status; protected Status status;
protected abstract void updateTitle(Status status);
protected MastodonAPIRequest<Status> loadRemoteInfo() {
String[] parts = status.url.split("/");
if (parts.length == 0) return null;
return new GetStatusByID(parts[parts.length - 1]);
}
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -17,7 +32,7 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
@Override @Override
protected boolean hasSubtitle(){ protected boolean hasSubtitle(){
return false; return remoteRequestFailed;
} }
@Override @Override
@@ -28,4 +43,35 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
: '@' + status.account.acct + '/' + status.id) : '@' + status.account.acct + '/' + status.id)
.build(); .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 android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses; import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -17,7 +16,7 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop { public class DiscoverPostsFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS); private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
@Override @Override
@@ -39,11 +38,6 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(contentWrap);
} }
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override @Override
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return Filter.FilterContext.PUBLIC;

View File

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

View File

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

View File

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

View File

@@ -148,6 +148,10 @@ public class Instance extends BaseModel{
return pleroma != null; return pleroma != null;
} }
public boolean isPixelfed() {
return version.contains("compatible; Pixelfed");
}
public boolean hasFeature(Feature feature) { public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma) Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata) .map(p -> p.metadata)

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ public enum StatusPrivacy{
@SerializedName("local") @SerializedName("local")
LOCAL(4); // akkoma LOCAL(4); // akkoma
private int privacy; private final int privacy;
StatusPrivacy(int privacy) { StatusPrivacy(int privacy) {
this.privacy = privacy; this.privacy = privacy;
@@ -24,6 +24,13 @@ public enum StatusPrivacy{
return privacy > other.getPrivacy(); return privacy > other.getPrivacy();
} }
public boolean isReblogPermitted(boolean isOwnStatus){
return (this == StatusPrivacy.PUBLIC ||
this == StatusPrivacy.UNLISTED ||
this == StatusPrivacy.LOCAL ||
(this == StatusPrivacy.PRIVATE && isOwnStatus));
}
public int getPrivacy() { public int getPrivacy() {
return privacy; return privacy;
} }

View File

@@ -75,8 +75,7 @@ public class AccountSwitcherSheet extends BottomSheet{
this.fragment=fragment; this.fragment=fragment;
this.externalShare = externalShare; this.externalShare = externalShare;
this.openInApp = openInApp; this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList()); accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity); list=new UsableRecyclerView(activity);

View File

@@ -33,12 +33,14 @@ import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status; public final Status status;
public final String accountID;
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){ public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Status status){
super(parentID, parentFragment); super(parentID, parentFragment);
this.status=status; this.status=status;
this.accountID=accountID;
} }
@Override @Override
@@ -72,7 +74,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public void onBind(ExtendedFooterStatusDisplayItem item){ public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status; Status s=item.status;
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount)); favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int)(s.reblogsCount%1000), s.reblogsCount)); reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
reblogs.setVisibility(s.isReblogPermitted(item.accountID) ? View.VISIBLE : View.GONE);
if(s.editedAt!=null){ if(s.editedAt!=null){
editHistory.setVisibility(View.VISIBLE); editHistory.setVisibility(View.VISIBLE);
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt)); editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));

View File

@@ -5,7 +5,6 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -17,7 +16,6 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
@@ -125,9 +123,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void onBind(FooterStatusDisplayItem item){ public void onBind(FooterStatusDisplayItem item){
bindButton(replies, item.status.repliesCount); bindText(replies, item.status.repliesCount);
bindButton(boosts, item.status.reblogsCount); bindText(boosts, item.status.reblogsCount);
bindButton(favorites, item.status.favouritesCount); bindText(favorites, item.status.favouritesCount);
// in thread view, direct descendant posts display one direct reply to themselves, // in thread view, direct descendant posts display one direct reply to themselves,
// hence in that case displaying whether there is another reply // hence in that case displaying whether there is another reply
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1; int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
@@ -135,8 +133,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boost.setSelected(item.status.reblogged); boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited); favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked); bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL boost.setEnabled(item.status.isReblogPermitted(item.accountID));
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
int nextPos = getAbsoluteAdapterPosition() + 1; int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos && boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
@@ -146,12 +143,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams(); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0); condenseBottom ? V.dp(-5) : 0);
itemView.requestLayout(); itemView.requestLayout();
} }
private void bindButton(TextView btn, long count){ private void bindText(TextView btn, long count){
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){ if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
btn.setText(UiUtils.abbreviateNumber(count)); btn.setText(UiUtils.abbreviateNumber(count));
btn.setCompoundDrawablePadding(V.dp(8)); btn.setCompoundDrawablePadding(V.dp(8));
@@ -215,13 +212,13 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
onBoostLongClick(v); onBoostLongClick(v);
return; return;
} }
boosts.setSelected(!item.status.reblogged); boost.setSelected(!item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r)); AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
} }
private void boostConsumer(View v, Status r) { private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn); v.startAnimation(opacityIn);
bindButton(boosts, r.reblogsCount); bindText(boosts, r.reblogsCount);
} }
private boolean onBoostLongClick(View v){ private boolean onBoostLongClick(View v){
@@ -307,10 +304,10 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private void onFavoriteClick(View v){ private void onFavoriteClick(View v){
favorites.setSelected(!item.status.favourited); favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{ AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn); v.startAnimation(opacityIn);
bindButton(favorites, r.favouritesCount); bindText(favorites, r.favouritesCount);
}); });
} }

View File

@@ -196,7 +196,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putBoolean("navigateToStatus", true); args.putBoolean("navigateToStatus", true);
} }
} }
if(!redraft && TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){ boolean isPixelfed = item.parentFragment.isInstancePixelfed();
boolean textEmpty = TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText);
if(!redraft && (isPixelfed || textEmpty)){
// pixelfed doesn't support /statuses/:id/source :/
if (isPixelfed) {
args.putString("sourceText", HtmlParser.text(item.status.content));
args.putString("sourceSpoiler", item.status.spoilerText);
}
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else if(item.scheduledStatus!=null){ }else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text); args.putString("sourceText", item.status.text);

View File

@@ -105,6 +105,21 @@ public abstract class StatusDisplayItem{
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext); return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
} }
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
String parentID = parent.getID();
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
: fragment.getString(R.string.in_reply_to, account.displayName);
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: fragment.getString(R.string.in_reply_to, account.displayName);
return new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){ public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
String parentID=parentObject.getID(); String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>(); ArrayList<StatusDisplayItem> items=new ArrayList<>();
@@ -120,17 +135,7 @@ public abstract class StatusDisplayItem{
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){ if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
Account account = knownAccounts.get(statusForContent.inReplyToAccountId); Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
String text = threadReply ? fragment.getString(R.string.sk_show_thread) replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
: account == null ? fragment.getString(R.string.sk_in_reply)
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
: fragment.getString(R.string.in_reply_to, account.displayName);
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: fragment.getString(R.string.in_reply_to, account.displayName);
replyLine = new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
);
} }
if(status.reblog!=null){ if(status.reblog!=null){

View File

@@ -241,7 +241,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos && boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem; item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
int bottomPadding = (translateVisible && nextIsFooter) ? 0 int bottomPadding = (translateVisible && nextIsFooter) ? 0
: nextIsFooter ? V.dp(8) : nextIsFooter ? V.dp(6)
: V.dp(12); : V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding); itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);

View File

@@ -4,8 +4,7 @@ import android.graphics.Canvas;
import android.graphics.CornerPathEffect; import android.graphics.CornerPathEffect;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Rect; import android.os.Build;
import android.graphics.RectF;
import android.text.Layout; import android.text.Layout;
import android.text.Spanned; import android.text.Spanned;
import android.view.GestureDetector; import android.view.GestureDetector;
@@ -33,7 +32,8 @@ public class ClickableLinksDelegate {
hlPaint=new Paint(); hlPaint=new Paint();
hlPaint.setAntiAlias(true); hlPaint.setAntiAlias(true);
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3))); hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light)); hlPaint.setStyle(Paint.Style.FILL_AND_STROKE);
hlPaint.setStrokeWidth(V.dp(4));
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler()); gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
} }
@@ -58,7 +58,7 @@ public class ClickableLinksDelegate {
public void onDraw(Canvas canvas){ public void onDraw(Canvas canvas){
if(hlPath!=null){ if(hlPath!=null){
canvas.save(); canvas.save();
canvas.translate(0, view.getPaddingTop()); canvas.translate(view.getTotalPaddingLeft(), view.getTotalPaddingTop());
canvas.drawPath(hlPath, hlPaint); canvas.drawPath(hlPath, hlPaint);
canvas.restore(); canvas.restore();
} }
@@ -73,59 +73,29 @@ public class ClickableLinksDelegate {
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override @Override
public boolean onDown(@NonNull MotionEvent event) { public boolean onDown(@NonNull MotionEvent event) {
int line=-1; int padLeft=view.getTotalPaddingLeft(), padRight=view.getTotalPaddingRight(), padTop=view.getTotalPaddingTop(), padBottom=view.getTotalPaddingBottom();
Rect rect=new Rect(); float x=event.getX(), y=event.getY();
Layout l=view.getLayout(); if(x<padLeft || y<padTop || x>view.getWidth()-padRight || y>view.getHeight()-padBottom)
for(int i=0;i<l.getLineCount();i++){
view.getLineBounds(i, rect);
if(rect.contains((int)event.getX(), (int)event.getY())){
line=i;
break;
}
}
if(line==-1){
return false; return false;
} x-=padLeft;
y-=padTop;
Layout l=view.getLayout();
int line=l.getLineForVertical(Math.round(y));
int position=l.getOffsetForHorizontal(line, x);
CharSequence text=view.getText(); CharSequence text=view.getText();
if(text instanceof Spanned s){ if(text instanceof Spanned s){
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class); LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
if(spans.length>0){ for(LinkSpan span:spans){
for(LinkSpan span:spans){ int start=s.getSpanStart(span);
int start=s.getSpanStart(span); int end=s.getSpanEnd(span);
int end=s.getSpanEnd(span); if(start<=position && end>position){
int lstart=l.getLineForOffset(start); selectedSpan=span;
int lend=l.getLineForOffset(end); hlPath=new Path();
if(line>=lstart && line<=lend){ l.getSelectionPath(start, end, hlPath);
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){ hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
continue; view.invalidate();
} return true;
if(line==lend && event.getX()-view.getPaddingLeft()>l.getPrimaryHorizontal(end)){
continue;
}
hlPath=new Path();
selectedSpan=span;
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
Rect bounds=new Rect();
l.getLineBounds(j, bounds);
//bounds.left+=view.getPaddingLeft();
if(j==lstart){
bounds.left=Math.round(l.getPrimaryHorizontal(start));
}
if(j==lend){
bounds.right=Math.round(l.getPrimaryHorizontal(end));
}else{
CharSequence lineChars=view.getText().subSequence(l.getLineStart(j), l.getLineEnd(j));
bounds.right=Math.round(view.getPaint().measureText(lineChars.toString()))/*+view.getPaddingRight()*/;
}
bounds.inset(V.dp(-2), V.dp(-2));
hlPath.addRect(new RectF(bounds), Path.Direction.CW);
}
hlPath.offset(view.getPaddingLeft(), 0);
view.invalidate();
return true;
}
} }
} }
} }

View File

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

View File

@@ -56,6 +56,8 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
@@ -1051,50 +1053,46 @@ public class UiUtils {
}, null); }, null);
} }
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) { public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results -> return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty() !results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
); );
} }
public static void lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) { public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results -> return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty() !results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
); );
} }
public static <T extends Searchable> void lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) { public static <T extends Searchable> Optional<MastodonAPIRequest<SearchResults>> lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) { if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
resultConsumer.accept(query); resultConsumer.accept(query);
return; return Optional.empty();
} }
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() { return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override @Override
public void onSuccess(SearchResults results) { public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results); Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get()); if (result.isPresent()) resultConsumer.accept(result.get());
else { else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null); resultConsumer.accept(null);
} }
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
error.showToast(context); error.showToast(context);
} }
}) })
.wrapProgress((Activity) context, R.string.loading, true, .wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d)) d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID); .exec(targetAccountID));
} }
public static void openURL(Context context, String accountID, String url) { public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) { if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID))); dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else { } else {
@@ -1109,11 +1107,35 @@ public class UiUtils {
} }
} }
private static Bundle bundleError(String error) {
Bundle args = new Bundle();
args.putString("error", error);
return args;
}
private static Bundle bundleError(ErrorResponse error) {
Bundle args = new Bundle();
if (error instanceof MastodonErrorResponse e) {
args.putString("error", e.error);
args.putInt("httpStatus", e.httpStatus);
}
return args;
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) { public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, launchBrowser, (clazz, args) -> { lookupURL(context, accountID, url, (clazz, args) -> {
if (clazz == null) return; 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); 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) { public static boolean acctMatches(String accountID, String acct, String queriedUsername, @Nullable String queriedDomain) {
@@ -1136,9 +1158,17 @@ public class UiUtils {
} }
} }
public static void lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) { 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("")); String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true) return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(SearchResults results) { public void onSuccess(SearchResults results) {
@@ -1152,23 +1182,22 @@ public class UiUtils {
go.accept(ProfileFragment.class, args); go.accept(ProfileFragment.class, args);
return; return;
} }
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
go.accept(null, null);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
go.accept(null, bundleError(error));
} }
}).exec(accountID); }).exec(accountID);
} }
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) { public static Optional<MastodonAPIRequest<?>> lookupURL(Context context, String accountID, String url, BiConsumer<Class<? extends Fragment>, Bundle> go) {
Uri uri = Uri.parse(url); Uri uri = Uri.parse(url);
List<String> path = uri.getPathSegments(); List<String> path = uri.getPathSegments();
if (accountID != null && "https".equals(uri.getScheme())) { if (accountID != null && "https".equals(uri.getScheme())) {
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) { if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1)) return Optional.of(new GetStatusByID(path.get(1))
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Status result) { public void onSuccess(Status result) {
@@ -1180,17 +1209,12 @@ public class UiUtils {
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
error.showToast(context); go.accept(null, bundleError(error));
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
} }
}) })
.wrapProgress((Activity) context, R.string.loading, true, .exec(accountID));
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} else if (looksLikeFediverseUrl(url)) { } else if (looksLikeFediverseUrl(url)) {
new GetSearchResults(url, null, true) return Optional.of(new GetSearchResults(url, null, true)
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(SearchResults results) { public void onSuccess(SearchResults results) {
@@ -1208,26 +1232,19 @@ public class UiUtils {
go.accept(ProfileFragment.class, args); go.accept(ProfileFragment.class, args);
return; return;
} }
if (launchBrowser) launchWebBrowser(context, url); go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
error.showToast(context); go.accept(null, bundleError(error));
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
} }
}) })
.wrapProgress((Activity) context, R.string.loading, true, .exec(accountID));
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} }
} }
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null); go.accept(null, null);
return Optional.empty();
} }
public static void copyText(View v, String text) { public static void copyText(View v, String text) {
@@ -1395,7 +1412,7 @@ public class UiUtils {
extras.putString("account", accountID); extras.putString("account", accountID);
if (n.status!=null) { if (n.status!=null) {
Status status=n.status; Status status=n.status;
extras.putParcelable("status", Parcels.wrap(status)); extras.putParcelable("status", Parcels.wrap(status.clone()));
Nav.go((Activity) context, ThreadFragment.class, extras); Nav.go((Activity) context, ThreadFragment.class, extras);
} else if (n.report != null) { } else if (n.report != null) {
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain; String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;

File diff suppressed because one or more lines are too long

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: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"> <item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval"> <shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/> <stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/> <solid android:color="?android:colorAccent"/>
</shape> </shape>
</item> </item>

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,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: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"> <item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval"> <shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/> <stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/> <solid android:color="?android:colorAccent"/>
</shape> </shape>
</item> </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: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"> <item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval"> <shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/> <stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/> <solid android:color="?android:colorAccent"/>
</shape> </shape>
</item> </item>

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

@@ -292,4 +292,12 @@
<string name="sk_instance_info_unavailable">Informationen zur Instanz momentan nicht verfügbar</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_open_in_app">In App öffnen</string>
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string> <string name="sk_settings_content_types_explanation">Dadurch 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> </resources>

View File

@@ -286,10 +286,19 @@
<string name="sk_settings_default_content_type">Contenido por defecto</string> <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_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_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 gente en tu servidor de Akkoma.</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_timeline_bubble">Burbuja</string>
<string name="sk_instance_info_unavailable">Información de la instancia temporalmente no disponible</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_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_open_in_app">Abrir en la app</string>
<string name="sk_external_share_title">Compartir con una cuenta</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> </resources>

View File

@@ -290,7 +290,12 @@
<string name="sk_open_in_app">Ouvrir dans l\'application</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_title">Partager avec le compte</string>
<string name="sk_external_share_or_open_title">Partager ou ouvrir 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 publications les plus récentes des personnes présentes dans la bulle de votre serveur Akkoma.</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_timeline_bubble">Bulle</string>
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</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> </resources>

View File

@@ -289,8 +289,13 @@
<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_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_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string> <string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yamg paling terkini oleh orang-orang dalam gelembung server Akkoma Anda.</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_timeline_bubble">Gelembung</string>
<string name="sk_instance_info_unavailable">Info server sementara tidak tersedia</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_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> </resources>

View File

@@ -274,4 +274,26 @@
<string name="sk_compact_reblog_reply_line">Linea boost/risposta compatta</string> <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_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_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> </resources>

View File

@@ -10,11 +10,13 @@
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="preparing_auth">Förbereder för autentisering…</string> <string name="preparing_auth">Förbereder för autentisering…</string>
<string name="finishing_auth">Slutfö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="in_reply_to">Som svar på %s</string>
<string name="notifications">Notiser</string> <string name="notifications">Notiser</string>
<string name="user_followed_you">följde dig</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_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="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="poll_ended">omröstning avslutad</string>
<string name="time_seconds">%ds</string> <string name="time_seconds">%ds</string>
<string name="time_minutes">%dm</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="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_user">Avfölj %s</string>
<string name="unfollow">Avfölj</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="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_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> <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="hide_content">Dölj innehåll</string>
<string name="new_post">Nytt inlägg</string> <string name="new_post">Nytt inlägg</string>
<string name="button_reply">Svara</string> <string name="button_reply">Svara</string>
<string name="button_reblog">Boosta</string>
<string name="button_favorite">Favoritmarkera</string> <string name="button_favorite">Favoritmarkera</string>
<string name="button_share">Dela</string> <string name="button_share">Dela</string>
<string name="media_no_description">Media utan beskrivning</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. --> <!-- %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_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="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="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="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_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> </resources>

View File

@@ -6,17 +6,17 @@
<string name="next">Sonraki</string> <string name="next">Sonraki</string>
<string name="loading_instance">Sunucu bilgisi alınıyor…</string> <string name="loading_instance">Sunucu bilgisi alınıyor…</string>
<string name="error">Hata</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="ok">Tamam</string>
<string name="preparing_auth">Kimlik doğrulama için hazırlanıyor…</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="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="in_reply_to">%s için yanıt</string>
<string name="notifications">Bildirimler</string> <string name="notifications">Bildirimler</string>
<string name="user_followed_you">sizi takip etti</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_sent_follow_request">sana bir takip isteği gönderdi</string>
<string name="user_favorited">gönderinizi favorilerine ekledi</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="poll_ended">oylama sona erdi</string>
<string name="time_seconds">%ds</string> <string name="time_seconds">%ds</string>
<string name="time_minutes">%ddk</string> <string name="time_minutes">%ddk</string>
@@ -123,7 +123,7 @@
<string name="delete">Sil</string> <string name="delete">Sil</string>
<string name="confirm_delete_title">Gönderiyi 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="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="notification_channel_audio_player">Ses çal</string>
<string name="play">Oynat</string> <string name="play">Oynat</string>
<string name="pause">Durdur</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_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">Hoşuma gitmiyor</string>
<string name="report_reason_personal_subtitle">Görmek isteyeceğin bir şey değil</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_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">Sunucu kurallarını ihlal ediyor</string>
<string name="report_reason_violation_subtitle">Belirli kuralları çiğnediğinin farkındasınız</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="report_sent_subtitle">Biz bunu incelerken siz %s karşı önlem alabilirsiniz.</string>
<string name="unfollow_user">Takipten çık %s</string> <string name="unfollow_user">Takipten çık %s</string>
<string name="unfollow">Takipten çık</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="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_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> <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> <string name="confirm_email_title">E-posta Kutunuzu Kontrol Edin</string>
<!-- %s is the email address --> <!-- %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_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="resend">Yeniden gönder</string>
<string name="open_email_app">E-posta uygulamasını</string> <string name="open_email_app">Eposta uygulamasını</string>
<string name="resent_email">Onay e-postası gönderildi</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="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="content_warning">İçerik Uyarısı</string>
<string name="add_image_description">Resim açıklaması ekle…</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_gif">Animasyonlu avatarları ve emojileri oynat</string>
<string name="settings_custom_tabs">Uygulama içi tarayıcıyı kullan</string> <string name="settings_custom_tabs">Uygulama içi tarayıcıyı kullan</string>
<string name="settings_notifications">Bildirimler</string> <string name="settings_notifications">Bildirimler</string>
<string name="notify_me_when">Beni şu durumda bilgilendir: </string> <string name="notify_me_when">Beni şu durumda bilgilendir</string>
<string name="notify_anyone">Herhangi biri</string> <string name="notify_anyone">Herhangibiri</string>
<string name="notify_follower">Bir takipçim</string> <string name="notify_follower">Bir takipçim</string>
<string name="notify_followed">Takip ettiğim biri</string> <string name="notify_followed">Takip ettiğim biri</string>
<string name="notify_none">Bilgilendirme</string> <string name="notify_none">Bilgilendirme</string>
<string name="notify_favorites">Gönderimi favorilerine eklediğinde</string> <string name="notify_favorites">Gönderimi favorilerine eklediğinde</string>
<string name="notify_follow">Beni takip ettiğ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="notify_mention">Benden bahsettiğinde</string>
<string name="settings_boring">Sıkıcı bölge</string> <string name="settings_boring">Sıkıcı bölge</string>
<string name="settings_account">Hesap ayarları</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_tos">Kullanım Şartları</string>
<string name="settings_privacy_policy">Gizlilik Politikası</string> <string name="settings_privacy_policy">Gizlilik Politikası</string>
<string name="settings_spicy">Tehlikeli bölge</string> <string name="settings_spicy">Tehlikeli bölge</string>
@@ -271,7 +271,7 @@
<string name="hide_content">İçeriği gizle</string> <string name="hide_content">İçeriği gizle</string>
<string name="new_post">Yeni gönderi</string> <string name="new_post">Yeni gönderi</string>
<string name="button_reply">Cevapla</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_favorite">Favorile</string>
<string name="button_share">Paylaş</string> <string name="button_share">Paylaş</string>
<string name="media_no_description">ıklamasız medya</string> <string name="media_no_description">ıklamasız medya</string>
@@ -282,7 +282,7 @@
<string name="home_timeline">Anasayfa</string> <string name="home_timeline">Anasayfa</string>
<string name="my_profile">Profilim</string> <string name="my_profile">Profilim</string>
<string name="media_viewer">Medya görüntüleyici</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="unfollowed_user">%s takip edilmedi</string>
<string name="followed_user">%s kişisini takip ediyorsunuz</string> <string name="followed_user">%s kişisini takip ediyorsunuz</string>
<string name="following_user_requested">%s takip isteği gönderdi</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="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="dismiss">Yoksay</string>
<string name="see_new_posts">Yeni gönderileri gör</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="load_missing_posts">Daha fazlası</string>
<string name="follow_back">Geri Takip Et</string> <string name="follow_back">Takip edeni Takip Et</string>
<string name="button_follow_pending">Bekliyor</string> <string name="button_follow_pending">Bekliyor</string>
<string name="follows_you">Seni takip ediyor</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="current_account">Kullanılan hesap</string>
<string name="log_out_account">%s oturumunu kapat</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 --> <!-- 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> <item quantity="other">%,d favori</item>
</plurals> </plurals>
<plurals name="x_reblogs"> <plurals name="x_reblogs">
<item quantity="one">%,d yineleme</item> <item quantity="one">%,d paylaşma</item>
<item quantity="other">%,d yineleme</item> <item quantity="other">%,d paylaşma</item>
</plurals> </plurals>
<string name="timestamp_via_app">%1$s tarihinde %2$s uygulamasıyla</string> <string name="timestamp_via_app">%1$s tarihinde %2$s uygulamasıyla</string>
<string name="time_now">şimdi</string> <string name="time_now">şimdi</string>
@@ -361,7 +361,7 @@
<string name="edit_media_added">Medya eklendi</string> <string name="edit_media_added">Medya eklendi</string>
<string name="edit_media_removed">Medya kaldırıldı</string> <string name="edit_media_removed">Medya kaldırıldı</string>
<string name="edit_media_reordered">Medya düzenlendi</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_marked_not_sensitive">Hassas değil olarak işaretlendi</string>
<string name="edit_multiple_changed">Gönderi düzenlendi</string> <string name="edit_multiple_changed">Gönderi düzenlendi</string>
<string name="edit">Düzenle</string> <string name="edit">Düzenle</string>
@@ -373,16 +373,16 @@
<string name="file_size_gb">%.2f GB</string> <string name="file_size_gb">%.2f GB</string>
<string name="file_upload_progress">%2$s dosyadan %1$s</string> <string name="file_upload_progress">%2$s dosyadan %1$s</string>
<string name="file_upload_time_remaining">%s kaldı</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> <string name="upload_processing">İşleniyor…</string>
<!-- %s is version like 1.2.3 --> <!-- %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 --> <!-- %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 --> <!-- %s is file size -->
<string name="download_update">İndir (%s)</string> <string name="download_update">İndir (%s)</string>
<string name="install_update">Kur</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="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="i_agree">Kabul ediyorum</string>
<string name="empty_list">Bu liste boş</string> <string name="empty_list">Bu liste boş</string>
@@ -392,9 +392,9 @@
<string name="remove_bookmark">Yer İmi Kaldır</string> <string name="remove_bookmark">Yer İmi Kaldır</string>
<string name="bookmarks">Yer İmleri</string> <string name="bookmarks">Yer İmleri</string>
<string name="your_favorites">Favorilerin</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="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="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_any_language">Herhangi Bir Dil</string>
<string name="server_filter_instant_signup">Koşulsuz Kayıt</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="server_rules_disagree">Reddet</string>
<string name="privacy_policy_explanation">Kısacası: Hiçbir veri işlemiyor ya da toplamıyoruz.</string> <string name="privacy_policy_explanation">Kısacası: Hiçbir veri işlemiyor ya da toplamıyoruz.</string>
<!-- %s is server domain --> <!-- %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> <string name="profile_bio">Hakkımda</string>
<!-- Shown in a progress dialog when you tap "follow all" --> <!-- Shown in a progress dialog when you tap "follow all" -->
<string name="sending_follows">Hesaplar takip ediliyor…</string> <string name="sending_follows">Hesaplar takip ediliyor…</string>
@@ -437,12 +437,12 @@
<string name="verified_link">Onaylanmış bağlantı</string> <string name="verified_link">Onaylanmış bağlantı</string>
<string name="show">Göster</string> <string name="show">Göster</string>
<string name="hide">Gizle</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="pick_server">Başka sunucu seç</string>
<string name="signup_or_login">veya</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_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="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> </resources>

View File

@@ -288,8 +288,13 @@
<string name="sk_settings_content_types_explanation">Дозволяє налаштувати тип вмісту, наприклад, Markdown, під час написання допису. Зауважте, що не всі сервери підтримують цю функцію.</string> <string name="sk_settings_content_types_explanation">Дозволяє налаштувати тип вмісту, наприклад, Markdown, під час написання допису. Зауважте, що не всі сервери підтримують цю функцію.</string>
<string name="sk_open_in_app">Відкрити у застосунку</string> <string name="sk_open_in_app">Відкрити у застосунку</string>
<string name="sk_external_share_title">Поділитися через обліковий запис</string> <string name="sk_external_share_title">Поділитися через обліковий запис</string>
<string name="sk_bubble_timeline_info_banner">Це найновіші дописи людей у бульбашці вашого сервера Akkoma.</string> <string name="sk_bubble_timeline_info_banner">Це найновіші дописи з мережі керованої адміністраторами вашого сервера.</string>
<string name="sk_timeline_bubble">Бульбашка</string> <string name="sk_timeline_bubble">Бульбашка</string>
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string> <string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</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> </resources>

View File

@@ -293,4 +293,12 @@
<string name="sk_open_in_app_failed">Could not 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_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open 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> </resources>