Compare commits

...

46 Commits

Author SHA1 Message Date
sk
56a93288c4 reimplement thread ancestry 2023-06-02 19:05:18 +02:00
sk
02e3421f98 fix null pointer exception 2023-06-02 19:03:29 +02:00
sk
1ce49c68fe bump version 2023-06-02 01:45:55 +02:00
sk
d37e880993 don't close sheet after logging out 2023-06-02 01:45:30 +02:00
sk
6fdb81a01f Merge remote-tracking branch 'upstream/l10n_master' 2023-06-02 01:37:14 +02:00
ihor_ck
f9d6827572 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (292 of 292 strings)

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

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

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-01 23:36:24 +00:00
sk
bfcff1e19f fix null pointer exception
closes sk22#539
2023-06-02 01:34:31 +02:00
sk
f373e7df3e put code in method, add todo 2023-06-02 01:16:21 +02:00
sk
3985de5b14 visually connect descendant replies in threads
closes sk22#256
closes sk22#510
2023-06-02 00:55:42 +02:00
sk
e175a721d4 remove additional padding with translate button 2023-06-02 00:17:50 +02:00
sk
d9784ebc31 use /about web uri for akkoma 2023-06-01 19:28:46 +02:00
sk
f241092277 use isInstanceAkkoma() 2023-06-01 19:22:01 +02:00
sk
0702703d78 normalize instance uri 2023-06-01 19:13:03 +02:00
sk
2c4504bad3 fix current fragment detection 2023-06-01 19:12:50 +02:00
Eugen Rochko
07ca5a8b77 New translations strings.xml (Bengali) 2023-06-01 19:07:10 +02:00
sk
798a43906f denser account switcher
this one's for @experiencersinternational
2023-06-01 18:46:53 +02:00
sk
41cb0f2e09 implement assist url in instance rules 2023-06-01 18:38:45 +02:00
sk
e12c0fb81f bump version 2023-06-01 18:10:30 +02:00
Oliebol
ac39f119e2 Translated using Weblate (Dutch)
Currently translated at 88.4% (253 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-06-01 16:08:59 +00:00
Espasant3
016faf3df0 Translated using Weblate (Galician)
Currently translated at 99.6% (285 of 286 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-06-01 16:08:59 +00:00
sk
b2d6879282 reimplement assist content 2023-06-01 18:02:33 +02:00
Eugen Rochko
6926a212f4 New translations strings.xml (Bengali) 2023-06-01 17:46:40 +02:00
sk
89afc05d5c Merge branch 'main' into pr/FineFindus/530 2023-06-01 16:32:04 +02:00
Eugen Rochko
936f39161b New translations strings.xml (German) 2023-05-31 20:11:48 +02:00
sk
ee20ee0722 include mentions in reply from notification
closes sk22#536
2023-05-31 12:42:48 +02:00
sk
02f9f8c8ea always update active account, but not others 2023-05-31 12:23:04 +02:00
sk
de3a252884 not as huge share sheet heading 2023-05-31 10:12:12 +02:00
sk
5e7a00de3e fix crash when logging out active account 2023-05-31 10:05:31 +02:00
sk
2858aeb55e only set last account id if creating new activity 2023-05-31 09:45:24 +02:00
sk
357104efa9 set checked on basis of fragment's account id
closes sk22#538
2023-05-31 09:42:29 +02:00
sk
bb8027c7ef open externally opened content in main activity
closes sk22#533
2023-05-31 01:44:00 +02:00
sk
f9dd787009 fix rules crashing the app
closes sk22#535
2023-05-31 00:19:38 +02:00
sk
e005731ba6 theming support for m3 colors 2023-05-30 23:52:26 +02:00
sk
18ae3f4f61 Merge branch 'pr/FineFindus/531'
Co-authored-by: FineFindus <63370021+finefindus@users.noreply.github.com>
2023-05-30 22:46:08 +02:00
sk
10dfe0327e use new account switcher 2023-05-30 22:42:56 +02:00
Jacoco
1d1e921137 Fix GoToSocial crash when markers are null (#529) 2023-05-30 19:07:34 +02:00
sk
0985a4c968 getInstance returns optional 2023-05-30 18:57:17 +02:00
sk
8df589c103 safer file writing 2023-05-30 18:56:55 +02:00
FineFindus
71b6b2f451 feat(share): add option open URL 2023-05-30 16:33:09 +02:00
FineFindus
d85940ded8 fix: re-add removed imports 2023-05-30 16:28:04 +02:00
LucasGGamerM
e9e491c0b0 feat: redesign account picker sheet 2023-05-30 16:25:04 +02:00
FineFindus
c73562fb75 feat(external-share): use AccountSwitcherSheet 2023-05-30 16:24:43 +02:00
FineFindus
3feacb59c8 feat(external-share): use transparent background 2023-05-30 16:19:28 +02:00
FineFindus
a033d711c1 feat: show page URL in recents 2023-05-30 15:40:20 +02:00
82 changed files with 2042 additions and 656 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 85 versionCode 87
versionName "1.2.3+fork.85" versionName "1.2.3+fork.87"
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']
} }

View File

@@ -0,0 +1,89 @@
package org.joinmastodon.android.fragments;
import static org.junit.Assert.*;
import android.util.Pair;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.junit.Test;
import java.util.List;
import java.util.stream.Collectors;
public class ThreadFragmentTest {
private Status fakeStatus(String id, String inReplyTo) {
Status status = Status.ofFake(id, null, null);
status.inReplyToId = inReplyTo;
return status;
}
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
ThreadFragment.NeighborAncestryInfo info = new ThreadFragment.NeighborAncestryInfo(s);
info.descendantNeighbor = d;
info.ancestoringNeighbor = a;
return info;
}
@Test
public void mapNeighborhoodAncestry() {
StatusContext context = new StatusContext();
context.ancestors = List.of(
fakeStatus("oldest ancestor", null),
fakeStatus("younger ancestor", "oldest ancestor")
);
Status mainStatus = fakeStatus("main status", "younger ancestor");
context.descendants = List.of(
fakeStatus("first reply", "main status"),
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("another reply", "main status")
);
List<ThreadFragment.NeighborAncestryInfo> neighbors =
ThreadFragment.mapNeighborhoodAncestry(mainStatus, context);
assertEquals(List.of(
fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null),
fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)),
fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)),
fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus),
fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)),
fakeInfo(context.descendants.get(2), null, context.descendants.get(1)),
fakeInfo(context.descendants.get(3), null, null)
), neighbors);
}
@Test
public void sortStatusContext() {
StatusContext context = new StatusContext();
context.ancestors = List.of(
fakeStatus("younger ancestor", "oldest ancestor"),
fakeStatus("oldest ancestor", null)
);
context.descendants = List.of(
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("first reply", "main status"),
fakeStatus("another reply", "main status")
);
ThreadFragment.sortStatusContext(
fakeStatus("main status", "younger ancestor"),
context
);
List<Status> expectedAncestors = List.of(
fakeStatus("oldest ancestor", null),
fakeStatus("younger ancestor", "oldest ancestor")
);
List<Status> expectedDescendants = List.of(
fakeStatus("first reply", "main status"),
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("another reply", "main status")
);
// TODO: ??? i have no idea how this code works. it certainly doesn't return what i'd expect
}
}

View File

@@ -62,7 +62,8 @@
<data android:scheme="megalodon-android-auth" android:host="callback"/> <data android:scheme="megalodon-android-auth" android:host="callback"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"> <activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
android:theme="@style/TransparentDialog">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android;
import android.app.Fragment; import android.app.Fragment;
import android.content.ClipData; import android.content.ClipData;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
@@ -12,12 +11,14 @@ import android.widget.Toast;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil; import org.jsoup.internal.StringUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
@@ -28,18 +29,34 @@ public class ExternalShareActivity extends FragmentStackActivity{
UiUtils.setUserPreferredTheme(this); UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(savedInstanceState==null){ if(savedInstanceState==null){
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
boolean isMastodonURL = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts(); List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){ if(sessions.isEmpty()){
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
finish(); finish();
}else if(sessions.size()==1){ }else if(sessions.size()==1 && !isMastodonURL){
openComposeFragment(sessions.get(0).getID()); openComposeFragment(sessions.get(0).getID());
}else{ }else{
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000)); new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> {
UiUtils.pickAccount(this, null, R.string.choose_account, 0, if (open && text.isPresent()) {
session -> openComposeFragment(session.getID()), UiUtils.lookupURL(this, accountId, text.get(), false, (clazz, args) -> {
b -> b.setOnCancelListener(d -> finish()) if (clazz == null) {
); finish();
return;
}
args.putString("fromExternalShare", clazz.getSimpleName());
Intent intent = new Intent(this, MainActivity.class);
intent.putExtras(args);
finish();
startActivity(intent);
});
} else {
openComposeFragment(accountId);
}
}).show();
} }
} }
} }

View File

@@ -2,11 +2,14 @@ package org.joinmastodon.android;
import android.Manifest; import android.Manifest;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
@@ -20,12 +23,13 @@ import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity{ public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this); UiUtils.setUserPreferredTheme(this);
@@ -35,10 +39,18 @@ public class MainActivity extends FragmentStackActivity{
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){ if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment()); showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{ }else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session; AccountSession session;
Bundle args=new Bundle(); Bundle args=new Bundle();
Intent intent=getIntent(); Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
AccountSessionManager.getInstance()
.setLastActiveAccountID(intent.getStringExtra("account"));
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
AccountSessionManager.getInstance().getLastActiveAccount());
showFragmentForExternalShare(intent.getExtras());
return;
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false); boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification"); boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){ if(fromNotification){
@@ -52,6 +64,7 @@ public class MainActivity extends FragmentStackActivity{
}else{ }else{
session=AccountSessionManager.getInstance().getLastActiveAccount(); session=AccountSessionManager.getInstance().getLastActiveAccount();
} }
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
args.putString("account", session.getID()); args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment(); Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args); fragment.setArguments(args);
@@ -75,11 +88,12 @@ public class MainActivity extends FragmentStackActivity{
@Override @Override
protected void onNewIntent(Intent intent){ protected void onNewIntent(Intent intent){
super.onNewIntent(intent); super.onNewIntent(intent);
if(intent.getBooleanExtra("fromNotification", false)){ AccountSessionManager.getInstance().maybeUpdateLocalInfo();
if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras());
else if (intent.getBooleanExtra("fromNotification", false)) {
String accountID=intent.getStringExtra("accountID"); String accountID=intent.getStringExtra("accountID");
AccountSession accountSession;
try{ try{
accountSession=AccountSessionManager.getInstance().getAccount(accountID); AccountSessionManager.getInstance().getAccount(accountID);
}catch(IllegalStateException x){ }catch(IllegalStateException x){
return; return;
} }
@@ -124,6 +138,19 @@ public class MainActivity extends FragmentStackActivity{
showFragment(fragment); showFragment(fragment);
} }
private void showFragmentForExternalShare(Bundle args) {
String clazz = args.getString("fromExternalShare");
Fragment fragment = switch (clazz) {
case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment();
default -> null;
};
if (fragment == null) return;
args.putBoolean("_can_go_back", true);
fragment.setArguments(args);
showFragment(fragment);
}
private void showCompose(){ private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount(); AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated) if(session==null || !session.activated)
@@ -153,18 +180,40 @@ public class MainActivity extends FragmentStackActivity{
(fragmentContainers.get(fragmentContainers.size() - 1)).getId() (fragmentContainers.get(fragmentContainers.size() - 1)).getId()
); );
Bundle currentArgs = currentFragment.getArguments(); Bundle currentArgs = currentFragment.getArguments();
if (this.fragmentContainers.size() == 1 if (fragmentContainers.size() != 1
&& currentArgs != null || currentArgs == null
&& currentArgs.getBoolean("_can_go_back", false) || !currentArgs.getBoolean("_can_go_back", false)) {
&& currentArgs.containsKey("account")) { super.onBackPressed();
return;
}
if (currentArgs.getBoolean("_finish_on_back", false)) {
finish();
} else if (currentArgs.containsKey("account")) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString("account", currentArgs.getString("account")); args.putString("account", currentArgs.getString("account"));
if (getIntent().getBooleanExtra("fromNotification", false)) {
args.putString("tab", "notifications"); args.putString("tab", "notifications");
}
Fragment fragment=new HomeFragment(); Fragment fragment=new HomeFragment();
fragment.setArguments(args); fragment.setArguments(args);
showFragmentClearingBackStack(fragment); showFragmentClearingBackStack(fragment);
} else {
super.onBackPressed();
} }
} }
public Fragment getCurrentFragment() {
for (int i = fragmentContainers.size() - 1; i >= 0; i--) {
FrameLayout fl = fragmentContainers.get(i);
if (fl.getVisibility() == View.VISIBLE) {
return getFragmentManager().findFragmentById(fl.getId());
}
}
return null;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
super.onProvideAssistContent(assistContent);
Fragment fragment = getCurrentFragment();
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
}
} }

View File

@@ -25,6 +25,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent; import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.NotificationAction; import org.joinmastodon.android.model.NotificationAction;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification; import org.joinmastodon.android.model.PushNotification;
@@ -33,6 +34,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
@@ -273,8 +275,23 @@ public class PushNotificationReceiver extends BroadcastReceiver{
} }
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY); CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
// copied from ComposeFragment - TODO: generalize?
ArrayList<String> mentions=new ArrayList<>();
Status status = notification.status;
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!status.account.id.equals(ownID))
mentions.add('@'+status.account.acct);
for(Mention mention:status.mentions){
if(mention.id.equals(ownID))
continue;
String m='@'+mention.acct;
if(!mentions.contains(m))
mentions.add(m);
}
String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
CreateStatus.Request req=new CreateStatus.Request(); CreateStatus.Request req=new CreateStatus.Request();
req.status = input.toString(); req.status = initialText + input.toString();
req.language = preferences.postingDefaultLanguage; req.language = preferences.postingDefaultLanguage;
req.visibility = preferences.postingDefaultVisibility; req.visibility = preferences.postingDefaultVisibility;
req.inReplyToId = notification.status.id; req.inReplyToId = notification.status.id;
@@ -282,7 +299,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
req.spoilerText = "re: " + notification.status.spoilerText; req.spoilerText = "re: " + notification.status.spoilerText;
} }
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<Status>() { new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Status status) { public void onSuccess(Status status) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

View File

@@ -159,7 +159,7 @@ public class CacheController{
} }
} }
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain); Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isPleroma()) new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Notification> result){ public void onSuccess(List<Notification> result){

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.api.session; package org.joinmastodon.android.api.session;
import android.net.Uri;
import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
@@ -15,6 +17,7 @@ import org.joinmastodon.android.model.Token;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
public class AccountSession{ public class AccountSession{
public Token token; public Token token;
@@ -89,7 +92,14 @@ public class AccountSession{
return pushSubscriptionManager; return pushSubscriptionManager;
} }
public Instance getInstance() { public Optional<Instance> getInstance() {
return AccountSessionManager.getInstance().getInstanceInfo(domain); return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
}
public Uri getInstanceUri() {
return new Uri.Builder()
.scheme("https")
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
.build();
} }
} }

View File

@@ -125,14 +125,16 @@ public class AccountSessionManager{
} }
public synchronized void writeAccountsFile(){ public synchronized void writeAccountsFile(){
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json"); File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~");
File file = new File(MastodonApp.context.getFilesDir(), "accounts.json");
try{ try{
try(FileOutputStream out=new FileOutputStream(file)){ try(FileOutputStream out=new FileOutputStream(tmpFile)){
SessionsStorageWrapper w=new SessionsStorageWrapper(); SessionsStorageWrapper w=new SessionsStorageWrapper();
w.accounts=new ArrayList<>(sessions.values()); w.accounts=new ArrayList<>(sessions.values());
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(w, writer); MastodonAPIController.gson.toJson(w, writer);
writer.flush(); writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
} }
}catch(IOException x){ }catch(IOException x){
Log.e(TAG, "Error writing accounts file", x); Log.e(TAG, "Error writing accounts file", x);
@@ -256,31 +258,35 @@ public class AccountSessionManager{
} }
public void maybeUpdateLocalInfo(){ public void maybeUpdateLocalInfo(){
maybeUpdateLocalInfo(null);
}
public void maybeUpdateLocalInfo(AccountSession activeSession){
long now=System.currentTimeMillis(); long now=System.currentTimeMillis();
HashSet<String> domains=new HashSet<>(); HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){ for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase()); domains.add(session.domain.toLowerCase());
// if(now-session.infoLastUpdated>24L*3600_000L){ if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
updateSessionPreferences(session); updateSessionPreferences(session);
updateSessionLocalInfo(session); updateSessionLocalInfo(session);
// } }
// if(now-session.filtersLastUpdated>3600_000L){ if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
updateSessionWordFilters(session); updateSessionWordFilters(session);
// } }
updateSessionMarkers(session); updateSessionMarkers(session);
} }
if(loadedInstances){ if(loadedInstances){
maybeUpdateCustomEmojis(domains); maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
} }
} }
private void maybeUpdateCustomEmojis(Set<String> domains){ private void maybeUpdateCustomEmojis(Set<String> domains, String activeDomain){
long now=System.currentTimeMillis(); long now=System.currentTimeMillis();
for(String domain:domains){ for(String domain:domains){
// Long lastUpdated=instancesLastUpdated.get(domain); Long lastUpdated=instancesLastUpdated.get(domain);
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){ if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
updateInstanceInfo(domain); updateInstanceInfo(domain);
// } }
} }
} }
@@ -408,7 +414,9 @@ public class AccountSessionManager{
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
} }
}) })
.execNoAuth(domain); .execNoAuth(domain);
@@ -419,10 +427,13 @@ public class AccountSessionManager{
} }
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){ private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){ File file = getInstanceInfoFile(domain);
File tmpFile = new File(file.getPath() + "~");
try(FileOutputStream out=new FileOutputStream(tmpFile)){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer); MastodonAPIController.gson.toJson(emojis, writer);
writer.flush(); writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}catch(IOException x){ }catch(IOException x){
Log.w(TAG, "Error writing instance info file for "+domain, x); Log.w(TAG, "Error writing instance info file for "+domain, x);
} }
@@ -442,7 +453,7 @@ public class AccountSessionManager{
} }
if(!loadedInstances){ if(!loadedInstances){
loadedInstances=true; loadedInstances=true;
maybeUpdateCustomEmojis(domains); maybeUpdateCustomEmojis(domains, null);
} }
} }
@@ -463,12 +474,7 @@ public class AccountSessionManager{
} }
public Instance getInstanceInfo(String domain){ public Instance getInstanceInfo(String domain){
Instance instance = instances.get(domain); return instances.get(domain);
if (instance == null) {
throw new IllegalStateException("Cannot get instance for " + domain + ". Sessions: "
+ String.join(", ", instances.keySet()));
}
return instance;
} }
public void updateAccountInfo(String id, Account account){ public void updateAccountInfo(String id, Account account){

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -130,4 +131,13 @@ public class AccountTimelineFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT; return Filter.FilterContext.ACCOUNT;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
// could return different uris based on filter (e.g. media -> "/media"), but i want to
// return the remote url to the user, and i don't know whether i'd need to append
// '#media' (akkoma/pleroma) or '/media' (glitch/mastodon) since i don't know anything
// about the remote instance. so, just returning the base url to the user instead
return Uri.parse(user.url);
}
} }

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@@ -103,4 +104,9 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
}) })
.exec(accountID); .exec(accountID);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/announcements").build() : null;
}
} }

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.app.assist.AssistContent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
@@ -15,6 +16,7 @@ import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.animation.TranslateAnimation; import android.view.animation.TranslateAnimation;
import android.widget.ImageButton; import android.widget.ImageButton;
@@ -45,6 +47,7 @@ import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.TypedObjectPool; import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList; import java.util.ArrayList;
@@ -68,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab{ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, 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;
@@ -129,7 +132,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.clear(); displayItems.clear();
} }
protected void prependItems(List<T> items, boolean notify){ protected int prependItems(List<T> items, boolean notify){
data.addAll(0, items); data.addAll(0, items);
int offset=0; int offset=0;
for(T s:items){ for(T s:items){
@@ -142,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
if(notify) if(notify)
adapter.notifyItemRangeInserted(0, offset); adapter.notifyItemRangeInserted(0, offset);
return offset;
} }
protected String getMaxID(){ protected String getMaxID(){
@@ -202,7 +206,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override @Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index); MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){ if(holder!=null && list!=null){
transitioningHolder=holder; transitioningHolder=holder;
View view=transitioningHolder.photo; View view=transitioningHolder.photo;
int[] pos={0, 0}; int[] pos={0, 0};
@@ -334,6 +338,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
private Rect tmpRect=new Rect(); private Rect tmpRect=new Rect();
@Override @Override
public void getSelectorBounds(View view, Rect outRect){ public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect); list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view); RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){ if(holder instanceof StatusDisplayItem.Holder){
@@ -345,19 +351,41 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){ for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i); View child=list.getChildAt(i);
holder=list.getChildViewHolder(child); holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){ if(holder instanceof StatusDisplayItem.Holder<?> h){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID(); String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(otherID.equals(id)){ if(otherID.equals(id)){
if (firstIndex < 0) firstIndex = i;
lastIndex = i;
StatusDisplayItem item = h.getItem();
hasDescendant = item.hasDescendantNeighbor();
// no for direct descendants because main status (right above) is
// being displayed with an extended footer - no connected layout
hasAncestor = item.hasAncestoringNeighbor() && !item.isDirectDescendant;
list.getDecoratedBoundsWithMargins(child, tmpRect); list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left); outRect.left=Math.min(outRect.left, tmpRect.left);
outRect.top=Math.min(outRect.top, tmpRect.top); outRect.top=Math.min(outRect.top, tmpRect.top);
outRect.right=Math.max(outRect.right, tmpRect.right); outRect.right=Math.max(outRect.right, tmpRect.right);
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom); outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
isWarning = true;
} }
} }
} }
} }
} }
// shifting the selection box down
// see also: FooterStatusDisplayItem#onBind (setMargins)
if (isWarning || firstIndex < 0 || lastIndex < 0) return;
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(prevIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(nextIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4);
if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4);
}
}); });
list.setItemAnimator(new BetterItemAnimator()); list.setItemAnimator(new BetterItemAnimator());
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true); ((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
@@ -570,6 +598,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
warning.getItem().status.filterRevealed = true; warning.getItem().status.filterRevealed = true;
} }
@Override
public String getAccountID(){ public String getAccountID(){
return accountID; return accountID;
} }
@@ -703,6 +732,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return attachmentViewsPool; return attachmentViewsPool;
} }
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@@ -764,6 +797,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling); RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){ && (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor()) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint); drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
} }
} }

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses; import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
@@ -41,4 +42,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT; return Filter.FilterContext.ACCOUNT;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/bookmarks").build();
}
} }

View File

@@ -263,9 +263,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Nav.finish(this); Nav.finish(this);
return; return;
} }
if(customEmojis.isEmpty()){
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments(); Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus")); if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
@@ -1087,7 +1084,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
req.status=text; req.status=text;
req.localOnly=localOnly; req.localOnly=localOnly;
req.visibility=localOnly && instance.isPleroma() ? StatusPrivacy.LOCAL : statusVisibility; req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive; req.sensitive=sensitive;
req.language=language; req.language=language;
req.contentType=contentType; req.contentType=contentType;
@@ -1902,7 +1899,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Menu m=visibilityPopup.getMenu(); Menu m=visibilityPopup.getMenu();
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.isPleroma()) { if (instance.isAkkoma()) {
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

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses; import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
@@ -41,4 +42,11 @@ public class FavoritedStatusListFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT; return Filter.FilterContext.ACCOUNT;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.encodedPath(isInstanceAkkoma()
? '/' + getSession().self.username + "#favorites"
: "/favourites").build();
}
} }

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
@@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;
@@ -148,6 +150,16 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/friend-requests" : "/follow_requests").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop { public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String nextMaxID; private String nextMaxID;
private String accountId; private String accountID;
public FollowedHashtagsFragment() { public FollowedHashtagsFragment() {
super(20); super(20);
@@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bundle args=getArguments(); Bundle args=getArguments();
accountId=args.getString("account"); accountID=args.getString("account");
setTitle(R.string.sk_hashtags_you_follow); setTitle(R.string.sk_hashtags_you_follow);
} }
@@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
onDataLoaded(result, nextMaxID!=null); onDataLoaded(result, nextMaxID!=null);
} }
}) })
.exec(accountId); .exec(accountID);
} }
@Override @Override
@@ -75,6 +77,16 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/followed_tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{ private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull @NonNull
@Override @Override
@@ -109,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override @Override
public void onClick() { public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following); UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
} }
} }
} }

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.fragments;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import java.util.Optional;
public interface HasAccountID {
String getAccountID();
default AccountSession getSession() {
return AccountSessionManager.getInstance().getAccount(getAccountID());
}
default boolean isInstanceAkkoma() {
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.Menu; import android.view.Menu;
@@ -8,7 +9,6 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
@@ -159,4 +159,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return Filter.FilterContext.PUBLIC;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
}
} }

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Fragment; import android.app.Fragment;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.graphics.Outline; import android.graphics.Outline;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@@ -16,6 +17,11 @@ import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.notifications.GetNotifications;
@@ -30,16 +36,13 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -52,7 +55,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout; import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{ public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID {
private FragmentRootLinearLayout content; private FragmentRootLinearLayout content;
private HomeTabFragment homeTabFragment; private HomeTabFragment homeTabFragment;
private NotificationsFragment notificationsFragment; private NotificationsFragment notificationsFragment;
@@ -74,8 +77,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
E.register(this); E.register(this);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name); setTitle(R.string.sk_app_name);
Instance instance = AccountSessionManager.getInstance().getAccount(accountID).getInstance(); isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
isPleroma = instance.isPleroma(); .map(Instance::isAkkoma)
.orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true); setRetainInstance(true);
@@ -222,6 +226,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
public void setCurrentTab(@IdRes int tab){
if(tab==currentTab)
return;
tabBar.selectTab(tab);
onTabSelected(tab);
}
private void onTabSelected(@IdRes int tab){ private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab); Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){ if(tab==currentTab){
@@ -263,7 +274,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")"); options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
} }
new AccountSwitcherSheet(getActivity()).show(); new AccountSwitcherSheet(getActivity(), this).show();
return true; return true;
} }
return false; return false;
@@ -296,10 +307,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void updateNotificationBadge() { public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID); AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = session.getInstance(); Optional<Instance> instance = session.getInstance();
if (instance == null) return; if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.isPleroma()) new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(List<Notification> notifications) { public void onSuccess(List<Notification> notifications) {
@@ -336,4 +347,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) { public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
setNotificationBadge(false); setNotificationBadge(false);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent);
}
} }

View File

@@ -10,6 +10,7 @@ import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.app.assist.AssistContent;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@@ -54,6 +55,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@@ -71,7 +73,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab { public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
private static final int ANNOUNCEMENTS_RESULT = 654; private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID; private String accountID;
@@ -693,6 +695,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return fab; return fab;
} }
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> { private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull @NonNull
@Override @Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -285,4 +286,9 @@ public class HomeTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME; return Filter.FilterContext.HOME;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/").build();
}
} }

View File

@@ -1,13 +1,13 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -168,4 +168,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME; return Filter.FilterContext.HOME;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/lists/" + listID).build();
}
} }

View File

@@ -1,258 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.ListTimelineEditor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop {
private String accountId;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
private final HashMap<String, Boolean> userInList = new HashMap<>();
private ListsAdapter adapter;
public ListTimelinesFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
setHasOptionsMenu(true);
E.register(this);
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
String profileDisplayUsername = args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else {
setTitle(R.string.sk_your_lists);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId)
)
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
return true;
}
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
if (getActivity() == null) return;
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
if (getActivity() == null) return;
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
data.remove(i);
adapter.notifyItemRemoved(i);
break;
}
}
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
adapter.notifyItemChanged(i);
break;
}
}
}
@Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
return adapter = new ListsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
Bundle args=new Bundle();
args.putString("account", accountId);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
}
}

View File

@@ -0,0 +1,270 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.ListTimelineEditor;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
private final HashMap<String, Boolean> userInList = new HashMap<>();
private ListsAdapter adapter;
public ListsFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
accountID = args.getString("account");
setHasOptionsMenu(true);
E.register(this);
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
String profileDisplayUsername = args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else {
setTitle(R.string.sk_your_lists);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID)
)
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
return true;
}
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
if (getActivity() == null) return;
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
if (getActivity() == null) return;
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountID);
}
})
.exec(accountID);
}
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
data.remove(i);
adapter.notifyItemRemoved(i);
break;
}
}
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
adapter.notifyItemChanged(i);
break;
}
}
}
@Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
return adapter = new ListsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/lists").build();
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -13,6 +14,12 @@ import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -24,12 +31,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{ public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
private TabLayout tabLayout; private TabLayout tabLayout;
private ViewPager2 pager; private ViewPager2 pager;
@@ -47,7 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
private NotificationsListFragment allNotificationsFragment, mentionsFragment; private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private String accountID; private String accountID;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -227,6 +228,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
}; };
} }
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{ private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull @NonNull
@Override @Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@@ -9,10 +10,8 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.markers.SaveMarkers; import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead; import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent; import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
@@ -22,6 +21,7 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
@@ -44,7 +44,6 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{ public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
@@ -156,18 +155,19 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
loadRelationships(needRelationships); loadRelationships(needRelationships);
maxID=result.maxID; maxID=result.maxID;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && AccountSessionManager.getInstance().getAccount(accountID).markers.notifications != null){ Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
E.post(new AllNotificationsSeenEvent()); E.post(new AllNotificationsSeenEvent());
new SaveMarkers(null, result.items.get(0).id).exec(accountID); new SaveMarkers(null, result.items.get(0).id).exec(accountID);
if (AccountSessionManager.getInstance().getAccount(accountID).markers != null)
AccountSessionManager.getInstance().getAccount(accountID).markers AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id; .notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile(); AccountSessionManager.getInstance().writeAccountsFile();
if (AccountSessionManager.getInstance().getAccount(accountID).getInstance().isPleroma()) if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID); new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
} }
} }
}
}); });
} }
@@ -272,4 +272,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
displayItems.subList(index, lastIndex).clear(); displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index); adapter.notifyItemRangeRemoved(index, lastIndex-index);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + getSession().self.username + "/interactions"
: "/notifications").build();
}
} }

View File

@@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.app.Activity; import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Color; import android.graphics.Color;
@@ -50,7 +51,6 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials; import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
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.account_list.FollowerListFragment; import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment; import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
@@ -75,6 +75,7 @@ import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView; import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -92,6 +93,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2; import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@@ -111,7 +113,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{ public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
private static final int AVATAR_RESULT=722; private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343; private static final int COVER_RESULT=343;
@@ -182,12 +184,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
loaded=true; loaded=true;
if(!isOwnProfile) if(!isOwnProfile)
loadRelationship(); loadRelationship();
else { else if (isInstanceAkkoma() && getInstance().isPresent())
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(domain); maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
if (instance.isPleroma()) {
maxFields = instance.pleroma.metadata.fieldsLimits.maxFields;
}
}
}else{ }else{
profileAccountID=getArguments().getString("profileAccountID"); profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false)) if(!getArguments().getBoolean("noAutoLoad", false))
@@ -712,7 +710,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileAccount", profileAccountID); args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername()); args.putString("profileDisplayUsername", account.getDisplayUsername());
} }
Nav.go(getActivity(), ListTimelinesFragment.class, args); Nav.go(getActivity(), ListsFragment.class, args);
}else if(id==R.id.followed_hashtags){ }else if(id==R.id.followed_hashtags){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
@@ -1168,6 +1166,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if (adapter != null) adapter.notifyDataSetChanged(); if (adapter != null) adapter.notifyDataSetChanged();
} }
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(account.url);
}
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter { private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
public MetadataAdapter(){ public MetadataAdapter(){
super(imgLoader); super(imgLoader);

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.ImageButton; import android.widget.ImageButton;
@@ -181,4 +182,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
} }
return null; return null;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
// TODO: adapt when frontends finally implement a scheduled posts list
return null;
}
} }

View File

@@ -7,9 +7,9 @@ import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.util.LruCache; import android.util.LruCache;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Gravity; import android.view.Gravity;
@@ -59,9 +59,11 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TextInputFrameLayout; import org.joinmastodon.android.ui.views.TextInputFrameLayout;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
@@ -78,7 +80,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class SettingsFragment extends MastodonToolbarFragment{ public class SettingsFragment extends MastodonToolbarFragment implements ProvidesAssistContent.ProvidesWebUri {
private UsableRecyclerView list; private UsableRecyclerView list;
private ArrayList<Item> items=new ArrayList<>(); private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem; private ThemeItem themeItem;
@@ -105,7 +107,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
imageCache = ImageCache.getInstance(getActivity()); imageCache = ImageCache.getInstance(getActivity());
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = session.getInstance(); Optional<Instance> instance = session.getInstance();
String instanceName = UiUtils.getInstanceName(accountID); String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){ if(GithubSelfUpdater.needSelfUpdating()){
@@ -223,7 +225,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.showReplies=i.checked; GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
})); }));
if (instance.isPleroma()) { if (isInstanceAkkoma()) {
items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{ items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL); PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.reply_visibility); popupMenu.inflate(R.menu.reply_visibility);
@@ -299,7 +301,9 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.save(); GlobalUserPreferences.save();
needAppRestart=true; needAppRestart=true;
})); }));
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled; boolean translationAvailable = instance
.map(i -> i.v2 != null && i.v2.configuration.translation != null && i.v2.configuration.translation.enabled)
.orElse(false);
items.add(new SmallTextItem(getString(translationAvailable ? items.add(new SmallTextItem(getString(translationAvailable ?
R.string.sk_settings_translation_availability_note_available : R.string.sk_settings_translation_availability_note_available :
R.string.sk_settings_translation_availability_note_unavailable, instanceName))); R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
@@ -324,16 +328,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular)); items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
items.add(new HeaderItem(instanceName)); items.add(new HeaderItem(instanceName));
items.add(new TextItem(R.string.sk_settings_rules, ()->{ items.add(new TextItem(R.string.sk_settings_rules, instance.<Runnable>map(i -> () -> {
Bundle args=new Bundle(); Bundle args = new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); args.putParcelable("instance", Parcels.wrap(i));
Nav.go(getActivity(), InstanceRulesFragment.class, args); Nav.go(getActivity(), InstanceRulesFragment.class, args);
}, R.drawable.ic_fluent_task_list_ltr_24_regular)); }).orElse(null), R.drawable.ic_fluent_task_list_ltr_24_regular));
items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular)); items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular)); items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular)); items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular)); items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version))); items.add(new SmallTextItem(instance
.map(i -> getString(R.string.sk_settings_server_version, i.version))
.orElse(getString(R.string.sk_instance_info_unavailable))));
items.add(new HeaderItem(R.string.sk_instance_features)); items.add(new HeaderItem(R.string.sk_instance_features));
items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{ items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{
@@ -361,14 +367,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
b.setText(getContentTypeString(contentType)); b.setText(getContentTypeString(contentType));
contentTypeMenu = popupMenu.getMenu(); contentTypeMenu = popupMenu.getMenu();
contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true); contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true);
ContentType.adaptMenuToInstance(contentTypeMenu, instance); instance.ifPresent(i -> ContentType.adaptMenuToInstance(contentTypeMenu, i));
})); }));
items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation))); items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation)));
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{ items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
glitchModeItem.enabled = i.checked; glitchModeItem.enabled = i.checked;
if (i.checked) { if (i.checked) {
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID); GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID); if (!isInstanceAkkoma()) {
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
}
} else { } else {
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID); GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
GlobalUserPreferences.accountsInGlitchMode.remove(accountID); GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
@@ -734,6 +742,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
} }
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/about" : "/settings").build();
}
@Override
public String getAccountID() {
return accountID;
}
private static abstract class Item{ private static abstract class Item{
public abstract int getViewType(); public abstract int getViewType();
} }

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -24,13 +25,13 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class StatusEditHistoryFragment extends StatusListFragment{ public class StatusEditHistoryFragment extends StatusListFragment{
private String id; private String id, url;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
id=getArguments().getString("id"); id=getArguments().getString("id");
url=getArguments().getString("url");
loadData(); loadData();
} }
@@ -162,4 +163,9 @@ public class StatusEditHistoryFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return null; return null;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(url);
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
@@ -28,7 +29,7 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
public abstract class StatusListFragment extends BaseStatusListFragment<Status>{ public abstract class StatusListFragment extends BaseStatusListFragment<Status> {
protected EventListener eventListener=new EventListener(); protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
@@ -182,7 +183,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
} }
@Override @Override
public void onConfigurationChanged(Configuration newConfig){ public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo(); if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
} }

View File

@@ -1,37 +1,67 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext; import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext; import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate; import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class ThreadFragment extends StatusListFragment{ public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus; protected Status mainStatus;
/**
* lists the hierarchy of ancestors and descendants in a thread. level 0 = the main status.
* e.g.
* <pre>
* [0] ancestor: -2 ↰
* [1] ancestor: -1 ↰
* [2] main status: 0 ↰
* [3] descendant: 1 ↰
* [4] descendant: 2 ↰
* [5] descendant: 3
* [6] descendant: 1
* [7] descendant: 1 ↰
* [8] descendant: 2
* </pre>
* confused? good. /j
*/
private final List<Pair<String, Integer>> levels = new ArrayList<>();
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -47,13 +77,42 @@ public class ThreadFragment extends StatusListFragment{
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s); List<StatusDisplayItem> items=super.buildDisplayItems(s);
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
// modifying hidden filtered items if status is displayed as a warning
List<StatusDisplayItem> itemsToModify =
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
? warning.filteredItems
: items;
for(int i = 0; i < itemsToModify.size(); i++){
StatusDisplayItem item = itemsToModify.get(i);
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
if (ancestryInfo != null) {
item.setAncestryInfo(
ancestryInfo,
s.id.equals(mainStatus.id),
ancestryInfo.getAncestoringNeighbor()
.map(ancestor -> ancestor.id.equals(mainStatus.id))
.orElse(false)
);
}
if (item instanceof ReblogOrReplyLineStatusDisplayItem && !item.isDirectDescendant) {
deleteTheseItems.add(i);
}
if(s.id.equals(mainStatus.id)){ if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
if(item instanceof TextStatusDisplayItem text) if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true; text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer) else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true; footer.hideCounts=true;
} }
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus())); items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
} }
return items; return items;
@@ -68,36 +127,22 @@ public class ThreadFragment extends StatusListFragment{
if (getActivity() == null) return; if (getActivity() == null) return;
if(refreshing){ if(refreshing){
data.clear(); data.clear();
ancestryMap.clear();
displayItems.clear(); displayItems.clear();
data.add(mainStatus); data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus)); onAppendItems(Collections.singletonList(mainStatus));
} }
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.isPleroma()){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=result.ancestors.size()-1; i >= 0; i--){
Status s=result.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList()); // TODO: figure out how this code works
result.descendants=getDescendantsOrdered(mainStatus.id, if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
result.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
result.descendants=filterStatuses(result.descendants); result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors); result.ancestors=filterStatuses(result.ancestors);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
}
if(footerProgress!=null) if(footerProgress!=null)
footerProgress.setVisibility(View.GONE); footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants); data.addAll(result.descendants);
@@ -106,7 +151,12 @@ public class ThreadFragment extends StatusListFragment{
int count=displayItems.size(); int count=displayItems.size();
if(!refreshing) if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount); adapter.notifyItemRangeInserted(prevCount, count-prevCount);
prependItems(result.ancestors, !refreshing); int prependedCount = prependItems(result.ancestors, !refreshing);
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
displayItems.remove(prependedCount);
adapter.notifyItemRemoved(prependedCount);
count--;
}
dataLoaded(); dataLoaded();
if(refreshing){ if(refreshing){
refreshDone(); refreshDone();
@@ -118,7 +168,61 @@ public class ThreadFragment extends StatusListFragment{
.exec(accountID); .exec(accountID);
} }
private List<Status> getDescendantsOrdered(String id, List<Status> statuses){ public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
List<Status> statuses = new ArrayList<>(context.ancestors);
statuses.add(mainStatus);
statuses.addAll(context.descendants);
int count = statuses.size();
for (int index = 0; index < count; index++) {
Status current = statuses.get(index);
NeighborAncestryInfo item = new NeighborAncestryInfo(current);
item.descendantNeighbor = Optional
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
.filter(s -> s.inReplyToId.equals(current.id))
.orElse(null);
item.ancestoringNeighbor = Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
.filter(ancestor -> ancestor
.getDescendantNeighbor()
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
.orElse(false))
.flatMap(NeighborAncestryInfo::getStatus)
.orElse(null);
ancestry.add(item);
}
return ancestry;
}
public static void sortStatusContext(Status mainStatus, StatusContext context) {
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:context.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=context.ancestors.size()-1; i >= 0; i--){
Status s=context.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
context.ancestors=context.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
context.descendants=getDescendantsOrdered(mainStatus.id,
context.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
private static List<Status> getDescendantsOrdered(String id, List<Status> statuses){
List<Status> out=new ArrayList<>(); List<Status> out=new ArrayList<>();
for(Status s:getDirectDescendants(id, statuses)){ for(Status s:getDirectDescendants(id, statuses)){
out.add(s); out.add(s);
@@ -130,7 +234,7 @@ public class ThreadFragment extends StatusListFragment{
return out; return out;
} }
private List<Status> getDirectDescendants(String id, List<Status> statuses){ private static List<Status> getDirectDescendants(String id, List<Status> statuses){
return statuses.stream() return statuses.stream()
.filter(s -> s.inReplyToId.equals(id)) .filter(s -> s.inReplyToId.equals(id))
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -187,4 +291,52 @@ public class ThreadFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.THREAD; return Filter.FilterContext.THREAD;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(mainStatus.url);
}
public static class NeighborAncestryInfo {
protected Status status, descendantNeighbor, ancestoringNeighbor;
public NeighborAncestryInfo(@NonNull Status status) {
this.status = status;
}
public Optional<Status> getStatus() {
return Optional.ofNullable(status);
}
public Optional<Status> getDescendantNeighbor() {
return Optional.ofNullable(descendantNeighbor);
}
public Optional<Status> getAncestoringNeighbor() {
return Optional.ofNullable(ancestoringNeighbor);
}
public boolean hasDescendantNeighbor() {
return getDescendantNeighbor().isPresent();
}
public boolean hasAncestoringNeighbor() {
return getAncestoringNeighbor().isPresent();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NeighborAncestryInfo that = (NeighborAncestryInfo) o;
return status.equals(that.status)
&& Objects.equals(descendantNeighbor, that.descendantNeighbor)
&& Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor);
}
@Override
public int hashCode() {
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
}
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
@@ -14,4 +15,11 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
account=Parcels.unwrap(getArguments().getParcelable("targetAccount")); account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct); setTitle("@"+account.acct);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + account.id
: '@' + account.acct).build();
}
} }

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
@@ -23,7 +24,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ListTimelinesFragment; import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment; import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
@@ -34,6 +36,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
@@ -57,7 +60,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> { public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID; protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>(); protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -170,6 +173,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super.onApplyWindowInsets(insets); super.onApplyWindowInsets(insets);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){
super(imgLoader); super(imgLoader);
@@ -387,7 +400,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
args.putString("account", accountID); args.putString("account", accountID);
args.putString("profileAccount", account.id); args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername()); args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args); Nav.go(getActivity(), ListsFragment.class, args);
} }
return true; return true;
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -19,4 +20,10 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
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(account.id, maxID, count);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath(isInstanceAkkoma() ? "#followers" : "/followers").build();
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -19,4 +20,10 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
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(account.id, maxID, count);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath(isInstanceAkkoma() ? "#followees" : "/following").build();
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -18,4 +19,12 @@ public class StatusFavoritesListFragment extends StatusRelatedAccountListFragmen
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(status.id, maxID, count);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
Uri statusUri = super.getWebUri(base);
return isInstanceAkkoma()
? statusUri
: statusUri.buildUpon().appendPath("favourites").build();
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -18,4 +19,12 @@ public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
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(status.id, maxID, count);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
Uri statusUri = super.getWebUri(base);
return isInstanceAkkoma()
? statusUri
: statusUri.buildUpon().appendPath("reblogs").build();
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -18,4 +19,13 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
protected boolean hasSubtitle(){ protected boolean hasSubtitle(){
return false; return false;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base
.encodedPath(isInstanceAkkoma()
? "/notice/" + status.id
: '@' + status.account.acct + '/' + status.id)
.build();
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -52,4 +53,9 @@ public class BubbleTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return Filter.FilterContext.PUBLIC;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/main/bubble").build() : null;
}
} }

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
@@ -27,6 +28,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
@@ -49,7 +51,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop { public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;
@@ -145,6 +147,16 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/suggestions").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
@@ -26,6 +27,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -37,7 +39,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop { public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent {
private TabLayout tabLayout; private TabLayout tabLayout;
private ViewPager2 pager; private ViewPager2 pager;
@@ -50,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private ProgressBar searchProgress; private ProgressBar searchProgress;
private DiscoverPostsFragment postsFragment; private DiscoverPostsFragment postsFragment;
private TrendingHashtagsFragment hashtagsFragment; private DiscoverHashtagsFragment hashtagsFragment;
private DiscoverNewsFragment newsFragment; private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment; private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment; private SearchFragment searchFragment;
@@ -118,7 +120,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment=new DiscoverPostsFragment(); postsFragment=new DiscoverPostsFragment();
postsFragment.setArguments(args); postsFragment.setArguments(args);
hashtagsFragment=new TrendingHashtagsFragment(); hashtagsFragment=new DiscoverHashtagsFragment();
hashtagsFragment.setArguments(args); hashtagsFragment.setArguments(args);
newsFragment=new DiscoverNewsFragment(); newsFragment=new DiscoverNewsFragment();
@@ -321,6 +323,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE); V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
} }
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(searchActive
? searchFragment
: getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{ private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull @NonNull
@Override @Override

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams; import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams; import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -18,6 +19,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView; import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List; import java.util.List;
@@ -27,11 +29,11 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop { public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS); private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public TrendingHashtagsFragment(){ public DiscoverHashtagsFragment(){
super(10); super(10);
} }
@@ -76,6 +78,16 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{ private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull @NonNull
@Override @Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@@ -19,6 +20,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -35,7 +37,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop { public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList(); private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS); private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
@@ -88,6 +90,16 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{ private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(){ public LinksAdapter(){
super(imgLoader); super(imgLoader);

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -43,9 +44,13 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return Filter.FilterContext.PUBLIC;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/posts").build();
}
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -25,7 +26,6 @@ public class FederatedTimelineFragment extends StatusListFragment {
return true; return true;
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count) currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
@@ -52,4 +52,9 @@ public class FederatedTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return Filter.FilterContext.PUBLIC;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/all" : "/public").build();
}
} }

View File

@@ -1,9 +1,11 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -24,7 +26,6 @@ public class LocalTimelineFragment extends StatusListFragment {
return true; return true;
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count) currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@@ -51,4 +52,9 @@ public class LocalTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return Filter.FilterContext.PUBLIC;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build();
}
} }

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.app.Activity; import android.app.Activity;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
@@ -316,6 +317,14 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
return isRecyclerViewOnTop(list); return isRecyclerViewOnTop(list);
} }
@Override
public Uri getWebUri(Uri.Builder base) {
Uri.Builder searchUri = base.path("/search");
return isInstanceAkkoma()
? searchUri.appendQueryParameter("query", currentQuery).build()
: searchUri.build();
}
@FunctionalInterface @FunctionalInterface
public interface ProgressVisibilityListener{ public interface ProgressVisibilityListener{
void onProgressVisibilityChanged(boolean visible); void onProgressVisibilityChanged(boolean visible);

View File

@@ -89,13 +89,6 @@ public class AccountActivationFragment extends ToolbarFragment{
return !UiUtils.isDarkTheme(); return !UiUtils.isDarkTheme();
} }
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override @Override
protected void onUpdateToolbar(){ protected void onUpdateToolbar(){
super.onUpdateToolbar(); super.onUpdateToolbar();
@@ -110,7 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override @Override
public void onToolbarNavigationClick(){ public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity()).show(); new AccountSwitcherSheet(getActivity(), null).show();
} }
@Override @Override

View File

@@ -2,7 +2,9 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.assist.AssistContent;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.Html; import android.text.Html;
@@ -24,6 +26,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -38,7 +41,7 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout; import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{ public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAssistContent {
private UsableRecyclerView list; private UsableRecyclerView list;
private MergeRecyclerAdapter adapter; private MergeRecyclerAdapter adapter;
private Button btn; private Button btn;
@@ -130,6 +133,15 @@ public class InstanceRulesFragment extends ToolbarFragment{
} }
} }
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(new Uri.Builder()
.scheme("https")
.authority(instance.normalizedUri)
.path("/about")
.build());
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{ private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull @NonNull

View File

@@ -5,6 +5,7 @@ import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.SparseIntArray; import android.util.SparseIntArray;
@@ -267,4 +268,11 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() { protected Filter.FilterContext getFilterContext() {
return null; return null;
} }
@Override
public Uri getWebUri(Uri.Builder base) {
if (reportStatus != null) return Uri.parse(reportStatus.url);
if (reportAccount != null) return Uri.parse(reportAccount.url);
return null;
}
} }

View File

@@ -11,6 +11,7 @@ import java.net.IDN;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
@Parcel @Parcel
public class Instance extends BaseModel{ public class Instance extends BaseModel{
@@ -88,6 +89,9 @@ public class Instance extends BaseModel{
public PleromaPollLimits pollLimits; public PleromaPollLimits pollLimits;
/** like uri, but always without scheme and trailing slash */
public transient String normalizedUri;
@Override @Override
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
super.postprocess(); super.postprocess();
@@ -97,6 +101,10 @@ public class Instance extends BaseModel{
rules=Collections.emptyList(); rules=Collections.emptyList();
if(shortDescription==null) if(shortDescription==null)
shortDescription=""; shortDescription="";
// akkoma says uri is "https://example.social" while just "example.social" on mastodon
normalizedUri = uri
.replaceFirst("^https://", "")
.replaceFirst("/$", "");
} }
@Override @Override
@@ -136,10 +144,26 @@ public class Instance extends BaseModel{
return ci; return ci;
} }
public boolean isPleroma() { public boolean isAkkoma() {
return pleroma != null; return pleroma != null;
} }
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)
.map(m -> m.features);
return switch (feature) {
case BUBBLE_TIMELINE -> pleromaFeatures
.map(f -> f.contains("bubble_timeline"))
.orElse(false);
};
}
public enum Feature {
BUBBLE_TIMELINE
}
@Parcel @Parcel
public static class Rule{ public static class Rule{
public String id; public String id;

View File

@@ -259,13 +259,14 @@ public class TimelineDefinition {
public boolean isCompatible(AccountSession session) { public boolean isCompatible(AccountSession session) {
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of // still enabling the bubble timeline for all pleroma/akkoma instances since i know of
// at least one instance that supports it, but doesn't list "bubble_timeline" // at least one instance that supports it, but doesn't list "bubble_timeline"
return session.getInstance().isPleroma(); return session.getInstance().map(Instance::isAkkoma).orElse(false);
} }
@Override @Override
public boolean wantsDefault(AccountSession session) { public boolean wantsDefault(AccountSession session) {
Instance instance = session.getInstance(); return session.getInstance()
return instance.isPleroma() && instance.pleroma.metadata.features.contains("bubble_timeline"); .map(i -> i.hasFeature(Instance.Feature.BUBBLE_TIMELINE))
.orElse(false);
} }
}; };

View File

@@ -2,8 +2,8 @@ package org.joinmastodon.android.ui;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@@ -14,7 +14,7 @@ import android.view.WindowInsets;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupMenu; import android.widget.RadioButton;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
@@ -23,13 +23,21 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken; import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment; import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -49,13 +57,25 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountSwitcherSheet extends BottomSheet{ public class AccountSwitcherSheet extends BottomSheet{
private final Activity activity; private final Activity activity;
private final HomeFragment fragment;
private final BiConsumer<String, Boolean> onClick;
private final boolean externalShare, openInApp;
private UsableRecyclerView list; private UsableRecyclerView list;
private List<WrappedAccount> accounts; private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader; private ListImageLoaderWrapper imgLoader;
private AccountsAdapter accountsAdapter;
public AccountSwitcherSheet(@NonNull Activity activity){ public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
this(activity, fragment, false, false, null);
}
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp, BiConsumer<String, Boolean> onClick){
super(activity); super(activity);
this.activity=activity; this.activity=activity;
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList()); accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
@@ -67,41 +87,59 @@ public class AccountSwitcherSheet extends BottomSheet{
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
View handle=new View(activity); View handle=new View(activity);
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle); handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
handle.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(36)));
adapter.addAdapter(new SingleViewRecyclerAdapter(handle)); adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
adapter.addAdapter(new AccountsAdapter());
AccountViewHolder holder=new AccountViewHolder(); if (externalShare) {
holder.more.setVisibility(View.GONE); FrameLayout shareHeading = new FrameLayout(activity);
holder.currentIcon.setVisibility(View.GONE); activity.getLayoutInflater().inflate(R.layout.item_external_share_heading, shareHeading);
holder.name.setText(R.string.add_account); ((TextView) shareHeading.findViewById(R.id.title)).setText(openInApp
holder.avatar.setScaleType(ImageView.ScaleType.CENTER); ? R.string.sk_external_share_or_open_title
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled); : R.string.sk_external_share_title);
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary))); adapter.addAdapter(new SingleViewRecyclerAdapter(shareHeading));
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
setOnDismissListener((d) -> activity.finish());
}
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
if (!externalShare) {
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
Nav.go(activity, CustomWelcomeFragment.class, null); Nav.go(activity, CustomWelcomeFragment.class, null);
dismiss(); dismiss();
})); }));
// disabled in megalodon
// adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.log_out_all_accounts, R.drawable.ic_fluent_person_arrow_right_24_filled), this::confirmLogOutAll));
}
list.setAdapter(adapter); list.setAdapter(adapter);
DividerItemDecoration divider=new DividerItemDecoration(activity, R.attr.colorPollVoted, .5f, 72, 16, DividerItemDecoration.NOT_FIRST);
divider.setDrawBelowLastItem(true);
list.addItemDecoration(divider);
FrameLayout content=new FrameLayout(activity); FrameLayout content=new FrameLayout(activity);
content.setBackgroundResource(R.drawable.bg_bottom_sheet); content.setBackgroundResource(R.drawable.bg_bottom_sheet);
content.addView(list); content.addView(list);
setContentView(content); setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme()); setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(activity, R.attr.colorM3Surface),
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
} }
private void confirmLogOut(String accountID){ private void confirmLogOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(activity) new M3AlertDialogBuilder(activity)
.setTitle(R.string.log_out) .setMessage(activity.getString(R.string.confirm_log_out, session.getFullUsername()))
.setMessage(R.string.confirm_log_out)
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID)) .setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }
private void confirmLogOutAll(){
new M3AlertDialogBuilder(activity)
.setMessage(R.string.confirm_log_out_all_accounts)
.setPositiveButton(R.string.log_out, (dialog, which) -> logOutAll())
.setNegativeButton(R.string.cancel, null)
.show();
}
private void logOut(String accountID){ private void logOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken) new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
@@ -120,9 +158,55 @@ public class AccountSwitcherSheet extends BottomSheet{
.exec(accountID); .exec(accountID);
} }
private void logOutAll(){
final ProgressDialog progress=new ProgressDialog(activity);
progress.setMessage(activity.getString(R.string.loading));
progress.setCancelable(false);
progress.show();
ArrayList<AccountSession> sessions=new ArrayList<>(AccountSessionManager.getInstance().getLoggedInAccounts());
for(AccountSession session:sessions){
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
AccountSessionManager.getInstance().removeAccount(session.getID());
sessions.remove(session);
if(sessions.isEmpty()){
progress.dismiss();
Nav.goClearingStack(activity, SplashFragment.class, null);
dismiss();
}
}
@Override
public void onError(ErrorResponse error){
AccountSessionManager.getInstance().removeAccount(session.getID());
sessions.remove(session);
if(sessions.isEmpty()){
progress.dismiss();
Nav.goClearingStack(activity, SplashFragment.class, null);
dismiss();
}
}
})
.exec(session.getID());
}
}
private void onLoggedOut(String accountID){ private void onLoggedOut(String accountID){
AccountSessionManager.getInstance().removeAccount(accountID); AccountSessionManager.getInstance().removeAccount(accountID);
dismiss(); String activeAccountID = fragment != null
? fragment.getAccountID()
: AccountSessionManager.getInstance().getLastActiveAccountID();
if (accountID.equals(activeAccountID)) {
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
} else {
accounts.stream().filter(w -> accountID.equals(w.session.getID())).findAny().ifPresent(w -> {
accountsAdapter.notifyItemRemoved(accounts.indexOf(w));
accounts.remove(w);
});
}
} }
@Override @Override
@@ -140,6 +224,13 @@ public class AccountSwitcherSheet extends BottomSheet{
} }
} }
private View makeSimpleListItem(@StringRes int title, @DrawableRes int icon){
TextView tv=(TextView) activity.getLayoutInflater().inflate(R.layout.item_text_with_icon, list, false);
tv.setText(title);
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, 0, 0, 0);
return tv;
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){
super(imgLoader); super(imgLoader);
@@ -173,45 +264,42 @@ public class AccountSwitcherSheet extends BottomSheet{
} }
} }
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name; private final TextView name, username;
private final ImageView avatar; private final ImageView avatar;
private final ImageButton more; private final CheckableRelativeLayout view;
private final View currentIcon; private final View radioButton, extraBtnWrap;
private final PopupMenu menu; private final ImageButton extraBtn;
public AccountViewHolder(){ public AccountViewHolder(){
super(activity, R.layout.item_account_switcher, list); super(activity, R.layout.item_account_switcher, list);
name=findViewById(R.id.name); name=findViewById(R.id.name);
username=findViewById(R.id.username);
radioButton=findViewById(R.id.radiobtn);
radioButton.setBackground(new RadioButton(activity).getButtonDrawable());
avatar=findViewById(R.id.avatar); avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more); avatar.setOutlineProvider(OutlineProviders.roundedRect(OutlineProviders.RADIUS_MEDIUM));
currentIcon=findViewById(R.id.current);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true); avatar.setClipToOutline(true);
view=(CheckableRelativeLayout) itemView;
menu=new PopupMenu(activity, more); extraBtnWrap = findViewById(R.id.extra_btn_wrap);
menu.inflate(R.menu.account_switcher); extraBtn = findViewById(R.id.extra_btn);
menu.setOnMenuItemClickListener(item1 -> { extraBtn.setOnClickListener(this::onExtraBtnClick);
confirmLogOut(item.getID());
return true;
});
more.setOnClickListener(v->menu.show());
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@Override @Override
public void onBind(AccountSession item){ public void onBind(AccountSession item){
name.setText("@"+item.self.username+"@"+item.domain); name.setText(item.self.displayName);
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){ username.setText(item.getFullUsername());
more.setVisibility(View.GONE); radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
currentIcon.setVisibility(View.VISIBLE); extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);
}else{ if (externalShare) view.setCheckable(false);
more.setVisibility(View.VISIBLE); else {
currentIcon.setVisibility(View.GONE); String accountId = fragment != null
? fragment.getAccountID()
: AccountSessionManager.getInstance().getLastActiveAccountID();
view.setChecked(accountId.equals(item.getID()));
} }
menu.getMenu().findItem(R.id.log_out).setTitle(activity.getString(R.string.log_out_account, "@"+item.self.username));
UiUtils.enablePopupMenuIcons(activity, menu);
} }
@Override @Override
@@ -226,12 +314,32 @@ public class AccountSwitcherSheet extends BottomSheet{
setImage(index, null); setImage(index, null);
} }
private void onExtraBtnClick(View view) {
setOnDismissListener(null);
dismiss();
onClick.accept(item.getID(), true);
}
@Override @Override
public void onClick(){ public void onClick(){
setOnDismissListener(null);
if (onClick != null) {
dismiss();
onClick.accept(item.getID(), false);
return;
}
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID()); AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
activity.finish(); activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class)); activity.startActivity(new Intent(activity, MainActivity.class));
} }
@Override
public boolean onLongClick(){
if (externalShare) return false;
confirmLogOut(item.getID());
return true;
}
} }
private static class WrappedAccount{ private static class WrappedAccount{

View File

@@ -8,7 +8,15 @@ import android.view.ViewOutlineProvider;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class OutlineProviders{ public class OutlineProviders{
private static SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>(); private static final SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
private static final SparseArray<ViewOutlineProvider> topRoundedRects=new SparseArray<>();
private static final SparseArray<ViewOutlineProvider> endRoundedRects=new SparseArray<>();
public static final int RADIUS_XSMALL=4;
public static final int RADIUS_SMALL=8;
public static final int RADIUS_MEDIUM=12;
public static final int RADIUS_LARGE=16;
public static final int RADIUS_XLARGE=28;
private OutlineProviders(){ private OutlineProviders(){
//no instance //no instance
@@ -21,6 +29,12 @@ public class OutlineProviders{
outline.setAlpha(view.getAlpha()); outline.setAlpha(view.getAlpha());
} }
}; };
public static final ViewOutlineProvider OVAL=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
};
public static ViewOutlineProvider roundedRect(int dp){ public static ViewOutlineProvider roundedRect(int dp){
ViewOutlineProvider provider=roundedRects.get(dp); ViewOutlineProvider provider=roundedRects.get(dp);
@@ -31,6 +45,24 @@ public class OutlineProviders{
return provider; return provider;
} }
public static ViewOutlineProvider topRoundedRect(int dp){
ViewOutlineProvider provider=topRoundedRects.get(dp);
if(provider!=null)
return provider;
provider=new TopRoundRectOutlineProvider(V.dp(dp));
topRoundedRects.put(dp, provider);
return provider;
}
public static ViewOutlineProvider endRoundedRect(int dp){
ViewOutlineProvider provider=endRoundedRects.get(dp);
if(provider!=null)
return provider;
provider=new EndRoundRectOutlineProvider(V.dp(dp));
endRoundedRects.put(dp, provider);
return provider;
}
private static class RoundRectOutlineProvider extends ViewOutlineProvider{ private static class RoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius; private final int radius;
@@ -43,4 +75,34 @@ public class OutlineProviders{
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius); outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
} }
} }
private static class TopRoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius;
private TopRoundRectOutlineProvider(int radius){
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+radius, radius);
}
}
private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius;
private EndRoundRectOutlineProvider(int radius){
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
if(view.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL){
outline.setRoundRect(-radius, 0, view.getWidth(), view.getHeight(), radius);
}else{
outline.setRoundRect(0, 0, view.getWidth()+radius, view.getHeight(), radius);
}
}
}
} }

View File

@@ -131,6 +131,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID()); args.putString("account", item.parentFragment.getAccountID());
args.putString("id", item.status.id); args.putString("id", item.status.id);
args.putString("url", item.status.url);
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args); Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
} }
} }

View File

@@ -134,12 +134,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(reply, item.status.repliesCount); bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount); bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount); bindButton(favorite, item.status.favouritesCount);
reply.setSelected(item.status.repliesCount > 0); // in thread view, direct descendant posts display one direct reply to themselves,
// hence in that case displaying whether there is another reply
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor() ? 0 : 1;
reply.setSelected(item.status.repliesCount > compareTo);
boost.setSelected(item.status.reblogged); boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited); favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked); bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id))); || (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor() &&
!nextIsWarning;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0);
itemView.requestLayout();
} }
private void bindButton(TextView btn, long count){ private void bindButton(TextView btn, long count){

View File

@@ -1,11 +1,9 @@
package org.joinmastodon.android.ui.displayitems; package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@@ -24,8 +22,6 @@ import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.StringRes;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
@@ -36,7 +32,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.ListTimelinesFragment; import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment; import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
@@ -44,12 +40,10 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement; import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -59,7 +53,6 @@ import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -284,7 +277,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putString("account", item.parentFragment.getAccountID()); args.putString("account", item.parentFragment.getAccountID());
args.putString("profileAccount", account.id); args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername()); args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(item.parentFragment.getActivity(), ListTimelinesFragment.class, args); Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
} }
return true; return true;
}); });

View File

@@ -10,7 +10,6 @@ import android.view.ViewGroup;
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.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment;
@@ -22,7 +21,6 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.ScheduledStatus;
@@ -49,6 +47,32 @@ public abstract class StatusDisplayItem{
public final BaseStatusListFragment parentFragment; public final BaseStatusListFragment parentFragment;
public boolean inset; public boolean inset;
public int index; public int index;
private ThreadFragment.NeighborAncestryInfo ancestryInfo;
public boolean
isMainStatus = true,
isDirectDescendant = false;
public boolean hasDescendantNeighbor() {
return Optional.ofNullable(ancestryInfo)
.map(ThreadFragment.NeighborAncestryInfo::hasDescendantNeighbor)
.orElse(false);
}
public boolean hasAncestoringNeighbor() {
return Optional.ofNullable(ancestryInfo)
.map(ThreadFragment.NeighborAncestryInfo::hasAncestoringNeighbor)
.orElse(false);
}
public void setAncestryInfo(
ThreadFragment.NeighborAncestryInfo ancestryInfo,
boolean isMainStatus,
boolean isDirectDescendant
) {
this.ancestryInfo = ancestryInfo;
this.isMainStatus = isMainStatus;
this.isDirectDescendant = isDirectDescendant;
}
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){ public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID; this.parentID=parentID;

View File

@@ -237,6 +237,15 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand); readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE); spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
// remove additional padding when (transparently padded) translate button is visible
int pos = getAbsoluteAdapterPosition();
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(),
(translateVisible &&
item.parentFragment.getDisplayItems().size() >= pos + 1 &&
item.parentFragment.getDisplayItems().get(pos + 1) instanceof FooterStatusDisplayItem)
? 0 : V.dp(12)
);
if (!GlobalUserPreferences.collapseLongPosts) { if (!GlobalUserPreferences.collapseLongPosts) {
textScrollView.setLayoutParams(wrapParams); textScrollView.setLayoutParams(wrapParams);
readMore.setVisibility(View.GONE); readMore.setVisibility(View.GONE);

View File

@@ -7,6 +7,7 @@ import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Fragment;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ClipData; import android.content.ClipData;
@@ -110,6 +111,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@@ -948,8 +950,8 @@ public class UiUtils {
public static String getInstanceName(String accountID) { public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID); AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = session.getInstance(); Optional<Instance> instance = session.getInstance();
return instance != null && !instance.title.isBlank() ? instance.title : session.domain; return instance.isPresent() && !instance.get().title.isBlank() ? instance.get().title : session.domain;
} }
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) { public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
@@ -1080,6 +1082,13 @@ public class UiUtils {
} }
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) -> {
if (clazz == null) return;
Nav.go((Activity) context, clazz, args);
});
}
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, 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())) {
@@ -1091,13 +1100,14 @@ public class UiUtils {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result)); args.putParcelable("status", Parcels.wrap(result));
Nav.go((Activity) context, ThreadFragment.class, args); go.accept(ThreadFragment.class, args);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
error.showToast(context); error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url); if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
} }
}) })
.wrapProgress((Activity) context, R.string.loading, true, .wrapProgress((Activity) context, R.string.loading, true,
@@ -1113,27 +1123,26 @@ public class UiUtils {
args.putString("account", accountID); args.putString("account", accountID);
if (!results.statuses.isEmpty()) { if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0))); args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args); go.accept(ThreadFragment.class, args);
return; return;
} }
Optional<Account> account = results.accounts.stream() Optional<Account> account = results.accounts.stream()
.filter(a -> uri.equals(Uri.parse(a.url))).findAny(); .filter(a -> uri.equals(Uri.parse(a.url))).findAny();
if (account.isPresent()) { if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get())); args.putParcelable("profileAccount", Parcels.wrap(account.get()));
Nav.go((Activity) context, ProfileFragment.class, args); go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) {
launchWebBrowser(context, url);
return; return;
} }
if (launchBrowser) launchWebBrowser(context, url);
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();
go.accept(null, null);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
error.showToast(context); error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url); if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
} }
}) })
.wrapProgress((Activity) context, R.string.loading, true, .wrapProgress((Activity) context, R.string.loading, true,
@@ -1142,7 +1151,8 @@ public class UiUtils {
return; return;
} }
} }
launchWebBrowser(context, url); if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
} }
public static void copyText(View v, String text) { public static void copyText(View v, String text) {

View File

@@ -0,0 +1,62 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.RelativeLayout;
public class CheckableRelativeLayout extends RelativeLayout implements Checkable{
private boolean checked, checkable = true;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public CheckableRelativeLayout(Context context){
this(context, null);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
public void setChecked(boolean checked){
this.checked=checked;
refreshDrawableState();
}
public void setCheckable(boolean checkable) {
this.checkable = checkable;
}
@Override
public boolean isChecked(){
return checked;
}
@Override
public void toggle(){
setChecked(!checked);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(info);
info.setCheckable(checkable);
info.setChecked(checked);
}
}

View File

@@ -0,0 +1,32 @@
package org.joinmastodon.android.utils;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.net.Uri;
import org.joinmastodon.android.fragments.HasAccountID;
public interface ProvidesAssistContent {
void onProvideAssistContent(AssistContent assistContent);
default boolean callFragmentToProvideAssistContent(Fragment fragment, AssistContent assistContent) {
if (fragment instanceof ProvidesAssistContent assistiveFragment) {
assistiveFragment.onProvideAssistContent(assistContent);
return true;
} else {
return false;
}
}
interface ProvidesWebUri extends ProvidesAssistContent, HasAccountID {
Uri getWebUri(Uri.Builder base);
default Uri.Builder getUriBuilder() {
return getSession().getInstanceUri().buildUpon();
}
default void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getUriBuilder()));
}
}
}

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="28dp" android:height="28dp" android:viewportWidth="28" android:viewportHeight="28">
<path android:pathData="M18.27 3.21l7.5 7.25c0.073 0.07 0.13 0.154 0.17 0.247C25.98 10.8 26 10.9 26 11c0 0.101-0.02 0.201-0.06 0.294-0.04 0.093-0.097 0.176-0.17 0.246l-7.5 7.25c-0.069 0.068-0.15 0.121-0.239 0.157-0.09 0.037-0.185 0.055-0.281 0.053-0.1 0-0.198-0.02-0.29-0.06-0.136-0.056-0.252-0.152-0.334-0.275C17.044 18.542 17 18.398 17 18.25v-3.74c-6.7 0.27-9.52 4.02-9.64 4.18-0.096 0.126-0.227 0.22-0.378 0.268-0.15 0.048-0.311 0.049-0.462 0.003-0.15-0.049-0.282-0.144-0.375-0.271C6.052 18.562 6 18.408 6 18.25c0-8.02 6.59-10.48 11-10.73V3.75c0-0.147 0.044-0.291 0.126-0.414s0.198-0.218 0.334-0.275c0.135-0.059 0.284-0.075 0.428-0.049 0.144 0.027 0.277 0.096 0.382 0.199zm0.23 10.54v2.71L24.17 11 18.5 5.52v2.73c-0.003 0.199-0.082 0.388-0.223 0.528-0.14 0.14-0.329 0.22-0.527 0.223-0.97 0-8.85 0.22-10.09 7.28 2.876-2.24 6.447-3.401 10.09-3.28 0.198 0.002 0.387 0.082 0.527 0.222s0.22 0.33 0.223 0.527zm4.223 5.473c0.14-0.14 0.329-0.22 0.527-0.223 0.198 0.003 0.387 0.083 0.527 0.223s0.22 0.33 0.223 0.527v0.5c0 1.26-0.5 2.468-1.391 3.36-0.891 0.89-2.1 1.39-3.359 1.39H7.75c-1.26 0-2.468-0.5-3.359-1.39C3.501 22.717 3 21.51 3 20.25V8.75c0-1.26 0.5-2.468 1.391-3.358C5.282 4.5 6.491 4 7.75 4h4.5c0.199 0 0.39 0.08 0.53 0.22C12.921 4.36 13 4.552 13 4.75c0 0.2-0.079 0.39-0.22 0.53-0.14 0.141-0.331 0.22-0.53 0.22h-4.5C6.889 5.503 6.064 5.846 5.455 6.455 4.845 7.065 4.503 7.89 4.5 8.751v11.5c0.003 0.86 0.346 1.686 0.955 2.295S6.889 23.498 7.75 23.5h11.5c0.861-0.002 1.686-0.345 2.295-0.954 0.61-0.61 0.952-1.434 0.955-2.296v-0.5c0.003-0.198 0.082-0.387 0.223-0.527z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -1,44 +1,82 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.joinmastodon.android.ui.views.CheckableRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="wrap_content"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <ImageView
android:id="@+id/avatar" android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp"
android:layout_alignParentStart="true"
android:layout_centerInParent="true"
android:importantForAccessibility="no"/>
<View
android:id="@+id/radiobtn"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:importantForAccessibility="no"/> android:layout_centerInParent="true"
android:layout_toStartOf="@+id/extra_btn_wrap"
android:layout_alignWithParentIfMissing="true"
android:layout_marginEnd="20dp"
android:layout_marginStart="12dp"
android:duplicateParentState="true" />
<FrameLayout
android:id="@id/extra_btn_wrap"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_alignParentEnd="true"
android:layout_marginStart="12dp"
android:visibility="gone">
<View
android:layout_width="1dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical|start"
android:background="?colorPollVoted" />
<ImageButton
android:id="@+id/extra_btn"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:contentDescription="@string/sk_open_in_app"
android:tooltipText="@string/sk_open_in_app"
android:src="@drawable/ic_fluent_open_24_regular"
android:background="?android:selectableItemBackground" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/radiobtn"
android:layout_centerInParent="true"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/name" android:id="@+id/name"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:textAppearance="@style/m3_body_large"
android:layout_marginStart="24dp" android:textColor="?colorM3OnSurface"
android:textSize="16sp" android:gravity="center_vertical"
android:textColor="?android:textColorPrimary"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end"/> android:ellipsize="end"/>
<View <TextView
android:id="@+id/current" android:id="@+id/username"
android:layout_width="24dp" android:layout_width="match_parent"
android:layout_height="24dp" android:layout_height="wrap_content"
android:background="@drawable/ic_fluent_checkmark_24_filled" android:textColor="?colorM3OnSurfaceVariant"
android:backgroundTint="?android:textColorSecondary" android:textAppearance="@style/m3_body_medium"
android:contentDescription="@string/current_account"/> android:singleLine="true"
android:gravity="center_vertical"
android:ellipsize="end"/>
<ImageButton </LinearLayout>
android:id="@+id/more"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_fluent_more_vertical_24_regular"
android:tint="?android:textColorSecondary"
android:contentDescription="@string/more_options"
android:background="?android:selectableItemBackgroundBorderless"/>
</LinearLayout> </org.joinmastodon.android.ui.views.CheckableRelativeLayout>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/icon"
android:src="@drawable/ic_fluent_share_28_regular"
android:scaleType="centerInside"
android:adjustViewBounds="true"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginHorizontal="16dp"
android:importantForAccessibility="no"
tools:ignore="RtlSymmetry" />
<TextView
style="@style/sheet_title"
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sk_external_share_or_open_title"
android:textColor="?colorM3OnSurface" />
</LinearLayout>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurface"
android:textAppearance="@style/m3_body_large"
android:singleLine="true"
android:ellipsize="end"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:drawablePadding="24dp"
android:drawableTint="?colorM3OnSurfaceVariant"
tools:text="List Item"/>

View File

@@ -88,6 +88,7 @@
<item quantity="one">%d জন ব্যক্তি বলছেন</item> <item quantity="one">%d জন ব্যক্তি বলছেন</item>
<item quantity="other">%d jon ব্যক্তিরা বলছেন</item> <item quantity="other">%d jon ব্যক্তিরা বলছেন</item>
</plurals> </plurals>
<string name="sending_report">রিপোর্ট পাঠানো হচ্ছে…</string>
<!-- %s is the email address --> <!-- %s is the email address -->
<!-- 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 -->
<!-- %s is version like 1.2.3 --> <!-- %s is version like 1.2.3 -->
@@ -96,4 +97,8 @@
<!-- %s is server domain --> <!-- %s is server domain -->
<!-- Shown in a progress dialog when you tap "follow all" --> <!-- Shown in a progress dialog when you tap "follow all" -->
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. --> <!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="welcome_to_mastodon">Mastodon - এ আপনাকে স্বাগত জানাই</string>
<string name="welcome_paragraph1">Mastodon হল একটি বিকেন্দ্রীভূত সামাজিক নেটওয়ার্ক, যার মানে কোনো একক কোম্পানি এটিকে নিয়ন্ত্রণ করে না। এটি অনেকগুলি স্বাধীনভাবে চালিত সার্ভারের সমন্বয়ে গঠিত, যেখানে সব সার্ভারগুলি একসাথে সংযুক্ত৷</string>
<string name="what_are_servers">সার্ভার কি?</string>
<string name="welcome_paragraph2"><![CDATA[প্রতিটি Mastodon অ্যাকাউন্টকে একটি সার্ভারে হোস্ট করা হয় — প্রত্যেকটির নিজস্ব মান, নিয়ম এবং প্রশাসক (অ্যাডমিন) রয়েছে। আপনি যে কোনো সার্ভারই বেছে নিন না কেন তা বিবেচ্য নয়, আপনি যেকোনো সার্ভারের লোকেদের সাথে যোগাযোগ করতে এবং তাদের ফলো করতে পারেন।]]></string>
</resources> </resources>

View File

@@ -438,8 +438,11 @@
<string name="show">Anzeigen</string> <string name="show">Anzeigen</string>
<string name="hide">Ausblenden</string> <string name="hide">Ausblenden</string>
<string name="join_default_server">%s beitreten</string> <string name="join_default_server">%s beitreten</string>
<string name="pick_server">Wähle einen anderen Server</string>
<string name="signup_or_login">oder</string> <string name="signup_or_login">oder</string>
<string name="learn_more">Mehr erfahren</string> <string name="learn_more">Mehr erfahren</string>
<string name="welcome_to_mastodon">Willkommen auf Mastodon</string> <string name="welcome_to_mastodon">Willkommen auf Mastodon</string>
<string name="welcome_paragraph1">Mastodon ist ein dezentrales, soziales Netzwerk. Das bedeutet, dass es nicht von einem einzigen Unternehmen kontrolliert wird. Das Netzwerk besteht aus unabhängig voneinander betriebenen Servern, die miteinander verbunden sind.</string>
<string name="what_are_servers">Was sind Server?</string> <string name="what_are_servers">Was sind Server?</string>
<string name="welcome_paragraph2"><![CDATA[Jedes Mastodon-Konto wird auf einem Server gehostet. Jeder Server hat dabei seine eigenen Werte, Regeln und Administrator*innen. Aber egal, für welchen Server Du Dich entscheidest: Du kannst mit Leuten von anderen Servern interagieren und ihnen folgen.]]></string>
</resources> </resources>

View File

@@ -287,4 +287,10 @@
<string name="sk_content_type_unspecified">Non spécifié</string> <string name="sk_content_type_unspecified">Non spécifié</string>
<string name="sk_settings_content_types_explanation">Permet de définir un type de contenu comme Markdown lors de la création d\'un message. Gardez à l\'esprit que toutes les instances ne le prennent pas en charge.</string> <string name="sk_settings_content_types_explanation">Permet de définir un type de contenu comme Markdown lors de la création d\'un message. Gardez à l\'esprit que toutes les instances ne le prennent pas en charge.</string>
<string name="sk_settings_default_content_type">Type de contenu par défaut</string> <string name="sk_settings_default_content_type">Type de contenu par défaut</string>
<string name="sk_open_in_app">Ouvrir dans l\'application</string>
<string name="sk_external_share_title">Partager avec le compte</string>
<string name="sk_external_share_or_open_title">Partager ou ouvrir avec le compte</string>
<string name="sk_bubble_timeline_info_banner">Ce sont les publications les plus récentes des personnes présentes dans la bulle de votre serveur Akkoma.</string>
<string name="sk_timeline_bubble">Bulle</string>
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
</resources> </resources>

View File

@@ -275,4 +275,15 @@
<string name="sk_settings_confirm_before_reblog">Confirma antes de impulsar</string> <string name="sk_settings_confirm_before_reblog">Confirma antes de impulsar</string>
<string name="sk_reacted_with">Redactado con %s</string> <string name="sk_reacted_with">Redactado con %s</string>
<string name="sk_reacted">redactado</string> <string name="sk_reacted">redactado</string>
<string name="sk_content_type_unspecified">Non especificado</string>
<string name="sk_content_type_plain">Texto plano</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Activar o formato de publicacións</string>
<string name="sk_settings_default_content_type">Tipo de contido por defecto</string>
<string name="sk_settings_default_content_type_explanation">Isto permítelle ter un tipo de contido preseleccionado á hora de crear novas publicacións, sobrescribindo o valor establecido en \"Publicar preferencias\".</string>
<string name="sk_content_type">Tipo de contido</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_settings_content_types_explanation">Permite configurar un tipo de contido como Markdown ao crear unha publicación. Teña en conta que non tódalas instancias soportan isto.</string>
</resources> </resources>

View File

@@ -287,4 +287,10 @@
<string name="sk_content_type">Jenis konten</string> <string name="sk_content_type">Jenis konten</string>
<string name="sk_content_type_unspecified">Tidak ditentukan</string> <string name="sk_content_type_unspecified">Tidak ditentukan</string>
<string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string> <string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string>
<string name="sk_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yamg paling terkini oleh orang-orang dalam gelembung server Akkoma Anda.</string>
<string name="sk_timeline_bubble">Gelembung</string>
<string name="sk_instance_info_unavailable">Info server sementara tidak tersedia</string>
<string name="sk_external_share_or_open_title">Bagikan atau buka dengan akun</string>
</resources> </resources>

View File

@@ -251,4 +251,7 @@
<string name="sk_settings_hide_interaction">Verberg interactie knoppen</string> <string name="sk_settings_hide_interaction">Verberg interactie knoppen</string>
<string name="sk_follow_as">Volgen met ander account</string> <string name="sk_follow_as">Volgen met ander account</string>
<string name="sk_followed_as">Gevolgd met %s</string> <string name="sk_followed_as">Gevolgd met %s</string>
<string name="sk_quoting_user">Quoting %s</string>
<string name="sk_settings_reply_visibility">Zichtbaarheid reactie</string>
<string name="sk_settings_reply_visibility_all">Alle reacties</string>
</resources> </resources>

View File

@@ -286,4 +286,10 @@
<string name="sk_settings_default_content_type_explanation">Це дозволяє вам попередньо вибрати тип вмісту під час написання нових дописів, замінивши значення, встановлене в «Налаштуваннях постингу».</string> <string name="sk_settings_default_content_type_explanation">Це дозволяє вам попередньо вибрати тип вмісту під час написання нових дописів, замінивши значення, встановлене в «Налаштуваннях постингу».</string>
<string name="sk_content_type_markdown">Markdown</string> <string name="sk_content_type_markdown">Markdown</string>
<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_external_share_title">Поділитися через обліковий запис</string>
<string name="sk_bubble_timeline_info_banner">Це найновіші дописи людей у бульбашці вашого сервера Akkoma.</string>
<string name="sk_timeline_bubble">Бульбашка</string>
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</string>
</resources> </resources>

View File

@@ -4,32 +4,57 @@
<color name="m3_navigation_bar_bg">@android:color/system_neutral1_50</color> <color name="m3_navigation_bar_bg">@android:color/system_neutral1_50</color>
<color name="m3_gray_900">@android:color/system_neutral1_900</color> <color name="m3_neutral1_900">@android:color/system_neutral1_900</color>
<color name="m3_gray_800t">@android:color/system_neutral1_800</color> <color name="m3_neutral1_800t">@android:color/system_neutral1_800</color>
<color name="m3_gray_800">@android:color/system_neutral1_800</color> <color name="m3_neutral1_800">@android:color/system_neutral1_800</color>
<color name="m3_gray_700">@android:color/system_neutral1_700</color> <color name="m3_neutral1_700">@android:color/system_neutral1_700</color>
<color name="m3_gray_600">@android:color/system_neutral1_600</color> <color name="m3_neutral1_600">@android:color/system_neutral1_600</color>
<color name="m3_gray_500">@android:color/system_neutral1_500</color> <color name="m3_neutral1_500">@android:color/system_neutral1_500</color>
<color name="m3_gray_400">@android:color/system_neutral1_400</color> <color name="m3_neutral1_400">@android:color/system_neutral1_400</color>
<color name="m3_gray_300">@android:color/system_neutral1_300</color> <color name="m3_neutral1_300">@android:color/system_neutral1_300</color>
<color name="m3_gray_200">@android:color/system_neutral1_200</color> <color name="m3_neutral1_200">@android:color/system_neutral1_200</color>
<color name="m3_gray_100">@android:color/system_neutral1_100</color> <color name="m3_neutral1_100">@android:color/system_neutral1_100</color>
<color name="m3_gray_50t">@android:color/system_neutral1_50</color> <color name="m3_neutral1_50t">@android:color/system_neutral1_50</color>
<color name="m3_gray_50">@android:color/system_neutral1_50</color> <color name="m3_neutral1_50">@android:color/system_neutral1_50</color>
<color name="m3_gray_25">@android:color/system_neutral1_10</color> <color name="m3_neutral1_25">@android:color/system_neutral1_10</color>
<color name="m3_primary_25">@android:color/system_accent1_10</color> <color name="m3_accent1_25">@android:color/system_accent1_10</color>
<color name="m3_primary_50">@android:color/system_accent1_50</color> <color name="m3_accent1_50">@android:color/system_accent1_50</color>
<color name="m3_primary_100">@android:color/system_accent1_100</color> <color name="m3_accent1_100">@android:color/system_accent1_100</color>
<color name="m3_primary_200">@android:color/system_accent1_200</color> <color name="m3_accent1_200">@android:color/system_accent1_200</color>
<color name="m3_primary_300">@android:color/system_accent1_300</color> <color name="m3_accent1_300">@android:color/system_accent1_300</color>
<color name="m3_primary_400">@android:color/system_accent1_400</color> <color name="m3_accent1_400">@android:color/system_accent1_400</color>
<color name="m3_primary_500">@android:color/system_accent1_500</color> <color name="m3_accent1_500">@android:color/system_accent1_500</color>
<color name="m3_primary_600">@android:color/system_accent1_600</color> <color name="m3_accent1_600">@android:color/system_accent1_600</color>
<color name="m3_primary_700">@android:color/system_accent1_700</color> <color name="m3_accent1_700">@android:color/system_accent1_700</color>
<color name="m3_primary_800">@android:color/system_accent1_800</color> <color name="m3_accent1_800">@android:color/system_accent1_800</color>
<color name="m3_primary_900">@android:color/system_accent1_900</color> <color name="m3_accent1_900">@android:color/system_accent1_900</color>
<color name="m3_neutral2_900">@android:color/system_neutral2_900</color>
<color name="m3_neutral2_800t">@android:color/system_neutral2_800</color>
<color name="m3_neutral2_800">@android:color/system_neutral2_800</color>
<color name="m3_neutral2_700">@android:color/system_neutral2_700</color>
<color name="m3_neutral2_600">@android:color/system_neutral2_600</color>
<color name="m3_neutral2_500">@android:color/system_neutral2_500</color>
<color name="m3_neutral2_400">@android:color/system_neutral2_400</color>
<color name="m3_neutral2_300">@android:color/system_neutral2_300</color>
<color name="m3_neutral2_200">@android:color/system_neutral2_200</color>
<color name="m3_neutral2_100">@android:color/system_neutral2_100</color>
<color name="m3_neutral2_50t">@android:color/system_neutral2_50</color>
<color name="m3_neutral2_50">@android:color/system_neutral2_50</color>
<color name="m3_neutral2_25">@android:color/system_neutral2_10</color>
<color name="m3_accent2_25">@android:color/system_accent2_10</color>
<color name="m3_accent2_50">@android:color/system_accent2_50</color>
<color name="m3_accent2_100">@android:color/system_accent2_100</color>
<color name="m3_accent2_200">@android:color/system_accent2_200</color>
<color name="m3_accent2_300">@android:color/system_accent2_300</color>
<color name="m3_accent2_400">@android:color/system_accent2_400</color>
<color name="m3_accent2_500">@android:color/system_accent2_500</color>
<color name="m3_accent2_600">@android:color/system_accent2_600</color>
<color name="m3_accent2_700">@android:color/system_accent2_700</color>
<color name="m3_accent2_800">@android:color/system_accent2_800</color>
<color name="m3_accent2_900">@android:color/system_accent2_900</color>
<!-- light theme --> <!-- light theme -->
<color name="m3_sys_light_primary">@android:color/system_accent1_600</color> <color name="m3_sys_light_primary">@android:color/system_accent1_600</color>

View File

@@ -106,4 +106,42 @@
<attr name="colorGray800" format="color" /> <attr name="colorGray800" format="color" />
<attr name="colorGray800t" format="color" /> <attr name="colorGray800t" format="color" />
<attr name="colorGray900" format="color" /> <attr name="colorGray900" format="color" />
<attr name="colorSecondary25" format="color" />
<attr name="colorSecondary50" format="color" />
<attr name="colorSecondary100" format="color" />
<attr name="colorSecondary200" format="color" />
<attr name="colorSecondary300" format="color" />
<attr name="colorSecondary400" format="color" />
<attr name="colorSecondary500" format="color" />
<attr name="colorSecondary600" format="color" />
<attr name="colorSecondary700" format="color" />
<attr name="colorSecondary800" format="color" />
<attr name="colorSecondary900" format="color" />
<attr name="colorTertiary25" format="color" />
<attr name="colorTertiary50" format="color" />
<attr name="colorTertiary100" format="color" />
<attr name="colorTertiary200" format="color" />
<attr name="colorTertiary300" format="color" />
<attr name="colorTertiary400" format="color" />
<attr name="colorTertiary500" format="color" />
<attr name="colorTertiary600" format="color" />
<attr name="colorTertiary700" format="color" />
<attr name="colorTertiary800" format="color" />
<attr name="colorTertiary900" format="color" />
<attr name="colorNeutral25" format="color" />
<attr name="colorNeutral50" format="color" />
<attr name="colorNeutral50t" format="color" />
<attr name="colorNeutral100" format="color" />
<attr name="colorNeutral200" format="color" />
<attr name="colorNeutral300" format="color" />
<attr name="colorNeutral400" format="color" />
<attr name="colorNeutral500" format="color" />
<attr name="colorNeutral600" format="color" />
<attr name="colorNeutral700" format="color" />
<attr name="colorNeutral800" format="color" />
<attr name="colorNeutral800t" format="color" />
<attr name="colorNeutral900" format="color" />
</resources> </resources>

View File

@@ -103,31 +103,69 @@
<!-- M3 dynamic colors --> <!-- M3 dynamic colors -->
<color name="m3_navigation_bar_bg">@color/gray_50</color> <color name="m3_navigation_bar_bg">@color/gray_50</color>
<color name="m3_gray_900">@color/gray_900</color> <color name="m3_neutral1_900">@color/gray_900</color>
<color name="m3_gray_800t">@color/gray_800t</color> <color name="m3_neutral1_800t">@color/gray_800t</color>
<color name="m3_gray_800">@color/gray_800</color> <color name="m3_neutral1_800">@color/gray_800</color>
<color name="m3_gray_700">@color/gray_700</color> <color name="m3_neutral1_700">@color/gray_700</color>
<color name="m3_gray_600">@color/gray_600</color> <color name="m3_neutral1_600">@color/gray_600</color>
<color name="m3_gray_500">@color/gray_500</color> <color name="m3_neutral1_500">@color/gray_500</color>
<color name="m3_gray_400">@color/gray_400</color> <color name="m3_neutral1_400">@color/gray_400</color>
<color name="m3_gray_300">@color/gray_300</color> <color name="m3_neutral1_300">@color/gray_300</color>
<color name="m3_gray_200">@color/gray_200</color> <color name="m3_neutral1_200">@color/gray_200</color>
<color name="m3_gray_100">@color/gray_100</color> <color name="m3_neutral1_100">@color/gray_100</color>
<color name="m3_gray_50t">@color/gray_50t</color> <color name="m3_neutral1_50t">@color/gray_50t</color>
<color name="m3_gray_50">@color/gray_50</color> <color name="m3_neutral1_50">@color/gray_50</color>
<color name="m3_gray_25">@color/gray_25</color> <color name="m3_neutral1_25">@color/gray_25</color>
<color name="m3_primary_25">@color/primary_25</color> <color name="m3_neutral2_900">@color/gray_900</color>
<color name="m3_primary_50">@color/primary_50</color> <color name="m3_neutral2_800t">@color/gray_800t</color>
<color name="m3_primary_100">@color/primary_100</color> <color name="m3_neutral2_800">@color/gray_800</color>
<color name="m3_primary_200">@color/primary_200</color> <color name="m3_neutral2_700">@color/gray_700</color>
<color name="m3_primary_300">@color/primary_300</color> <color name="m3_neutral2_600">@color/gray_600</color>
<color name="m3_primary_400">@color/primary_400</color> <color name="m3_neutral2_500">@color/gray_500</color>
<color name="m3_primary_500">@color/primary_500</color> <color name="m3_neutral2_400">@color/gray_400</color>
<color name="m3_primary_600">@color/primary_600</color> <color name="m3_neutral2_300">@color/gray_300</color>
<color name="m3_primary_700">@color/primary_700</color> <color name="m3_neutral2_200">@color/gray_200</color>
<color name="m3_primary_800">@color/primary_800</color> <color name="m3_neutral2_100">@color/gray_100</color>
<color name="m3_primary_900">@color/primary_900</color> <color name="m3_neutral2_50t">@color/gray_50t</color>
<color name="m3_neutral2_50">@color/gray_50</color>
<color name="m3_neutral2_25">@color/gray_25</color>
<color name="m3_accent1_25">@color/primary_25</color>
<color name="m3_accent1_50">@color/primary_50</color>
<color name="m3_accent1_100">@color/primary_100</color>
<color name="m3_accent1_200">@color/primary_200</color>
<color name="m3_accent1_300">@color/primary_300</color>
<color name="m3_accent1_400">@color/primary_400</color>
<color name="m3_accent1_500">@color/primary_500</color>
<color name="m3_accent1_600">@color/primary_600</color>
<color name="m3_accent1_700">@color/primary_700</color>
<color name="m3_accent1_800">@color/primary_800</color>
<color name="m3_accent1_900">@color/primary_900</color>
<color name="m3_accent2_25">@color/primary_25</color>
<color name="m3_accent2_50">@color/primary_50</color>
<color name="m3_accent2_100">@color/primary_100</color>
<color name="m3_accent2_200">@color/primary_200</color>
<color name="m3_accent2_300">@color/primary_300</color>
<color name="m3_accent2_400">@color/primary_400</color>
<color name="m3_accent2_500">@color/primary_500</color>
<color name="m3_accent2_600">@color/primary_600</color>
<color name="m3_accent2_700">@color/primary_700</color>
<color name="m3_accent2_800">@color/primary_800</color>
<color name="m3_accent2_900">@color/primary_900</color>
<color name="m3_accent3_25">@color/primary_25</color>
<color name="m3_accent3_50">@color/primary_50</color>
<color name="m3_accent3_100">@color/primary_100</color>
<color name="m3_accent3_200">@color/primary_200</color>
<color name="m3_accent3_300">@color/primary_300</color>
<color name="m3_accent3_400">@color/primary_400</color>
<color name="m3_accent3_500">@color/primary_500</color>
<color name="m3_accent3_600">@color/primary_600</color>
<color name="m3_accent3_700">@color/primary_700</color>
<color name="m3_accent3_800">@color/primary_800</color>
<color name="m3_accent3_900">@color/primary_900</color>
<!-- light theme --> <!-- light theme -->
<color name="m3_sys_light_primary">#6750A4</color> <color name="m3_sys_light_primary">#6750A4</color>

View File

@@ -26,34 +26,115 @@
<item name="colorGray50t">@color/gray_50t</item> <item name="colorGray50t">@color/gray_50t</item>
<item name="colorGray50">@color/gray_50</item> <item name="colorGray50">@color/gray_50</item>
<item name="colorGray25">@color/gray_25</item> <item name="colorGray25">@color/gray_25</item>
<!--
custom themes generally don't have secondary/tertiary accent colors -
falling back to primary colors
-->
<item name="colorSecondary25">@color/primary_25</item>
<item name="colorSecondary50">@color/primary_50</item>
<item name="colorSecondary100">@color/primary_100</item>
<item name="colorSecondary200">@color/primary_200</item>
<item name="colorSecondary300">@color/primary_300</item>
<item name="colorSecondary400">@color/primary_400</item>
<item name="colorSecondary500">@color/primary_500</item>
<item name="colorSecondary600">@color/primary_600</item>
<item name="colorSecondary700">@color/primary_700</item>
<item name="colorSecondary800">@color/primary_800</item>
<item name="colorSecondary900">@color/primary_900</item>
<item name="colorTertiary25">@color/primary_25</item>
<item name="colorTertiary50">@color/primary_50</item>
<item name="colorTertiary100">@color/primary_100</item>
<item name="colorTertiary200">@color/primary_200</item>
<item name="colorTertiary300">@color/primary_300</item>
<item name="colorTertiary400">@color/primary_400</item>
<item name="colorTertiary500">@color/primary_500</item>
<item name="colorTertiary600">@color/primary_600</item>
<item name="colorTertiary700">@color/primary_700</item>
<item name="colorTertiary800">@color/primary_800</item>
<item name="colorTertiary900">@color/primary_900</item>
<item name="colorNeutral900">@color/gray_900</item>
<item name="colorNeutral800t">@color/gray_800t</item>
<item name="colorNeutral800">@color/gray_800</item>
<item name="colorNeutral700">@color/gray_700</item>
<item name="colorNeutral600">@color/gray_600</item>
<item name="colorNeutral500">@color/gray_500</item>
<item name="colorNeutral400">@color/gray_400</item>
<item name="colorNeutral300">@color/gray_300</item>
<item name="colorNeutral200">@color/gray_200</item>
<item name="colorNeutral100">@color/gray_100</item>
<item name="colorNeutral50t">@color/gray_50t</item>
<item name="colorNeutral50">@color/gray_50</item>
<item name="colorNeutral25">@color/gray_25</item>
</style> </style>
<style name="ColorPalette.Material3"> <style name="ColorPalette.Material3">
<item name="colorPrimary25">@color/m3_primary_25</item> <item name="colorPrimary25">@color/m3_accent1_25</item>
<item name="colorPrimary50">@color/m3_primary_50</item> <item name="colorPrimary50">@color/m3_accent1_50</item>
<item name="colorPrimary100">@color/m3_primary_100</item> <item name="colorPrimary100">@color/m3_accent1_100</item>
<item name="colorPrimary200">@color/m3_primary_200</item> <item name="colorPrimary200">@color/m3_accent1_200</item>
<item name="colorPrimary300">@color/m3_primary_300</item> <item name="colorPrimary300">@color/m3_accent1_300</item>
<item name="colorPrimary400">@color/m3_primary_400</item> <item name="colorPrimary400">@color/m3_accent1_400</item>
<item name="colorPrimary500">@color/m3_primary_500</item> <item name="colorPrimary500">@color/m3_accent1_500</item>
<item name="colorPrimary600">@color/m3_primary_600</item> <item name="colorPrimary600">@color/m3_accent1_600</item>
<item name="colorPrimary700">@color/m3_primary_700</item> <item name="colorPrimary700">@color/m3_accent1_700</item>
<item name="colorPrimary800">@color/m3_primary_800</item> <item name="colorPrimary800">@color/m3_accent1_800</item>
<item name="colorPrimary900">@color/m3_primary_900</item> <item name="colorPrimary900">@color/m3_accent1_900</item>
<item name="colorGray900">@color/m3_gray_900</item> <item name="colorSecondary25">@color/m3_accent2_25</item>
<item name="colorGray800t">@color/m3_gray_800t</item> <item name="colorSecondary50">@color/m3_accent2_50</item>
<item name="colorGray800">@color/m3_gray_800</item> <item name="colorSecondary100">@color/m3_accent2_100</item>
<item name="colorGray700">@color/m3_gray_700</item> <item name="colorSecondary200">@color/m3_accent2_200</item>
<item name="colorGray600">@color/m3_gray_600</item> <item name="colorSecondary300">@color/m3_accent2_300</item>
<item name="colorGray500">@color/m3_gray_500</item> <item name="colorSecondary400">@color/m3_accent2_400</item>
<item name="colorGray400">@color/m3_gray_400</item> <item name="colorSecondary500">@color/m3_accent2_500</item>
<item name="colorGray300">@color/m3_gray_300</item> <item name="colorSecondary600">@color/m3_accent2_600</item>
<item name="colorGray200">@color/m3_gray_200</item> <item name="colorSecondary700">@color/m3_accent2_700</item>
<item name="colorGray100">@color/m3_gray_100</item> <item name="colorSecondary800">@color/m3_accent2_800</item>
<item name="colorGray50t">@color/m3_gray_50t</item> <item name="colorSecondary900">@color/m3_accent2_900</item>
<item name="colorGray50">@color/m3_gray_50</item>
<item name="colorGray25">@color/m3_gray_25</item> <item name="colorTertiary25">@color/m3_accent3_25</item>
<item name="colorTertiary50">@color/m3_accent3_50</item>
<item name="colorTertiary100">@color/m3_accent3_100</item>
<item name="colorTertiary200">@color/m3_accent3_200</item>
<item name="colorTertiary300">@color/m3_accent3_300</item>
<item name="colorTertiary400">@color/m3_accent3_400</item>
<item name="colorTertiary500">@color/m3_accent3_500</item>
<item name="colorTertiary600">@color/m3_accent3_600</item>
<item name="colorTertiary700">@color/m3_accent3_700</item>
<item name="colorTertiary800">@color/m3_accent3_800</item>
<item name="colorTertiary900">@color/m3_accent3_900</item>
<item name="colorGray900">@color/m3_neutral1_900</item>
<item name="colorGray800t">@color/m3_neutral1_800t</item>
<item name="colorGray800">@color/m3_neutral1_800</item>
<item name="colorGray700">@color/m3_neutral1_700</item>
<item name="colorGray600">@color/m3_neutral1_600</item>
<item name="colorGray500">@color/m3_neutral1_500</item>
<item name="colorGray400">@color/m3_neutral1_400</item>
<item name="colorGray300">@color/m3_neutral1_300</item>
<item name="colorGray200">@color/m3_neutral1_200</item>
<item name="colorGray100">@color/m3_neutral1_100</item>
<item name="colorGray50t">@color/m3_neutral1_50t</item>
<item name="colorGray50">@color/m3_neutral1_50</item>
<item name="colorGray25">@color/m3_neutral1_25</item>
<item name="colorNeutral900">@color/m3_neutral2_900</item>
<item name="colorNeutral800t">@color/m3_neutral2_800t</item>
<item name="colorNeutral800">@color/m3_neutral2_800</item>
<item name="colorNeutral700">@color/m3_neutral2_700</item>
<item name="colorNeutral600">@color/m3_neutral2_600</item>
<item name="colorNeutral500">@color/m3_neutral2_500</item>
<item name="colorNeutral400">@color/m3_neutral2_400</item>
<item name="colorNeutral300">@color/m3_neutral2_300</item>
<item name="colorNeutral200">@color/m3_neutral2_200</item>
<item name="colorNeutral100">@color/m3_neutral2_100</item>
<item name="colorNeutral50t">@color/m3_neutral2_50t</item>
<item name="colorNeutral50">@color/m3_neutral2_50</item>
<item name="colorNeutral25">@color/m3_neutral2_25</item>
</style> </style>
<style name="ColorPalette.Material3.Dark"> <style name="ColorPalette.Material3.Dark">

View File

@@ -452,4 +452,6 @@
<string name="welcome_paragraph1">Mastodon is a decentralized social network, meaning no single company controls it. Its made up of many independently-run servers, all connected together.</string> <string name="welcome_paragraph1">Mastodon is a decentralized social network, meaning no single company controls it. Its made up of many independently-run servers, all connected together.</string>
<string name="what_are_servers">What are servers?</string> <string name="what_are_servers">What are servers?</string>
<string name="welcome_paragraph2"><![CDATA[Every Mastodon account is hosted on a server — each with its own values, rules, & admins. No matter which one you pick, you can follow and interact with people on any server.]]></string> <string name="welcome_paragraph2"><![CDATA[Every Mastodon account is hosted on a server — each with its own values, rules, & admins. No matter which one you pick, you can follow and interact with people on any server.]]></string>
<string name="log_out_all_accounts">Log out of all accounts</string>
<string name="confirm_log_out_all_accounts">Log out of all accounts?</string>
</resources> </resources>

View File

@@ -288,4 +288,8 @@
<string name="sk_settings_content_types_explanation">Allows setting a content type like Markdown when creating a post. Keep in mind that not all instances support this.</string> <string name="sk_settings_content_types_explanation">Allows setting a content type like Markdown when creating a post. Keep in mind that not all instances support this.</string>
<string name="sk_settings_default_content_type">Default content type</string> <string name="sk_settings_default_content_type">Default content type</string>
<string name="sk_settings_default_content_type_explanation">This lets you have a content type be pre-selected when creating new posts, overriding the value set in “Posting preferences”.</string> <string name="sk_settings_default_content_type_explanation">This lets you have a content type be pre-selected when creating new posts, overriding the value set in “Posting preferences”.</string>
<string name="sk_instance_info_unavailable">Instance info temporarily unavailable</string>
<string name="sk_open_in_app">Open in app</string>
<string name="sk_external_share_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open with account</string>
</resources> </resources>

View File

@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="TransparentDialog" parent="android:Theme.Dialog">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="Theme.Mastodon.Light" parent="Theme.AppKit.Light"> <style name="Theme.Mastodon.Light" parent="Theme.AppKit.Light">
<!-- needed to disable scrim on API 29+ --> <!-- needed to disable scrim on API 29+ -->
<item name="android:enforceNavigationBarContrast" tools:ignore="NewApi">false</item> <item name="android:enforceNavigationBarContrast" tools:ignore="NewApi">false</item>
@@ -53,29 +63,29 @@
<item name="android:actionOverflowMenuStyle">@style/Widget.Mastodon.PopupMenu</item> <item name="android:actionOverflowMenuStyle">@style/Widget.Mastodon.PopupMenu</item>
<!-- M3 colors --> <!-- M3 colors -->
<item name="colorM3Primary">@color/m3_sys_light_primary</item> <item name="colorM3Primary">?colorPrimary600</item>
<item name="colorM3OnPrimary">@color/m3_sys_light_on_primary</item> <item name="colorM3OnPrimary">@color/white</item>
<item name="colorM3PrimaryContainer">@color/m3_sys_light_primary_container</item> <item name="colorM3PrimaryContainer">?colorPrimary100</item>
<item name="colorM3OnPrimaryContainer">@color/m3_sys_light_on_primary_container</item> <item name="colorM3OnPrimaryContainer">?colorPrimary900</item>
<item name="colorM3Secondary">@color/m3_sys_light_secondary</item> <item name="colorM3Secondary">?colorSecondary600</item>
<item name="colorM3OnSecondary">@color/m3_sys_light_on_secondary</item> <item name="colorM3OnSecondary">@color/white</item>
<item name="colorM3SecondaryContainer">@color/m3_sys_light_secondary_container</item> <item name="colorM3SecondaryContainer">?colorSecondary100</item>
<item name="colorM3OnSecondaryContainer">@color/m3_sys_light_on_secondary_container</item> <item name="colorM3OnSecondaryContainer">?colorSecondary900</item>
<item name="colorM3Tertiary">@color/m3_sys_light_tertiary</item> <item name="colorM3Tertiary">?colorTertiary600</item>
<item name="colorM3OnTertiary">@color/m3_sys_light_on_tertiary</item> <item name="colorM3OnTertiary">@color/white</item>
<item name="colorM3TertiaryContainer">@color/m3_sys_light_tertiary_container</item> <item name="colorM3TertiaryContainer">?colorTertiary100</item>
<item name="colorM3OnTertiaryContainer">@color/m3_sys_light_on_tertiary_container</item> <item name="colorM3OnTertiaryContainer">?colorTertiary900</item>
<item name="colorM3Background">@color/m3_sys_light_background</item> <item name="colorM3Background">?colorGray25</item>
<item name="colorM3OnBackground">@color/m3_sys_light_on_background</item> <item name="colorM3OnBackground">?colorGray900</item>
<item name="colorM3Surface">@color/m3_sys_light_surface</item> <item name="colorM3Surface">?colorGray25</item>
<item name="colorM3OnSurface">@color/m3_sys_light_on_surface</item> <item name="colorM3OnSurface">?colorGray900</item>
<item name="colorM3SurfaceVariant">@color/m3_sys_light_surface_variant</item> <item name="colorM3SurfaceVariant">?colorNeutral100</item>
<item name="colorM3OnSurfaceVariant">@color/m3_sys_light_on_surface_variant</item> <item name="colorM3OnSurfaceVariant">?colorNeutral700</item>
<item name="colorM3Outline">@color/m3_sys_light_outline</item> <item name="colorM3Outline">?colorNeutral500</item>
<item name="colorM3DisabledBackground">#1F1F1F1F</item> <item name="colorM3DisabledBackground">#1F1F1F1F</item>
<item name="colorM3PressedOverlay">@color/m3_sys_light_on_primary</item> <item name="colorM3PressedOverlay">@color/white</item>
<item name="colorM3Error">#B3261E</item> <item name="colorM3Error">#B3261E</item>
<item name="colorM3OnError">#FFF</item> <item name="colorM3OnError">@color/white</item>
<item name="colorM3ErrorContainer">#F9DEDC</item> <item name="colorM3ErrorContainer">#F9DEDC</item>
<item name="colorM3OnErrorContainer">#410E0B</item> <item name="colorM3OnErrorContainer">#410E0B</item>
@@ -153,27 +163,27 @@
<item name="android:actionOverflowMenuStyle">@style/Widget.Mastodon.PopupMenu</item> <item name="android:actionOverflowMenuStyle">@style/Widget.Mastodon.PopupMenu</item>
<!-- M3 colors --> <!-- M3 colors -->
<item name="colorM3Primary">@color/m3_sys_dark_primary</item> <item name="colorM3Primary">?colorPrimary200</item>
<item name="colorM3OnPrimary">@color/m3_sys_dark_on_primary</item> <item name="colorM3OnPrimary">?colorPrimary800</item>
<item name="colorM3PrimaryContainer">@color/m3_sys_dark_primary_container</item> <item name="colorM3PrimaryContainer">?colorPrimary700</item>
<item name="colorM3OnPrimaryContainer">@color/m3_sys_dark_on_primary_container</item> <item name="colorM3OnPrimaryContainer">?colorPrimary100</item>
<item name="colorM3Secondary">@color/m3_sys_dark_secondary</item> <item name="colorM3Secondary">?colorSecondary200</item>
<item name="colorM3OnSecondary">@color/m3_sys_dark_on_secondary</item> <item name="colorM3OnSecondary">?colorSecondary800</item>
<item name="colorM3SecondaryContainer">@color/m3_sys_dark_secondary_container</item> <item name="colorM3SecondaryContainer">?colorSecondary700</item>
<item name="colorM3OnSecondaryContainer">@color/m3_sys_dark_on_secondary_container</item> <item name="colorM3OnSecondaryContainer">?colorSecondary100</item>
<item name="colorM3Tertiary">@color/m3_sys_dark_tertiary</item> <item name="colorM3Tertiary">?colorTertiary200</item>
<item name="colorM3OnTertiary">@color/m3_sys_dark_on_tertiary</item> <item name="colorM3OnTertiary">?colorTertiary800</item>
<item name="colorM3TertiaryContainer">@color/m3_sys_dark_tertiary_container</item> <item name="colorM3TertiaryContainer">?colorTertiary700</item>
<item name="colorM3OnTertiaryContainer">@color/m3_sys_dark_on_tertiary_container</item> <item name="colorM3OnTertiaryContainer">?colorTertiary100</item>
<item name="colorM3Background">@color/m3_sys_dark_background</item> <item name="colorM3Background">?colorGray900</item>
<item name="colorM3OnBackground">@color/m3_sys_dark_on_background</item> <item name="colorM3OnBackground">?colorGray100</item>
<item name="colorM3Surface">@color/m3_sys_dark_surface</item> <item name="colorM3Surface">?colorGray900</item>
<item name="colorM3OnSurface">@color/m3_sys_dark_on_surface</item> <item name="colorM3OnSurface">?colorGray100</item>
<item name="colorM3SurfaceVariant">@color/m3_sys_dark_surface_variant</item> <item name="colorM3SurfaceVariant">?colorNeutral700</item>
<item name="colorM3OnSurfaceVariant">@color/m3_sys_dark_on_surface_variant</item> <item name="colorM3OnSurfaceVariant">?colorNeutral200</item>
<item name="colorM3Outline">@color/m3_sys_dark_outline</item> <item name="colorM3Outline">?colorNeutral400</item>
<item name="colorM3DisabledBackground">#1FE3E3E3</item> <item name="colorM3DisabledBackground">#1FE3E3E3</item>
<item name="colorM3PressedOverlay">@color/m3_sys_dark_primary</item> <item name="colorM3PressedOverlay">?colorPrimary200</item>
<item name="colorM3Error">#F2B8B5</item> <item name="colorM3Error">#F2B8B5</item>
<item name="colorM3OnError">#601410</item> <item name="colorM3OnError">#601410</item>
<item name="colorM3ErrorContainer">#8C1D18</item> <item name="colorM3ErrorContainer">#8C1D18</item>
@@ -221,7 +231,8 @@
<item name="colorComposeButton">?android:colorAccent</item> <item name="colorComposeButton">?android:colorAccent</item>
<item name="colorComposeButtonBackground">?colorGray900</item> <item name="colorComposeButtonBackground">?colorGray900</item>
<item name="colorM3Background">#000</item> <item name="colorM3Background">@color/black</item>
<item name="colorM3Surface">@color/black</item>
<!-- <item name="colorButtonBackgroundPrimaryLightOnDarkDisabled">?colorGray900</item>--> <!-- <item name="colorButtonBackgroundPrimaryLightOnDarkDisabled">?colorGray900</item>-->
</style> </style>
@@ -447,6 +458,11 @@
<item name="android:gravity">center_vertical</item> <item name="android:gravity">center_vertical</item>
</style> </style>
<style name="sheet_title">
<item name="android:textSize">20sp</item>
<item name="android:gravity">center_vertical</item>
</style>
<style name="m3_body_large"> <style name="m3_body_large">
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>