Merge remote-tracking branch 'megalodon_main/main'

# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/MainActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowerListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusFavoritesListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusReblogsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverAccountsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverHashtagsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
#	mastodon/src/main/res/layout/item_account_switcher.xml
#	mastodon/src/main/res/values-ar-rDZ/strings_sk.xml
#	mastodon/src/main/res/values-es-rES/strings_sk.xml
#	mastodon/src/main/res/values-nl-rNL/strings_sk.xml
#	mastodon/src/main/res/values-pt-rPT/strings_sk.xml
#	mastodon/src/main/res/values-v31/colors.xml
#	mastodon/src/main/res/values/colors.xml
#	mastodon/src/main/res/values/styles.xml
This commit is contained in:
LucasGGamerM 2023-06-03 19:56:16 -03:00
commit 14175a9140
113 changed files with 4439 additions and 1513 deletions

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

@ -1,7 +1,6 @@
package org.joinmastodon.android;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
@ -19,6 +18,7 @@ import org.jsoup.internal.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
@ -30,8 +30,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text);
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
boolean isMastodonURL = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){
@ -40,11 +40,22 @@ public class ExternalShareActivity extends FragmentStackActivity{
}else if(sessions.size()==1 && !isMastodonURL){
openComposeFragment(sessions.get(0).getID());
}else{
new AccountSwitcherSheet(this, false, false, isMastodonURL, accountSession -> {
if(accountSession!=null)
openComposeFragment(accountSession.getID());
else
UiUtils.openURL(this, AccountSessionManager.getInstance().getLastActiveAccountID(), text);
new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> {
if (open && text.isPresent()) {
UiUtils.lookupURL(this, accountId, text.get(), false, (clazz, args) -> {
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();
}
}
@ -108,11 +119,4 @@ public class ExternalShareActivity extends FragmentStackActivity{
return null;
return new ArrayList<>(l);
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
}
}

View File

@ -88,6 +88,16 @@ public class GlobalUserPreferences{
catch (JsonSyntaxException ignored) { return orElse; }
}
public static void removeAccount(String accountId) {
recentLanguages.remove(accountId);
pinnedTimelines.remove(accountId);
accountsInGlitchMode.remove(accountId);
accountsWithLocalOnlySupport.remove(accountId);
accountsWithContentTypesEnabled.remove(accountId);
accountsDefaultContentTypes.remove(accountId);
save();
}
public static void load(){
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
@ -218,4 +228,3 @@ public class GlobalUserPreferences{
DARK
}
}

View File

@ -9,6 +9,8 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
@ -22,13 +24,13 @@ import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity{
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
@ -38,10 +40,18 @@ public class MainActivity extends FragmentStackActivity{
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
AccountSessionManager.getInstance()
.setLastActiveAccountID(intent.getStringExtra("account"));
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
AccountSessionManager.getInstance().getLastActiveAccount());
showFragmentForExternalShare(intent.getExtras());
return;
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
@ -55,6 +65,7 @@ public class MainActivity extends FragmentStackActivity{
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
@ -78,12 +89,12 @@ public class MainActivity extends FragmentStackActivity{
@Override
protected void onNewIntent(Intent intent){
super.onNewIntent(intent);
if(intent.getBooleanExtra("fromNotification", false)){
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras());
else if (intent.getBooleanExtra("fromNotification", false)) {
String accountID=intent.getStringExtra("accountID");
AccountSession accountSession;
try{
accountSession=AccountSessionManager.getInstance().getAccount(accountID);
DomainManager.getInstance().setCurrentDomain(accountSession.domain);
AccountSessionManager.getInstance().getAccount(accountID);
}catch(IllegalStateException x){
return;
}
@ -128,6 +139,19 @@ public class MainActivity extends FragmentStackActivity{
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(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
@ -157,25 +181,40 @@ public class MainActivity extends FragmentStackActivity{
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
);
Bundle currentArgs = currentFragment.getArguments();
if (this.fragmentContainers.size() == 1
&& currentArgs != null
&& currentArgs.getBoolean("_can_go_back", false)
&& currentArgs.containsKey("account")) {
if (fragmentContainers.size() != 1
|| currentArgs == null
|| !currentArgs.getBoolean("_can_go_back", false)) {
super.onBackPressed();
return;
}
if (currentArgs.getBoolean("_finish_on_back", false)) {
finish();
} else if (currentArgs.containsKey("account")) {
Bundle args = new Bundle();
args.putString("account", currentArgs.getString("account"));
args.putString("tab", "notifications");
if (getIntent().getBooleanExtra("fromNotification", false)) {
args.putString("tab", "notifications");
}
Fragment fragment=new HomeFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
} else {
super.onBackPressed();
}
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
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

@ -29,6 +29,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.NotificationAction;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification;
@ -38,6 +39,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@ -57,7 +59,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
private static final String ACTION_KEY_TEXT_REPLY = "ACTION_KEY_TEXT_REPLY";
private static final int SUMMARY_ID = 791;
private static int notificationId;
private static int notificationId = 0;
@Override
public void onReceive(Context context, Intent intent){
@ -298,27 +300,60 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
// copied from ComposeFragment - TODO: generalize?
ArrayList<String> mentions=new ArrayList<>();
Status status = notification.status;
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!status.account.id.equals(ownID))
mentions.add('@'+status.account.acct);
for(Mention mention:status.mentions){
if(mention.id.equals(ownID))
continue;
String m='@'+mention.acct;
if(!mentions.contains(m))
mentions.add(m);
}
String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
CreateStatus.Request req=new CreateStatus.Request();
req.status = input.toString() + "\n\n" + "@" + notification.status.account.acct;
req.language = notification.status.language;
req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility);
req.status = initialText + input.toString();
req.language = preferences.postingDefaultLanguage;
req.visibility = preferences.postingDefaultVisibility;
req.inReplyToId = notification.status.id;
if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){
req.spoilerText = "re: " + notification.status.spoilerText;
}
new CreateStatus(req, UUID.randomUUID().toString()).exec(accountID);
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
@Override
public void onSuccess(Status status) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
new Notification.Builder(context, accountID+"_"+notification.type) :
new Notification.Builder(context)
.setPriority(Notification.PRIORITY_DEFAULT)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
new Notification.Builder(context, accountID+"_"+notification.type) :
new Notification.Builder(context)
.setPriority(Notification.PRIORITY_DEFAULT)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
notification.status = status;
Intent contentIntent=new Intent(context, MainActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
contentIntent.putExtra("fromNotification", true);
contentIntent.putExtra("accountID", accountID);
contentIntent.putExtra("notification", Parcels.wrap(notification));
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentText(context.getString(R.string.mo_notification_action_replied, notification.status.account.getDisplayUsername()))
.build();
notificationManager.notify(accountID, notificationId, repliedNotification);
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentTitle(context.getString(R.string.sk_notification_action_replied, notification.status.account.displayName))
.setContentText(status.getStrippedText())
.setCategory(Notification.CATEGORY_SOCIAL)
.setContentIntent(PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.build();
notificationManager.notify(accountID, notificationId, repliedNotification);
}
@Override
public void onError(ErrorResponse errorResponse) {
}
}).exec(accountID);
}
}
}

View File

@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@ -160,7 +159,7 @@ public class CacheController{
}
}
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){

View File

@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.notifications;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
private String maxID;
public PleromaMarkNotificationsRead(String maxID) {
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
this.maxID = maxID;
}
@Override
public RequestBody getRequestBody() {
MultipartBody.Builder builder=new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if(!TextUtils.isEmpty(maxID))
builder.addFormDataPart("max_id", maxID);
return builder.build();
}
}

View File

@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.timelines;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
public GetBubbleTimeline(String maxID, int limit) {
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
addQueryParameter("limit", ""+limit);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -4,6 +4,7 @@ import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

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

View File

@ -15,6 +15,7 @@ import android.util.Log;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@ -121,6 +122,12 @@ public class AccountSessionManager{
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
// write initial instance info to file immediately to avoid sessions without instance info
InstanceInfoStorageWrapper wrapper = new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
@ -129,14 +136,16 @@ public class AccountSessionManager{
}
public synchronized void writeAccountsFile(){
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~");
File file = new File(MastodonApp.context.getFilesDir(), "accounts.json");
try{
try(FileOutputStream out=new FileOutputStream(file)){
try(FileOutputStream out=new FileOutputStream(tmpFile)){
SessionsStorageWrapper w=new SessionsStorageWrapper();
w.accounts=new ArrayList<>(sessions.values());
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(w, writer);
writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}
}catch(IOException x){
Log.e(TAG, "Error writing accounts file", x);
@ -189,6 +198,7 @@ public class AccountSessionManager{
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db");
GlobalUserPreferences.removeAccount(id);
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty())
@ -259,31 +269,35 @@ public class AccountSessionManager{
}
public void maybeUpdateLocalInfo(){
maybeUpdateLocalInfo(null);
}
public void maybeUpdateLocalInfo(AccountSession activeSession){
long now=System.currentTimeMillis();
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
// if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
// }
// if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session);
// }
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
}
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
updateSessionWordFilters(session);
}
updateSessionMarkers(session);
}
if(loadedInstances){
maybeUpdateCustomEmojis(domains);
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
}
}
private void maybeUpdateCustomEmojis(Set<String> domains){
private void maybeUpdateCustomEmojis(Set<String> domains, String activeDomain){
long now=System.currentTimeMillis();
for(String domain:domains){
// Long lastUpdated=instancesLastUpdated.get(domain);
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain);
// }
Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
updateInstanceInfo(domain);
}
}
}
@ -411,7 +425,9 @@ public class AccountSessionManager{
@Override
public void onError(ErrorResponse error){
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
}
})
.execNoAuth(domain);
@ -422,10 +438,13 @@ public class AccountSessionManager{
}
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
File file = getInstanceInfoFile(domain);
File tmpFile = new File(file.getPath() + "~");
try(FileOutputStream out=new FileOutputStream(tmpFile)){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer);
writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}catch(IOException x){
Log.w(TAG, "Error writing instance info file for "+domain, x);
}
@ -445,7 +464,7 @@ public class AccountSessionManager{
}
if(!loadedInstances){
loadedInstances=true;
maybeUpdateCustomEmojis(domains);
maybeUpdateCustomEmojis(domains, null);
}
}
@ -469,10 +488,6 @@ public class AccountSessionManager{
return instances.get(domain);
}
public Instance getInstanceInfoForAccount(String account) {
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
}
public void updateAccountInfo(String id, Account account){
AccountSession session=getAccount(id);
session.self=account;

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
@ -15,6 +16,7 @@ import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
@ -49,6 +51,7 @@ import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
@ -69,7 +72,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, DomainDisplay{
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri, DomainDisplay {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@ -132,7 +135,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.clear();
}
protected void prependItems(List<T> items, boolean notify){
protected int prependItems(List<T> items, boolean notify){
data.addAll(0, items);
int offset=0;
for(T s:items){
@ -145,6 +148,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
if(notify)
adapter.notifyItemRangeInserted(0, offset);
return offset;
}
protected String getMaxID(){
@ -205,7 +209,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){
if(holder!=null && list!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
int[] pos={0, 0};
@ -337,6 +341,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
@ -348,18 +354,40 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
if(holder instanceof StatusDisplayItem.Holder<?> h){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(otherID.equals(id)){
if (firstIndex < 0) firstIndex = i;
lastIndex = i;
StatusDisplayItem item = h.getItem();
hasDescendant = item.hasDescendantNeighbor;
// no for direct descendants because main status (right above) is
// being displayed with an extended footer - no connected layout
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left);
outRect.top=Math.min(outRect.top, tmpRect.top);
outRect.right=Math.max(outRect.right, tmpRect.right);
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
isWarning = true;
}
}
}
}
}
// shifting the selection box down
// see also: FooterStatusDisplayItem#onBind (setMargins)
if (isWarning || firstIndex < 0 || lastIndex < 0) 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());
@ -568,6 +596,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
holder.rebind();
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
@ -579,6 +615,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
warning.getItem().status.filterRevealed = true;
}
@Override
public String getAccountID(){
return accountID;
}
@ -717,6 +754,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return attachmentViewsPool;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@ -778,6 +819,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}

View File

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

View File

@ -252,10 +252,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
accountID=getArguments().getString("account");
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
if (contentType == null && GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
// if formatting is enabled, use plain to avoid confusing unspecified default setting
contentType = ContentType.PLAIN;
}
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
@ -274,9 +270,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Nav.finish(this);
return;
}
if(customEmojis.isEmpty()){
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
@ -1146,7 +1139,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
req.status=text;
req.localOnly=localOnly;
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.contentType=contentType;
@ -1800,11 +1793,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollChanged=true;
updatePublishButtonState();
}));
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
int maxCharactersPerOption = 50;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption;
else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
maxCharactersPerOption = instance.pollLimits.maxOptionChars;
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)});
pollOptionsView.addView(option.view);
pollOptions.add(option);
if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
int maxPollOptions = 4;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
maxPollOptions = instance.configuration.polls.maxOptions;
else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
maxPollOptions = instance.pollLimits.maxOptions;
if(pollOptions.size()==maxPollOptions)
addPollOptionBtn.setVisibility(View.GONE);
return option;
}
@ -1961,7 +1967,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
if (instance.isAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@ -32,9 +32,12 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CustomLocalTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.DividerItemDecoration;
@ -196,7 +199,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
@ -222,7 +225,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
updateOptionsMenu();
}

View File

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

View File

@ -4,6 +4,7 @@ import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.Collections;
@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@ -149,8 +151,13 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
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{

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop {
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String nextMaxID;
private String accountId;
private String accountID;
public FollowedHashtagsFragment() {
super(20);
@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
accountID=args.getString("account");
setTitle(R.string.sk_hashtags_you_follow);
}
@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountId);
.exec(accountID);
}
@Override
@ -76,8 +78,13 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
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>{
@ -114,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override
public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
}
}
}

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;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
@ -8,7 +9,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import org.joinmastodon.android.DomainManager;
@ -167,4 +167,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
protected Filter.FilterContext getFilterContext() {
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.NotificationManager;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.graphics.Outline;
import android.os.Build;
@ -40,16 +41,13 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import java.util.Optional;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
@ -63,11 +61,9 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID {
private FragmentRootLinearLayout content;
private HomeTabFragment homeTabFragment;
private NotificationsFragment notificationsFragment;
private DiscoverFragment searchFragment;
private ProfileFragment profileFragment;
@ -79,6 +75,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private int currentTab=R.id.tab_home;
private String accountID;
private boolean isPleroma;
@Override
public void onCreate(Bundle savedInstanceState){
@ -86,18 +83,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
E.register(this);
accountID=getArguments().getString("account");
setTitle(R.string.mo_app_name);
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
.map(Instance::isAkkoma)
.orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
// TODO: clean up
if(savedInstanceState==null){
Bundle args=new Bundle();
args.putString("account", accountID);
homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("disableDiscover", isPleroma);
args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment();
searchFragment.setArguments(args);
@ -149,7 +149,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit();
String defaultTab=getArguments().getString("tab");
if("notifications".equals(defaultTab)){
tabBar.selectTab(R.id.tab_notifications);
@ -170,19 +169,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public void onViewStateRestored(Bundle savedInstanceState){
super.onViewStateRestored(savedInstanceState);
if(savedInstanceState==null) return;
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab");
tabBar.selectTab(currentTab);
Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction()
.hide(homeTabFragment)
.hide(searchFragment)
@ -190,15 +184,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
.hide(profileFragment)
.show(current)
.commit();
maybeTriggerLoading(current);
}
@Override
public void onHiddenChanged(boolean hidden){
super.onHiddenChanged(hidden);
if (!hidden && fragmentForTab(currentTab) instanceof DomainDisplay display)
DomainManager.getInstance().setCurrentDomain(display.getDomain());
fragmentForTab(currentTab).onHiddenChanged(hidden);
}
@ -222,9 +213,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
homeTabFragment.onApplyWindowInsets(topOnlyInsets);
searchFragment.onApplyWindowInsets(topOnlyInsets);
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
profileFragment.onApplyWindowInsets(topOnlyInsets);
@ -243,34 +232,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
throw new IllegalArgumentException();
}
public void setCurrentTab(@IdRes int tab){
if(tab==currentTab)
return;
tabBar.selectTab(tab);
onTabSelected(tab);
}
private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){
if(tab == R.id.tab_search){
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
searchFragment.selectSearch();
return;
}
if(newFragment instanceof ScrollableToTop scrollable)
if (tab == R.id.tab_search)
searchFragment.onSelect();
else if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
}
if(tab==currentTab && tab == R.id.tab_search){
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
}
if (newFragment instanceof DomainDisplay display) {
DomainManager.getInstance().setCurrentDomain(display.getDomain());
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment);
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
}
private void maybeTriggerLoading(Fragment newFragment){
@ -297,10 +280,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new AccountSwitcherSheet(getActivity(), true, true, false, accountSession -> {
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}).show();
new AccountSwitcherSheet(getActivity(), this).show();
return true;
}
if(tab==R.id.tab_search){
@ -336,7 +316,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putInt("selectedTab", currentTab);
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
@ -345,10 +324,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
if (instance == null) return;
Optional<Instance> instance = session.getInstance();
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null)
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {
@ -356,9 +335,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
try {
long newestId = Long.parseLong(notifications.get(0).id);
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
System.out.println("NEWEST: " + newestId);
System.out.println("LAST SEEN: " + lastSeenId);
setNotificationBadge(newestId > lastSeenId);
} catch (Exception ignored) {
setNotificationBadge(false);
@ -372,6 +348,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
}).exec(accountID);
}
public void setNotificationBadge(boolean badge) {
notificationTabIcon.setImageResource(badge
? R.drawable.ic_fluent_alert_28_selector_badged
@ -387,4 +364,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
setNotificationBadge(false);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent);
}
}

View File

@ -10,6 +10,7 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.assist.AssistContent;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
@ -56,6 +57,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection;
import java.util.HashMap;
@ -73,7 +75,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, DomainDisplay, HasFab {
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID;
@ -108,7 +110,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
super.onCreate(savedInstanceState);
E.register(this);
accountID = getArguments().getString("account");
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
assert timelineDefinitions != null;
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
count = timelineDefinitions.size();
@ -209,10 +211,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
if (fragments[position] instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading()) page.loadData();
}
//update recent app list url
if (fragments[position] instanceof DomainDisplay page)
DomainManager.getInstance().setCurrentDomain(page.getDomain());
}
});
@ -297,14 +295,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}).exec(accountID);
}
@Override
public String getDomain() {
if (fragments[pager.getCurrentItem()] instanceof DomainDisplay page) {
return page.getDomain();
}
return DomainDisplay.super.getDomain();
}
private void onFabClick(View v){
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
l.onFabClick(v);
@ -722,6 +712,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return fab;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull
@Override

View File

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

View File

@ -1,263 +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);
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
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.Fragment;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -13,6 +14,12 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
@ -24,12 +31,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.V;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, DomainDisplay{
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
private TabLayout tabLayout;
private ViewPager2 pager;
@ -47,12 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
private String accountID;
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/notifications";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -107,6 +103,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
UiUtils.reduceSwipeSensitivity(pager);
tabViews=new FrameLayout[3];
for(int i=0;i<tabViews.length;i++){
@ -124,6 +121,18 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
scrollToTop();
}
});
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
@ -145,20 +154,17 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
args.putBoolean("noAutoLoad", true);
allNotificationsFragment=new NotificationsListFragment();
allNotificationsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyMentions", true);
args.putBoolean("noAutoLoad", true);
mentionsFragment=new NotificationsListFragment();
mentionsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyPosts", true);
args.putBoolean("noAutoLoad", true);
postsFragment=new NotificationsListFragment();
postsFragment.setArguments(args);
@ -190,6 +196,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Account> accounts) {
if (getActivity() == null) return;
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
}
@ -228,6 +235,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
protected void updateToolbar(){
super.updateToolbar();
getToolbar().setOutlineProvider(null);
getToolbar().setOnClickListener(v->scrollToTop());
}
private NotificationsListFragment getFragmentForPage(int page){
@ -239,6 +247,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
};
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
@ -263,4 +276,4 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
return position;
}
}
}
}

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@ -10,6 +11,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
@ -18,6 +20,8 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
@ -53,11 +57,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return false;
}
@Override
public String getDomain() {
return super.getDomain() + "/notifications";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -156,13 +155,17 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset == 0 && !result.items.isEmpty() && !result.isFromCache() && AccountSessionManager.getInstance().getAccount(accountID).markers != null && 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());
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
if (AccountSessionManager.getInstance().getAccount(accountID).markers != null)
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
}
}
});
@ -182,11 +185,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading){
refreshing=true;
loadData();
}
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
// loadData();
}
@Override
@ -272,4 +272,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + getSession().self.username + "/interactions"
: "/notifications").build();
}
}

View File

@ -21,7 +21,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment impl
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
}
@Override

View File

@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
@ -67,6 +68,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
@ -83,6 +85,7 @@ import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.time.LocalDateTime;
@ -93,9 +96,13 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@ -116,7 +123,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@ -146,6 +153,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private String note;
private Account account;
private String accountID;
private String domain;
private Relationship relationship;
private int statusBarHeight;
private boolean isOwnProfile;
@ -160,7 +168,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
private static final int MAX_FIELDS=4;
private int maxFields = 4;
// from ProfileAboutFragment
public UsableRecyclerView list;
@ -181,6 +189,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setRetainInstance(true);
accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
@ -188,6 +197,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
loaded=true;
if(!isOwnProfile)
loadRelationship();
else if (isInstanceAkkoma() && getInstance().isPresent())
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))
@ -206,14 +217,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setHasOptionsMenu(true);
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden) {
DomainManager.getInstance().setCurrentDomain(account.url);
}
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View content=inflater.inflate(R.layout.fragment_profile, container, false);
@ -396,7 +399,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setOnLongClickListener(v->{
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
usernameString+="@"+domain;
}
UiUtils.copyText(username, '@'+usernameString);
return true;
@ -469,11 +472,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onRefresh(){
if(isInEditMode){
refreshing=false;
refreshLayout.setRefreshing(false);
return;
}
if(refreshing)
return;
refreshing=true;
@ -577,12 +575,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void bindHeaderView(){
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
if((GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic) != null){
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
}
if((GlobalUserPreferences.playGifs ? account.header : account.headerStatic) != null) {
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
}
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
@ -610,7 +604,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
ssb.append(domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
@ -633,7 +627,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
}
CharSequence parsedBio = null;
if(account.note != null){
@ -645,8 +639,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bio.setVisibility(View.VISIBLE);
bio.setText(parsedBio);
}
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
@ -825,7 +817,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
}
Nav.go(getActivity(), ListTimelinesFragment.class, args);
Nav.go(getActivity(), ListsFragment.class, args);
}else if(id==R.id.followed_hashtags){
Bundle args=new Bundle();
args.putString("account", accountID);
@ -878,7 +870,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
// aboutFragment.setNote(relationship.note, accountID, profileAccountID);
}
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
DomainManager.getInstance().setCurrentDomain(account.url);
}
public ImageButton getFab() {
@ -1215,11 +1206,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
scrollView.smoothScrollTo(0, 0);
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
private void onFollowersOrFollowingClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
@ -1284,6 +1270,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if (adapter != null) adapter.notifyDataSetChanged();
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(account.url);
}
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
public MetadataAdapter(){
super(imgLoader);
@ -1314,7 +1315,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public int getItemCount(){
if(isInEditMode){
int size=metadataListData.size();
if(size<MAX_FIELDS)
if(size<maxFields)
size++;
return size;
}
@ -1448,7 +1449,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onClick(){
metadataListData.add(new AccountField());
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
if(metadataListData.size()==maxFields){ // replace this row with new row
adapter.notifyItemChanged(metadataListData.size()-1);
}else{
adapter.notifyItemInserted(metadataListData.size()-1);

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.os.Bundle;
@ -196,7 +197,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
}
@Override
public void onConfigurationChanged(Configuration newConfig){
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
}

View File

@ -1,9 +1,13 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.View;
import org.joinmastodon.android.DomainManager;
import androidx.annotation.NonNull;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSession;
@ -16,27 +20,51 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import me.grishka.appkit.api.SimpleCallback;
public class ThreadFragment extends StatusListFragment implements DomainDisplay{
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus;
@Override
public String getDomain() {
return mainStatus.url;
}
/**
* 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
public void onCreate(Bundle savedInstanceState){
@ -48,21 +76,50 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
DomainManager.getInstance().setCurrentDomain(getDomain());
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
// modifying hidden filtered items if status is displayed as a warning
List<StatusDisplayItem> itemsToModify =
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
? warning.filteredItems
: items;
for(int i = 0; i < itemsToModify.size(); i++){
StatusDisplayItem item = itemsToModify.get(i);
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
if (ancestryInfo != null) {
item.setAncestryInfo(
ancestryInfo.hasDescendantNeighbor(),
ancestryInfo.hasAncestoringNeighbor(),
s.id.equals(mainStatus.id),
ancestryInfo.getAncestoringNeighbor()
.map(ancestor -> ancestor.id.equals(mainStatus.id))
.orElse(false)
);
}
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
deleteTheseItems.add(i);
}
if(s.id.equals(mainStatus.id)){
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
}
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
}
return items;
}
@ -76,36 +133,22 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
if (getActivity() == null) return;
if(refreshing){
data.clear();
ancestryMap.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
}
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.pleroma != null){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=result.ancestors.size()-1; i >= 0; i--){
Status s=result.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
result.descendants=getDescendantsOrdered(mainStatus.id,
result.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
// TODO: figure out how this code works
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
}
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
@ -114,7 +157,12 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
int count=displayItems.size();
if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
prependItems(result.ancestors, !refreshing);
int prependedCount = prependItems(result.ancestors, !refreshing);
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
displayItems.remove(prependedCount);
adapter.notifyItemRemoved(prependedCount);
count--;
}
dataLoaded();
if(refreshing){
refreshDone();
@ -126,7 +174,61 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
.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<>();
for(Status s:getDirectDescendants(id, statuses)){
out.add(s);
@ -138,7 +240,7 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
return out;
}
private List<Status> getDirectDescendants(String id, List<Status> statuses){
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
return statuses.stream()
.filter(s -> s.inReplyToId.equals(id))
.collect(Collectors.toList());
@ -195,4 +297,52 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
protected Filter.FilterContext getFilterContext() {
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;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.model.Account;
@ -14,4 +15,11 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
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;
import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
@ -23,7 +24,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
@ -34,6 +36,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
@ -57,7 +60,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> {
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@ -175,6 +178,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super.onApplyWindowInsets(insets);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@ -406,7 +419,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
args.putString("account", accountID);
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
Nav.go(getActivity(), ListsFragment.class, args);
}
return true;
}

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@ -12,7 +13,6 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
targetAccount = account;
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@ -22,7 +22,8 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count){
return new GetAccountFollowers(id, maxID, count);
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;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@ -12,7 +13,6 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
targetAccount = account;
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
}
@ -22,7 +22,8 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count){
return new GetAccountFollowing(id, maxID, count);
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;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@ -19,6 +20,14 @@ public class StatusFavoritesListFragment extends StatusRelatedAccountListFragmen
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();
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count) {
return null;

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@ -19,6 +20,14 @@ public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
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();
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count) {
return null;

View File

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

View File

@ -0,0 +1,61 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class BubbleTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE);
private String maxID;
@Override
protected boolean wantsComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/main/bubble").build() : null;
}
}

View File

@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@ -15,7 +16,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
@ -28,6 +28,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.Collections;
@ -51,7 +52,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, DomainDisplay {
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@ -60,11 +61,6 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
super(20);
}
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/explore/suggestions";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -157,6 +153,16 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/suggestions").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.app.FragmentTransaction;
import android.os.Build;
import android.os.Bundle;
@ -22,13 +23,15 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.DomainManager;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -40,7 +43,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, DomainDisplay {
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent {
private TabLayout tabLayout;
private ViewPager2 pager;
@ -53,30 +56,14 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private ProgressBar searchProgress;
private DiscoverPostsFragment postsFragment;
private TrendingHashtagsFragment hashtagsFragment;
private DiscoverHashtagsFragment hashtagsFragment;
private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment;
private LocalTimelineFragment localTimelineFragment;
private FederatedTimelineFragment federatedTimelineFragment;
private ListTimelinesFragment listTimelinesFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
// private final boolean noFederated = !GlobalUserPreferences.showFederatedTimeline;
@Override
public String getDomain() {
if (searchActive) {
return searchFragment.getDomain();
}
if (tabViews[tabLayout.getSelectedTabPosition()] instanceof DomainDisplay page) {
return page.getDomain();
}
return DomainDisplay.super.getDomain() + "/explore";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -94,22 +81,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
// tabViews=new FrameLayout[noFederated ? 5 : 6];
tabViews=new FrameLayout[4];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
/// int switchIndex = noFederated && i > 0 ? i + 1 : i;
// tabView.setId(switch(switchIndex){
// case 0 -> R.id.discover_local_timeline;
// case 1 -> R.id.discover_federated_timeline;
// case 2 -> R.id.discover_hashtags;
// case 3 -> R.id.discover_posts;
// case 4 -> R.id.discover_news;
// case 5 -> R.id.discover_users;
// default -> throw new IllegalStateException("Unexpected value: "+switchIndex);
// });
tabView.setId(switch(i){
case 0 -> R.id.discover_hashtags;
case 1 -> R.id.discover_posts;
@ -117,7 +91,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
tabViews[i]=tabView;
@ -140,13 +113,10 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
if (_page instanceof DomainDisplay display)
DomainManager.getInstance().setCurrentDomain(display.getDomain());
}
});
if(localTimelineFragment==null || hashtagsFragment==null){
if(hashtagsFragment==null){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
@ -154,7 +124,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment=new DiscoverPostsFragment();
postsFragment.setArguments(args);
hashtagsFragment=new TrendingHashtagsFragment();
hashtagsFragment=new DiscoverHashtagsFragment();
hashtagsFragment.setArguments(args);
newsFragment=new DiscoverNewsFragment();
@ -163,27 +133,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment=new DiscoverAccountsFragment();
accountsFragment.setArguments(args);
localTimelineFragment=new LocalTimelineFragment();
localTimelineFragment.setArguments(args);
// listTimelinesFragment=new ListTimelinesFragment();
// listTimelinesFragment.setArguments(args);
//
// FragmentTransaction transaction = getChildFragmentManager().beginTransaction()
// .add(R.id.discover_posts, postsFragment)
// .add(R.id.discover_local_timeline, localTimelineFragment)
// .add(R.id.discover_hashtags, hashtagsFragment)
// .add(R.id.discover_news, newsFragment)
// .add(R.id.discover_users, accountsFragment)
// .add(R.id.discover_lists, listTimelinesFragment);
//
// if (!noFederated) {
// federatedTimelineFragment=new FederatedTimelineFragment();
// federatedTimelineFragment.setArguments(args);
// transaction.add(R.id.discover_federated_timeline, federatedTimelineFragment);
// }
//
// transaction.commit();
getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment)
@ -196,21 +145,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
// if (noFederated && position > 0) position++;
// tab.setText(switch(position){
// case 0 -> R.string.local_timeline;
// case 1 -> R.string.sk_federated_timeline;
// case 2 -> R.string.sk_list_timelines;
// case 3 -> R.string.hashtags;
// case 4 -> R.string.posts;
// case 5 -> R.string.news;
// case 6 -> R.string.for_you;
//
// default -> throw new IllegalStateException("Unexpected value: "+position);
// });
tab.setText(switch(position){
case 0 -> R.string.hashtags;
case 1 -> R.string.posts;
@ -224,9 +158,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayoutMediator.attach();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
DomainManager.getInstance().setCurrentDomain(getDomain());
}
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@ -303,20 +235,25 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
@Override
public boolean isScrolledToTop() {
if(!searchActive){
return ((ScrollableToTop)getFragmentForPage(pager.getCurrentItem())).isScrolledToTop();
}else{
return searchFragment.isScrolledToTop();
}
public boolean isOnTop() {
return searchActive ? searchFragment.isOnTop()
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
}
public void onSelect() {
if (isOnTop()) selectSearch();
else scrollToTop();
}
public void selectSearch() {
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
}
public void loadData(){
if(hashtagsFragment!=null && !hashtagsFragment.loaded && !hashtagsFragment.dataLoading)
hashtagsFragment.loadData();
// if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
// localTimelineFragment.loadData();
}
private void onSearchEditFocusChanged(View v, boolean hasFocus){
@ -342,6 +279,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
if (getArguments().getBoolean("disableDiscover"))
((HomeFragment) getParentFragment()).onBackPressed();
}
@Override
@ -351,19 +290,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
private Fragment getFragmentForPage(int page){
// if (noFederated && page > 0) page++;
// return switch(page){
// case 0 -> localTimelineFragment;
// case 1 -> federatedTimelineFragment;
// case 2 -> hashtagsFragment;
// case 3 -> postsFragment;
// case 4 -> newsFragment;
// case 5 -> accountsFragment;
// case 6 -> listTimelinesFragment;
// default -> throw new IllegalStateException("Unexpected value: "+page);
// };
return switch(page){
case 0 -> hashtagsFragment;
case 1 -> postsFragment;
@ -401,6 +327,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(searchActive
? searchFragment
: getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@ -1,5 +1,9 @@
package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@ -7,7 +11,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
@ -16,29 +19,24 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, DomainDisplay {
public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public TrendingHashtagsFragment(){
public DiscoverHashtagsFragment(){
super(10);
}
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/explore/tags";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -76,13 +74,18 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
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>{
@ -117,14 +120,19 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
@Override
public void onBind(Hashtag item){
title.setText('#'+item.name);
int numPeople = 0;
if(item.history != null){
numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
chart.setData(item.history);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
subtitle.setText(getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
chart.setData(item.history);
}
@Override

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@ -10,7 +11,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
@ -20,6 +20,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collections;
import java.util.List;
@ -28,7 +29,6 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@ -37,7 +37,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop, IsOnTop, DomainDisplay {
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
@ -46,11 +46,6 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
super(10);
}
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/explore/links";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -91,13 +86,18 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
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{

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@ -48,9 +49,13 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
return isRecyclerViewOnTop(list);
}
@Override
protected Filter.FilterContext getFilterContext() {
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;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@ -25,12 +26,6 @@ public class FederatedTimelineFragment extends StatusListFragment {
return true;
}
@Override
public String getDomain() {
return super.getDomain() + "/public";
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
@ -50,11 +45,16 @@ public class FederatedTimelineFragment extends StatusListFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// bannerHelper.maybeAddBanner(contentWrap);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/all" : "/public").build();
}
}

View File

@ -1,9 +1,11 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@ -24,11 +26,6 @@ public class LocalTimelineFragment extends StatusListFragment {
return true;
}
@Override
public String getDomain() {
return super.getDomain() + "/public/local";
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@ -55,4 +52,9 @@ public class LocalTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() {
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;
import android.app.Activity;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@ -11,6 +12,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
@ -42,7 +44,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
@ -57,11 +59,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
setLayout(R.layout.fragment_search);
}
@Override
public String getDomain() {
return super.getDomain() + "/search";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -190,7 +187,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return;
}
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode();
boolean recent=isInRecentMode() && !displayItems.isEmpty();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.forceUpdateImages();
@ -316,6 +313,19 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}
}
@Override
public boolean isOnTop() {
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
public interface ProgressVisibilityListener{
void onProgressVisibilityChanged(boolean visible);

View File

@ -23,7 +23,8 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@ -69,7 +70,7 @@ public class AccountActivationFragment extends ToolbarFragment{
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsMainFragment.class, args);
Nav.go(getActivity(), SettingsFragment.class, args);
return true;
});
resendBtn=view.findViewById(R.id.btn_resend);
@ -89,15 +90,8 @@ public class AccountActivationFragment extends ToolbarFragment{
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
// @Override
protected void onUpdateToolbar(){
// super.onUpdateToolbar();
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
@ -109,10 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity(), true, true, false, accountSession -> {
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}).show();
new AccountSwitcherSheet(getActivity(), null).show();
}
@Override

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import java.net.IDN;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Parcel
public class Instance extends BaseModel{
@ -86,6 +87,11 @@ public class Instance extends BaseModel{
public Pleroma pleroma;
public PleromaPollLimits pollLimits;
/** like uri, but always without scheme and trailing slash */
public transient String normalizedUri;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@ -95,6 +101,10 @@ public class Instance extends BaseModel{
rules=Collections.emptyList();
if(shortDescription==null)
shortDescription="";
// akkoma says uri is "https://example.social" while just "example.social" on mastodon
normalizedUri = uri
.replaceFirst("^https://", "")
.replaceFirst("/$", "");
}
@Override
@ -134,6 +144,26 @@ public class Instance extends BaseModel{
return ci;
}
public boolean isAkkoma() {
return pleroma != null;
}
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)
.map(m -> m.features);
return switch (feature) {
case BUBBLE_TIMELINE -> pleromaFeatures
.map(f -> f.contains("bubble_timeline"))
.orElse(false);
};
}
public enum Feature {
BUBBLE_TIMELINE
}
@Parcel
public static class Rule{
public String id;
@ -198,6 +228,28 @@ public class Instance extends BaseModel{
@Parcel
public static class Pleroma extends BaseModel {
// metadata etc
public Pleroma.Metadata metadata;
@Parcel
public static class Metadata {
public List<String> features;
public Pleroma.Metadata.FieldsLimits fieldsLimits;
@Parcel
public static class FieldsLimits {
public int maxFields;
public int maxRemoteFields;
public int nameLength;
public int valueLength;
}
}
}
@Parcel
public static class PleromaPollLimits {
public int maxExpiration;
public int maxOptionChars;
public int maxOptions;
public int minExpiration;
}
}

View File

@ -11,15 +11,19 @@ import androidx.annotation.StringRes;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.CustomLocalTimelineFragment;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.discover.BubbleTimelineFragment;
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class TimelineDefinition {
private TimelineType type;
@ -65,6 +69,14 @@ public class TimelineDefinition {
this.type = type;
}
public boolean isCompatible(AccountSession session) {
return true;
}
public boolean wantsDefault(AccountSession session) {
return true;
}
public String getTitle(Context ctx) {
return title != null ? title : getDefaultTitle(ctx);
}
@ -85,6 +97,7 @@ public class TimelineDefinition {
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
case LIST -> listTitle;
case HASHTAG -> hashtagName;
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
case CUSTOM_LOCAL_TIMELINE -> domain;
};
}
@ -98,6 +111,7 @@ public class TimelineDefinition {
case LIST -> Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
case CUSTOM_LOCAL_TIMELINE -> Icon.CUSTOM_LOCAL_TIMELINE;
case BUBBLE -> Icon.BUBBLE;
};
}
@ -109,6 +123,7 @@ public class TimelineDefinition {
case LIST -> new ListTimelineFragment();
case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment();
case BUBBLE -> new BubbleTimelineFragment();
case CUSTOM_LOCAL_TIMELINE -> new CustomLocalTimelineFragment();
};
}
@ -172,7 +187,7 @@ public class TimelineDefinition {
return args;
}
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, CUSTOM_LOCAL_TIMELINE }
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, CUSTOM_LOCAL_TIMELINE, BUBBLE }
public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
@ -236,7 +251,8 @@ public class TimelineDefinition {
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
CUSTOM_LOCAL_TIMELINE(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true);
CUSTOM_LOCAL_TIMELINE(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
public final int iconRes, nameRes;
public final boolean hidden;
@ -256,14 +272,50 @@ public class TimelineDefinition {
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
@Override
public boolean isCompatible(AccountSession session) {
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of
// at least one instance that supports it, but doesn't list "bubble_timeline"
return session.getInstance().map(Instance::isAkkoma).orElse(false);
}
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE.copy(),
LOCAL_TIMELINE.copy(),
FEDERATED_TIMELINE.copy(),
POSTS_TIMELINE.copy()
@Override
public boolean wantsDefault(AccountSession session) {
return session.getInstance()
.map(i -> i.hasFeature(Instance.Feature.BUBBLE_TIMELINE))
.orElse(false);
}
};
public static List<TimelineDefinition> getDefaultTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return DEFAULT_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session) && tl.wantsDefault(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
public static List<TimelineDefinition> getAllTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return ALL_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
private static final List<TimelineDefinition> DEFAULT_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
BUBBLE_TIMELINE,
FEDERATED_TIMELINE
);
private static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POSTS_TIMELINE,
BUBBLE_TIMELINE
);
}

View File

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

View File

@ -18,13 +18,15 @@ package org.joinmastodon.android.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.ViewPropertyAnimator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import java.util.ArrayList;
@ -358,7 +360,14 @@ public class BetterItemAnimator extends SimpleItemAnimator{
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
float alpha = 0;
if (holder instanceof MediaGridStatusDisplayItem.Holder mediaItemHolder) {
if (mediaItemHolder.isSizeUpdating()) {
alpha = 1; // Image will flicker out and then in if alpha is 0
mediaItemHolder.sizeUpdated();
}
}
oldViewAnim.alpha(alpha).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.oldHolder, true);

View File

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

View File

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

View File

@ -148,12 +148,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
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);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.canBeBoosted(item.accountID));
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
!nextIsWarning;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0);
itemView.requestLayout();
}
private void bindButton(TextView btn, long count){

View File

@ -1,7 +1,9 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
@ -25,6 +27,13 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
private final TextView title, subtitle;
private final HashtagChartView chart;
public static final RelativeLayout.LayoutParams
withHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
withoutHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
static {
withoutHistoryParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
}
public Holder(Context context, ViewGroup parent){
super(context, R.layout.item_trending_hashtag, parent);
@ -37,14 +46,20 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public void onBind(HashtagStatusDisplayItem _item){
Hashtag item=_item.tag;
title.setText('#'+item.name);
int numPeople = 0;
if(item.history != null){
numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
chart.setData(item.history);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
subtitle.setText(_item.parentFragment.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
chart.setData(item.history);
}
}
}

View File

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

View File

@ -7,6 +7,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
@ -17,7 +18,6 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
@ -40,7 +40,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private static final String TAG="MediaGridDisplayItem";
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments;
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
@ -98,6 +98,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private int altTextIndex=-1;
private Animator altTextAnimator;
private boolean sizeUpdating = false;
public Holder(Activity activity, ViewGroup parent){
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
wrapper=(FrameLayout)itemView;
@ -126,6 +128,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
layout.removeAllViews();
controllers.clear();
int i=0;
for(Attachment att:item.attachments){
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
@ -158,6 +161,19 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
@Override
public void setImage(int index, Drawable drawable){
Rect bounds=drawable.getBounds();
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
if(item.attachments.get(index).meta==null){
Attachment.Metadata metadata = new Attachment.Metadata();
metadata.width=drawable.getIntrinsicWidth();
metadata.height=drawable.getIntrinsicHeight();
item.attachments.get(index).meta=metadata;
item.tiledLayout=PhotoLayoutHelper.processThumbs(item.attachments);
sizeUpdating = true;
item.parentFragment.onImageUpdated(this, index);
}
controllers.get(index).setImage(drawable);
}
@ -314,5 +330,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
layout.setClipChildren(clip);
wrapper.setClipChildren(clip);
}
public boolean isSizeUpdating() {
return sizeUpdating;
}
public void sizeUpdated() {
sizeUpdating = false;
}
}
}

View File

@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@ -26,6 +27,7 @@ import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
@ -45,6 +47,23 @@ public abstract class StatusDisplayItem{
public final BaseStatusListFragment parentFragment;
public boolean inset;
public int index;
public boolean
hasDescendantNeighbor = false,
hasAncestoringNeighbor = false,
isMainStatus = true,
isDirectDescendant = false;
public void setAncestryInfo(
boolean hasDescendantNeighbor,
boolean hasAncestoringNeighbor,
boolean isMainStatus,
boolean isDirectDescendant
) {
this.hasDescendantNeighbor = hasDescendantNeighbor;
this.hasAncestoringNeighbor = hasAncestoringNeighbor;
this.isMainStatus = isMainStatus;
this.isDirectDescendant = isDirectDescendant;
}
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID;
@ -163,6 +182,15 @@ public abstract class StatusDisplayItem{
items.add(replyLine);
}
if (statusForContent.quote != null) {
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
if (!hasQuoteInlineTag) {
String quoteUrl = statusForContent.quote.url;
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
}
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)
@ -174,6 +202,12 @@ public abstract class StatusDisplayItem{
.filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN))
.collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorAccentLightest);
for (Attachment att : imageAttachments) {
if (att.blurhashPlaceholder == null) {
att.blurhashPlaceholder = new ColorDrawable(color);
}
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
}

View File

@ -65,6 +65,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
UiUtils.loadMaxWidth(parentFragment.getContext());
}
public void setTranslationShown(boolean translationShown) {
@ -225,13 +226,33 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
// remove additional padding when (transparently padded) translate button is visible
int 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) {
textScrollView.setLayoutParams(wrapParams);
readMore.setVisibility(View.GONE);
}
// incredibly ugly workaround for https://github.com/sk22/megalodon/issues/520
// i am so, so sorry. FIXME
// attempts to use OnPreDrawListener, OnGlobalLayoutListener and .post have failed -
// the view didn't want to reliably update after calling .setVisibility etc :(
int width = parent.getWidth() != 0 ? parent.getWidth()
: item.parentFragment.getView().getWidth() != 0
? item.parentFragment.getView().getWidth()
: item.parentFragment.getParentFragment() != null && item.parentFragment.getParentFragment().getView().getWidth() != 0
? item.parentFragment.getParentFragment().getView().getWidth() // YIKES
: UiUtils.MAX_WIDTH;
text.measure(
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {

View File

@ -38,6 +38,7 @@ public class DiscoverInfoBannerHelper{
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
case BUBBLE_TIMELINE -> R.string.sk_bubble_timeline_info_banner;
});
}
}
@ -63,6 +64,7 @@ public class DiscoverInfoBannerHelper{
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POST_NOTIFICATIONS,
// ACCOUNTS
// ACCOUNTS,
BUBBLE_TIMELINE
}
}

View File

@ -7,6 +7,7 @@ import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
@ -102,6 +103,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
@ -114,6 +116,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
@ -141,12 +144,13 @@ import me.grishka.appkit.utils.V;
import okhttp3.MediaType;
public class UiUtils {
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private static Handler mainHandler = new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public static int MAX_WIDTH;
private UiUtils(){}
private UiUtils() {
}
public static void loadMaxWidth(Context ctx) {
if (MAX_WIDTH == 0) MAX_WIDTH = (int) ctx.getResources().getDimension(R.dimen.layout_max_width);
@ -159,33 +163,33 @@ public class UiUtils {
.setShowTitle(true)
.build()
.launchUrl(context, Uri.parse(url));
}else{
} else {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}catch(ActivityNotFoundException x){
} catch (ActivityNotFoundException x) {
Toast.makeText(context, R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
}
}
public static String formatRelativeTimestamp(Context context, Instant instant){
long t=instant.toEpochMilli();
long now=System.currentTimeMillis();
long diff=now-t;
if(diff<1000L){
public static String formatRelativeTimestamp(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
long diff = now - t;
if (diff < 1000L) {
return context.getString(R.string.time_now);
}else if(diff<60_000L){
return context.getString(R.string.time_seconds, diff/1000L);
}else if(diff<3600_000L){
return context.getString(R.string.time_minutes, diff/60_000L);
}else if(diff<3600_000L*24L){
return context.getString(R.string.time_hours, diff/3600_000L);
}else{
int days=(int)(diff/(3600_000L*24L));
if(days>30){
ZonedDateTime dt=instant.atZone(ZoneId.systemDefault());
if(dt.getYear()==ZonedDateTime.now().getYear()){
} else if (diff < 60_000L) {
return context.getString(R.string.time_seconds, diff / 1000L);
} else if (diff < 3600_000L) {
return context.getString(R.string.time_minutes, diff / 60_000L);
} else if (diff < 3600_000L * 24L) {
return context.getString(R.string.time_hours, diff / 3600_000L);
} else {
int days = (int) (diff / (3600_000L * 24L));
if (days > 30) {
ZonedDateTime dt = instant.atZone(ZoneId.systemDefault());
if (dt.getYear() == ZonedDateTime.now().getYear()) {
return DATE_FORMATTER_SHORT.format(dt);
}else{
} else {
return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
}
}
@ -207,76 +211,77 @@ public class UiUtils {
long diff=now-t;
if(diff<1000L){
return context.getString(R.string.time_just_now);
}else if(diff<60_000L){
int secs=(int)(diff/1000L);
} else if (diff < 60_000L) {
int secs = (int) (diff / 1000L);
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
}else if(diff<3600_000L){
int mins=(int)(diff/60_000L);
} else if (diff < 3600_000L) {
int mins = (int) (diff / 60_000L);
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
}else{
} else {
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
}
}
public static String formatTimeLeft(Context context, Instant instant){
long t=instant.toEpochMilli();
long now=System.currentTimeMillis();
long diff=t-now;
if(diff<60_000L){
int secs=(int)(diff/1000L);
public static String formatTimeLeft(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
long diff = t - now;
if (diff < 60_000L) {
int secs = (int) (diff / 1000L);
return context.getResources().getQuantityString(R.plurals.x_seconds_left, secs, secs);
}else if(diff<3600_000L){
int mins=(int)(diff/60_000L);
} else if (diff < 3600_000L) {
int mins = (int) (diff / 60_000L);
return context.getResources().getQuantityString(R.plurals.x_minutes_left, mins, mins);
}else if(diff<3600_000L*24L){
int hours=(int)(diff/3600_000L);
} else if (diff < 3600_000L * 24L) {
int hours = (int) (diff / 3600_000L);
return context.getResources().getQuantityString(R.plurals.x_hours_left, hours, hours);
}else{
int days=(int)(diff/(3600_000L*24L));
} else {
int days = (int) (diff / (3600_000L * 24L));
return context.getResources().getQuantityString(R.plurals.x_days_left, days, days);
}
}
@SuppressLint("DefaultLocale")
public static String abbreviateNumber(int n){
if(n<1000){
public static String abbreviateNumber(int n) {
if (n < 1000) {
return String.format("%,d", n);
}else if(n<1_000_000){
float a=n/1000f;
return a>99f ? String.format("%,dK", (int)Math.floor(a)) : String.format("%,.1fK", a);
}else{
float a=n/1_000_000f;
return a>99f ? String.format("%,dM", (int)Math.floor(a)) : String.format("%,.1fM", n/1_000_000f);
} else if (n < 1_000_000) {
float a = n / 1000f;
return a > 99f ? String.format("%,dK", (int) Math.floor(a)) : String.format("%,.1fK", a);
} else {
float a = n / 1_000_000f;
return a > 99f ? String.format("%,dM", (int) Math.floor(a)) : String.format("%,.1fM", n / 1_000_000f);
}
}
@SuppressLint("DefaultLocale")
public static String abbreviateNumber(long n){
if(n<1_000_000_000L)
return abbreviateNumber((int)n);
public static String abbreviateNumber(long n) {
if (n < 1_000_000_000L)
return abbreviateNumber((int) n);
double a=n/1_000_000_000.0;
return a>99f ? String.format("%,dB", (int)Math.floor(a)) : String.format("%,.1fB", n/1_000_000_000.0);
double a = n / 1_000_000_000.0;
return a > 99f ? String.format("%,dB", (int) Math.floor(a)) : String.format("%,.1fB", n / 1_000_000_000.0);
}
/**
* Android 6.0 has a bug where start and end compound drawables don't get tinted.
* This works around it by setting the tint colors directly to the drawables.
*
* @param textView
*/
public static void fixCompoundDrawableTintOnAndroid6(TextView textView){
Drawable[] drawables=textView.getCompoundDrawablesRelative();
for(int i=0;i<drawables.length;i++){
if(drawables[i]!=null){
Drawable tinted=drawables[i].mutate();
public static void fixCompoundDrawableTintOnAndroid6(TextView textView) {
Drawable[] drawables = textView.getCompoundDrawablesRelative();
for (int i = 0; i < drawables.length; i++) {
if (drawables[i] != null) {
Drawable tinted = drawables[i].mutate();
tinted.setTintList(textView.getTextColors());
drawables[i]=tinted;
drawables[i] = tinted;
}
}
textView.setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]);
}
public static void runOnUiThread(Runnable runnable){
public static void runOnUiThread(Runnable runnable) {
mainHandler.post(runnable);
}
@ -284,67 +289,71 @@ public class UiUtils {
mainHandler.postDelayed(runnable, delay);
}
public static void removeCallbacks(Runnable runnable){
public static void removeCallbacks(Runnable runnable) {
mainHandler.removeCallbacks(runnable);
}
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
/**
* Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
*/
public static int lerp(int startValue, int endValue, float fraction) {
return startValue + Math.round(fraction * (endValue - startValue));
}
public static String getFileName(Uri uri){
if(uri.getScheme().equals("content")){
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){
public static String getFileName(Uri uri) {
if (uri.getScheme().equals("content")) {
try (Cursor cursor = MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
cursor.moveToFirst();
String name=cursor.getString(0);
if(name!=null)
String name = cursor.getString(0);
if (name != null)
return name;
}catch(Throwable ignore){}
} catch (Throwable ignore) {
}
}
return uri.getLastPathSegment();
}
public static String formatFileSize(Context context, long size, boolean atLeastKB){
if(size<1024 && !atLeastKB){
public static String formatFileSize(Context context, long size, boolean atLeastKB) {
if (size < 1024 && !atLeastKB) {
return context.getString(R.string.file_size_bytes, size);
}else if(size<1024*1024){
return context.getString(R.string.file_size_kb, size/1024.0);
}else if(size<1024*1024*1024){
return context.getString(R.string.file_size_mb, size/(1024.0*1024.0));
}else{
return context.getString(R.string.file_size_gb, size/(1024.0*1024.0*1024.0));
} else if (size < 1024 * 1024) {
return context.getString(R.string.file_size_kb, size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return context.getString(R.string.file_size_mb, size / (1024.0 * 1024.0));
} else {
return context.getString(R.string.file_size_gb, size / (1024.0 * 1024.0 * 1024.0));
}
}
public static MediaType getFileMediaType(File file){
String name=file.getName();
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1)));
public static MediaType getFileMediaType(File file) {
String name = file.getName();
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.') + 1)));
}
public static void loadCustomEmojiInTextView(TextView view){
CharSequence _text=view.getText();
if(!(_text instanceof Spanned text))
public static void loadCustomEmojiInTextView(TextView view) {
CharSequence _text = view.getText();
if (!(_text instanceof Spanned))
return;
CustomEmojiSpan[] spans=text.getSpans(0, text.length(), CustomEmojiSpan.class);
if(spans.length==0)
Spanned text = (Spanned) _text;
CustomEmojiSpan[] spans = text.getSpans(0, text.length(), CustomEmojiSpan.class);
if (spans.length == 0)
return;
int emojiSize=V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji=Arrays.stream(spans).collect(Collectors.groupingBy(s->s.emoji));
for(Map.Entry<Emoji, List<CustomEmojiSpan>> emoji:spansByEmoji.entrySet()){
ViewImageLoader.load(new ViewImageLoader.Target(){
int emojiSize = V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji = Arrays.stream(spans).collect(Collectors.groupingBy(s -> s.emoji));
for (Map.Entry<Emoji, List<CustomEmojiSpan>> emoji : spansByEmoji.entrySet()) {
ViewImageLoader.load(new ViewImageLoader.Target() {
@Override
public void setImageDrawable(Drawable d){
if(d==null)
public void setImageDrawable(Drawable d) {
if (d == null)
return;
for(CustomEmojiSpan span:emoji.getValue()){
for (CustomEmojiSpan span : emoji.getValue()) {
span.setDrawable(d);
}
view.invalidate();
}
@Override
public View getView(){
public View getView() {
return view;
}
}, null, new UrlImageLoaderRequest(emoji.getKey().url, emojiSize, emojiSize), null, false, true);
@ -371,22 +380,22 @@ public class UiUtils {
Bundle args = new Bundle();
args.putString("account", selfID);
args.putString("profileAccountID", id);
Nav.go((Activity)context, ProfileFragment.class, args);
Nav.go((Activity) context, ProfileFragment.class, args);
}
public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following){
Bundle args=new Bundle();
public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following) {
Bundle args = new Bundle();
args.putString("account", accountID);
args.putString("hashtag", hashtag);
if (following != null) args.putBoolean("following", following);
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
Nav.go((Activity) context, HashtagTimelineFragment.class, args);
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed) {
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed){
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed) {
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
}
@ -404,25 +413,25 @@ public class UiUtils {
.show();
}
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_person_prohibited_28_regular,
()->{
() -> {
new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship result){
public void onSuccess(Relationship result) {
if (activity == null) return;
resultCallback.accept(result);
if(!currentlyBlocked){
if (!currentlyBlocked) {
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@ -431,54 +440,54 @@ public class UiUtils {
});
}
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback){
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity,
activity.getString(R.string.sk_remove_follower),
activity.getString(R.string.sk_remove_follower_confirm, account.displayName),
activity.getString(R.string.sk_do_remove_follower),
R.drawable.ic_fluent_person_delete_24_regular,
() -> new SetAccountBlocked(account.id, true).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
new SetAccountBlocked(account.id, false).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
if (activity == null) return;
Toast.makeText(activity, R.string.sk_remove_follower_success, Toast.LENGTH_SHORT).show();
resultCallback.accept(relationship);
}
@Override
public void onSuccess(Relationship relationship) {
new SetAccountBlocked(account.id, false).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
if (activity == null) return;
Toast.makeText(activity, R.string.sk_remove_follower_success, Toast.LENGTH_SHORT).show();
resultCallback.accept(relationship);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
resultCallback.accept(relationship);
}
}).exec(accountID);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
resultCallback.accept(relationship);
}
}).exec(accountID);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
);
}
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_shield_28_regular,
()->{
() -> {
new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Object result){
public void onSuccess(Object result) {
resultCallback.run();
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@ -585,9 +594,9 @@ public class UiUtils {
R.string.delete,
R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus.Scheduled(status.id)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o){
public void onSuccess(Object o) {
resultCallback.run();
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
}
@ -675,38 +684,39 @@ public class UiUtils {
setRelationshipToActionButton(relationship, button, false);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText){
public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText) {
CharSequence textBefore = keepText ? button.getText() : null;
boolean secondaryStyle;
if(relationship.blocking){
if (relationship.blocking) {
button.setText(R.string.button_blocked);
secondaryStyle=true;
}else if(relationship.blockedBy){
button.setText(R.string.button_follow);
secondaryStyle=false;
}else if(relationship.requested){
secondaryStyle = true;
// } else if (relationship.blockedBy) {
// button.setText(R.string.button_follow);
// secondaryStyle = false;
} else if (relationship.requested) {
button.setText(R.string.button_follow_pending);
secondaryStyle=true;
}else if(!relationship.following){
secondaryStyle = true;
} else if (!relationship.following) {
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
secondaryStyle=false;
}else{
secondaryStyle = false;
} else {
button.setText(R.string.button_following);
secondaryStyle=true;
secondaryStyle = true;
}
if (keepText) button.setText(textBefore);
button.setEnabled(!relationship.blockedBy);
int attr=secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
TypedArray ta=button.getContext().obtainStyledAttributes(new int[]{attr});
int styleRes=ta.getResourceId(0, 0);
// https://github.com/sk22/megalodon/issues/526
// button.setEnabled(!relationship.blockedBy);
int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
int styleRes = ta.getResourceId(0, 0);
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
if(relationship.blocking)
ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
if (relationship.blocking)
button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
else
button.setTextColor(ta.getColorStateList(0));
@ -721,7 +731,7 @@ public class UiUtils {
public void onSuccess(Relationship result) {
resultCallback.accept(result);
progressCallback.accept(false);
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show();
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@' + account.username), Toast.LENGTH_SHORT).show();
}
@Override
@ -839,7 +849,8 @@ public class UiUtils {
@Override
public void onSuccess(Relationship rel) {
E.post(new FollowRequestHandledEvent(accountID, false, account, rel));
if (notificationID != null) E.post(new NotificationDeletedEvent(notificationID));
if (notificationID != null)
E.post(new NotificationDeletedEvent(notificationID));
resultCallback.accept(rel);
}
@ -852,34 +863,34 @@ public class UiUtils {
}
}
public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame){
public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame) {
// Save topmost item position and offset because for some reason RecyclerView would scroll the list to weird places when you insert items at the top
int topItem, topItemOffset;
if(list.getChildCount()==0){
topItem=topItemOffset=0;
}else{
View child=list.getChildAt(0);
topItem=list.getChildAdapterPosition(child);
topItemOffset=child.getTop();
if (list.getChildCount() == 0) {
topItem = topItemOffset = 0;
} else {
View child = list.getChildAt(0);
topItem = list.getChildAdapterPosition(child);
topItemOffset = child.getTop();
}
DiffUtil.calculateDiff(new DiffUtil.Callback(){
DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize(){
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize(){
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsSame.test(oldList.get(oldItemPosition), newList.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return true;
}
}).dispatchUpdatesTo(adapter);
@ -887,34 +898,35 @@ public class UiUtils {
list.scrollBy(0, topItemOffset);
}
public static Bitmap getBitmapFromDrawable(Drawable d){
if(d instanceof BitmapDrawable)
public static Bitmap getBitmapFromDrawable(Drawable d) {
if (d instanceof BitmapDrawable)
return ((BitmapDrawable) d).getBitmap();
Bitmap bitmap=Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
d.draw(new Canvas(bitmap));
return bitmap;
}
public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
insetPopupMenuIcon(item, iconTint);
}
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
Drawable icon = item.getIcon().mutate();
if (Build.VERSION.SDK_INT >= 26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint);
icon=new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
icon = new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
SpannableStringBuilder ssb = new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb);
}
public static void resetPopupItemTint(MenuItem item) {
if(Build.VERSION.SDK_INT>=26) {
if (Build.VERSION.SDK_INT >= 26) {
item.setIconTintList(null);
} else {
Drawable icon=item.getIcon().mutate();
Drawable icon = item.getIcon().mutate();
icon.setTintList(null);
item.setIcon(icon);
}
@ -923,7 +935,7 @@ public class UiUtils {
/// Add icons to the menu.
/// Passing in items will be colored to be visible on the background.
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
try {
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
@ -935,31 +947,33 @@ public class UiUtils {
}
public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for (int i = 0; i < m.size(); i++) {
MenuItem item = m.getItem(i);
SubMenu subMenu = item.getSubMenu();
if (subMenu != null) enableMenuIcons(context, subMenu, exclude);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId()))
continue;
insetPopupMenuIcon(item, iconTint);
}
}
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
Menu m=menu.getMenu();
if(Build.VERSION.SDK_INT>=29){
public static void enablePopupMenuIcons(Context context, PopupMenu menu) {
Menu m = menu.getMenu();
if (Build.VERSION.SDK_INT >= 29) {
menu.setForceShowIcon(true);
}else{
try{
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
} else {
try {
Method setOptionalIconsVisible = m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
setOptionalIconsVisible.setAccessible(true);
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
} catch (Exception ignore) {
}
}
enableMenuIcons(context, m);
}
public static void setUserPreferredTheme(Context context){
public static void setUserPreferredTheme(Context context) {
context.setTheme(switch (theme) {
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
@ -969,10 +983,11 @@ public class UiUtils {
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context);
}
public static boolean isDarkTheme(){
if(theme==GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
return theme==GlobalUserPreferences.ThemePreference.DARK;
public static boolean isDarkTheme() {
if (theme == GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
return theme == GlobalUserPreferences.ThemePreference.DARK;
}
// https://mastodon.foo.bar/@User
@ -1001,7 +1016,8 @@ public class UiUtils {
return false;
}
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false;
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null)
return false;
String it = uri.getPath();
return it.matches("^/@[^/]+$") ||
@ -1021,13 +1037,13 @@ public class UiUtils {
public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
Optional<Instance> instance = session.getInstance();
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) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList());
List<AccountSession> sessions = AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s -> !s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems(
@ -1140,7 +1156,7 @@ public class UiUtils {
error.showToast(context);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
}
@ -1250,28 +1266,36 @@ public class UiUtils {
}
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser){
Uri uri=Uri.parse(url);
List<String> path=uri.getPathSegments();
if(accountID!=null && "https".equals(uri.getScheme())){
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
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);
List<String> path = uri.getPathSegments();
if (accountID != null && "https".equals(uri.getScheme())) {
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1))
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result){
Bundle args=new Bundle();
public void onSuccess(Status result) {
Bundle args = new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
Nav.go((Activity) context, ThreadFragment.class, args);
go.accept(ThreadFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
@ -1280,54 +1304,62 @@ public class UiUtils {
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Bundle args=new Bundle();
Bundle args = new Bundle();
args.putString("account", accountID);
if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args);
} else if (!results.accounts.isEmpty()) {
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
if (launchBrowser) launchWebBrowser(context, url);
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(ThreadFragment.class, args);
return;
}
Optional<Account> account = results.accounts.stream()
.filter(a -> uri.equals(Uri.parse(a.url))).findAny();
if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) launchWebBrowser(context, url);
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
}
}
launchWebBrowser(context, url);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
}
public static void copyText(View v, String text) {
Context context = v.getContext();
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()) { // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
}
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
private static String getSystemProperty(String key){
try{
Class<?> props=Class.forName("android.os.SystemProperties");
Method get=props.getMethod("get", String.class);
return (String)get.invoke(null, key);
}catch(Exception ignore){}
private static String getSystemProperty(String key) {
try {
Class<?> props = Class.forName("android.os.SystemProperties");
Method get = props.getMethod("get", String.class);
return (String) get.invoke(null, key);
} catch (Exception ignore) {
}
return null;
}
public static boolean isMIUI(){
public static boolean isMIUI() {
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
}
@ -1335,17 +1367,25 @@ public class UiUtils {
return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"));
}
public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
int g = Math.round(((color1 >> 8) & 0xFF) * alpha0 + ((color2 >> 8) & 0xFF) * alpha);
int b = Math.round((color1 & 0xFF) * alpha0 + (color2 & 0xFF) * alpha);
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText) {
Bundle args = new Bundle();
if (prefilledText != null) args.putString("prefilledText", prefilledText);
return pickAccountForCompose(activity, accountID, args);
}
public static boolean pickAccountForCompose(Activity activity, String accountID){
public static boolean pickAccountForCompose(Activity activity, String accountID) {
return pickAccountForCompose(activity, accountID, (String) null);
}
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
args.putString("account", session.getID());

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

@ -29,10 +29,10 @@
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="24sp"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/button"
android:layout_marginTop="2sp"
android:layout_above="@+id/username"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
@ -40,16 +40,16 @@
tools:text="User"/>
<TextView
android:id="@+id/username"
android:id="@id/username"
android:layout_width="match_parent"
android:layout_height="20sp"
android:layout_below="@id/name"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/button"
android:layout_alignBottom="@id/avatar"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small"
android:paddingBottom="3sp"
tools:text="\@user@server"/>
<View

View File

@ -1,79 +1,82 @@
<?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:layout_width="match_parent"
android:layout_height="52dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="@+id/avatar"
android:layout_width="36dp"
android:layout_height="36dp"
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_height="32dp"
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:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
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
android:id="@+id/display_name"
android:layout_width="wrap_content"
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="24dp"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:gravity="center_vertical"
android:singleLine="true"
android:ellipsize="end"/>
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="24dp"
android:textSize="14sp"
android:textColor="?android:textColorSecondary"
android:textColor="?colorM3OnSurfaceVariant"
android:textAppearance="@style/m3_body_medium"
android:singleLine="true"
android:gravity="center_vertical"
android:ellipsize="end"/>
</LinearLayout>
<TextView
android:id="@+id/add_account"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:text="@string/add_account"
android:singleLine="true"
android:visibility="gone"
android:ellipsize="end"/>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<View
android:id="@+id/current"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/ic_fluent_checkmark_24_filled"
android:backgroundTint="?android:textColorSecondary"
android:contentDescription="@string/current_account"/>
<ImageButton
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

@ -465,21 +465,7 @@
<string name="privacy_policy_subtitle">على الرغم من أن تطبيق ماستدون لا يجمع أي بيانات، فإن الخادم الذي قمت بالتسجيل من خلاله قد تكون له سياسة مختلفة. خذ دقيقة للمراجعة والموافقة على سياسة خصوصية التطبيق ماستدون وسياسة الخصوصية للخادم الخاص بك.</string>
<string name="i_agree">أنا مُوافِق</string>
<string name="empty_list">هذه القائمة فارغة</string>
<string name="confirm_delete_and_redraft">هل أنت متأكد أنك تريد حذف وإعادة صياغة هذا المنشور؟</string>
<string name="visibility_unlisted">غير مدرج</string>
<string name="list_timelines">القوائم</string>
<string name="follow_requests">طلبات المتابعة</string>
<string name="instance_signup_closed">هذا الخادم لا يقبل تسجيلات جديدة.</string>
<string name="pinned_posts">مدبّس</string>
<string name="delete_and_redraft">حذف وإعادة الصياغة</string>
<string name="confirm_delete_and_redraft_title">حذف وإعادة صياغة الرسالة</string>
<string name="pin_post">تدبيس على الصفحة الشخصية</string>
<string name="confirm_pin_post_title">تدبيس الرسالة على الصفحة الشخصية</string>
<string name="settings_show_federated_timeline">إظهار الخيط الفديرالي</string>
<string name="settings_contribute_fork">المساهمة في Megalodon</string>
<string name="accept_follow_request">قبول طلب المتابعة</string>
<string name="reject_follow_request">رفض طلب المتابعة</string>
<string name="lists_with_user">قوائم بها %s</string>
<string name="text_copied">تم النسخ إلى الحافظة</string>
<string name="add_bookmark">إضافة إلى الفواصل المرجعية</string>
<string name="remove_bookmark">إزالة من الفواصل المرجعية</string>

View File

@ -1,2 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="sk_confirm_delete_and_redraft">هل أنت متأكد أنك تريد حذف وإعادة صياغة هذا المنشور؟</string>
<string name="sk_visibility_unlisted">غير مدرج</string>
<string name="sk_list_timelines">القوائم</string>
<string name="sk_follow_requests">طلبات المتابعة</string>
<string name="sk_pinned_posts">مدبّس</string>
<string name="sk_delete_and_redraft">حذف وإعادة الصياغة</string>
<string name="sk_confirm_delete_and_redraft_title">حذف وإعادة صياغة الرسالة</string>
<string name="sk_pin_post">تدبيس على الصفحة الشخصية</string>
<string name="sk_confirm_pin_post_title">تدبيس الرسالة على الصفحة الشخصية</string>
<string name="sk_settings_show_federated_timeline">إظهار الخيط الفديرالي</string>
<string name="sk_settings_contribute">المساهمة في Megalodon</string>
<string name="sk_accept_follow_request">قبول طلب المتابعة</string>
<string name="sk_reject_follow_request">رفض طلب المتابعة</string>
<string name="sk_lists_with_user">قوائم بها %s</string>
</resources>

View File

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

View File

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

View File

@ -275,16 +275,15 @@
<string name="sk_settings_confirm_before_reblog">Confirmar antes de volver a publicar</string>
<string name="sk_reacted_with">reaccionó con %s</string>
<string name="sk_reacted">reaccionó</string>
<string name="sk_content_type_unspecified">No especificado</string>
<string name="sk_content_type_plain">Texto plano</string>
<string name="sk_content_type">Tipo del contenido</string>
<string name="sk_content_type_unspecified">Sin especificar</string>
<string name="sk_content_type_plain">Texto sin formato</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_default_content_type">Tipo de contenido predeterminado</string>
<string name="sk_settings_default_content_type_explanation">Esto te permite tener un tipo de contenido preseleccionado cuando creas una nueva publicación, sobreescribiendo el valor en \"Preferencias de publicación\".</string>
<string name="sk_content_type">Tipo de contenido</string>
<string name="sk_settings_content_types">Activar formato de publicación</string>
<string name="sk_settings_content_types_explanation">Permite configurar un tipo de contenido como Markdown cuando creas una publicación. Ten en cuenta que no todas las instancias lo permiten.</string>
<string name="sk_settings_show_new_posts_button">Botón \"Ver nuevas publicaciones\"</string>
<string name="sk_settings_content_types">Habilitar el formato de los mensajes</string>
<string name="sk_settings_default_content_type">Contenido por defecto</string>
<string name="sk_settings_content_types_explanation">Permite establecer un tipo de contenido como Markdown al crear una entrada. Ten en cuenta que no todas las instancias lo admiten.</string>
<string name="sk_settings_default_content_type_explanation">Permite preseleccionar un tipo de contenido al crear nuevas entradas, anulando el valor establecido en \"Preferencias de publicación\".</string>
</resources>

View File

@ -276,4 +276,21 @@
<string name="sk_settings_confirm_before_reblog">Confirmer avant de booster</string>
<string name="sk_reacted">a réagi</string>
<string name="sk_reacted_with">a réagi avec %s</string>
<string name="sk_content_type">Type de contenu</string>
<string name="sk_content_type_plain">Texte brut</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Activer la mise en forme du message</string>
<string name="sk_settings_default_content_type_explanation">Cela vous permet de présélectionner un type de contenu lors de la création de nouveaux messages, en remplaçant la valeur définie dans \"Préférences de publication\".</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_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>

View File

@ -90,7 +90,7 @@
<string name="sk_loading_fediverse_resource_title">Buscando no Fediverso</string>
<string name="sk_reblog_with_visibility">Impulsar con visibilidade</string>
<string name="sk_quote_post">Publicar acerca disto</string>
<string name="sk_undo_reblog">Desfacer o impulso</string>
<string name="sk_undo_reblog">Desfacer impulso</string>
<string name="sk_copy_link_to_post">Copiar ligazón á publicación</string>
<string name="sk_loading_resource_on_instance_title">Buscando en %s</string>
<string name="sk_open_with_account">Abrir con outra conta</string>
@ -275,4 +275,21 @@
<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">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>
<string name="sk_bubble_timeline_info_banner">Estas son as publicacións máis recentes da xente na burbulla do seu servidor Akkoma.</string>
<string name="sk_timeline_bubble">Burbulla</string>
<string name="sk_instance_info_unavailable">Información da instancia temporalmente non dispoñible</string>
<string name="sk_open_in_app">Abrir na aplicación</string>
<string name="sk_external_share_title">Compartir coa conta</string>
<string name="sk_external_share_or_open_title">Compartir ou abrir coa conta</string>
</resources>

View File

@ -101,6 +101,8 @@
<string name="confirm_block_domain_title">Արգելափակել տիրույթը</string>
<string name="confirm_block">Հաստատեք %s-ի արգելափակումը</string>
<string name="do_block">Արգելափակել</string>
<string name="do_unblock">Արգելաբացել</string>
<string name="button_blocked">Արգելափակված</string>
<string name="action_vote">Քվեարկել</string>
<string name="tap_to_reveal">Սեղմեք տեսնելու համար</string>
<string name="delete">Ջնջել</string>
@ -118,7 +120,9 @@
<string name="mentions">Նշումներ</string>
<string name="report_title">Զեկուցել %s-ի մասին</string>
<string name="report_reason_personal">Ինձ դուր չի գալիս</string>
<string name="report_reason_personal_subtitle">Դուք սա չեք ուզում տեսնել</string>
<string name="report_reason_spam">Սպամ է</string>
<string name="report_reason_spam_subtitle">Վնասակար հղումներ, կեղծում կամ կրկնվող պատասխաններ</string>
<string name="report_reason_violation">Խախտում է սերվերի կանոնները</string>
<string name="report_reason_violation_subtitle">Գիտեք, որ այն խախտում է կանոնները</string>
<string name="report_reason_other">Այլ բան է</string>
@ -157,6 +161,10 @@
<string name="add_image_description">Ավելացնել պատկերի նկարագրություն</string>
<string name="edit_image">Խմբագրել նկարը</string>
<string name="save">Պահպանել</string>
<string name="alt_text_hint">օր․՝ շունը նեղ աչքերով կասկածելի նայում է տեսախցիկին</string>
<string name="visibility_public">Հրապարակային</string>
<string name="visibility_followers_only">Միայն հետեւողներին</string>
<string name="visibility_private">Միայն նշածս մարդկանց</string>
<string name="search_all">Բոլորը</string>
<string name="search_people">Մարդիկ</string>
<string name="recent_searches">Վերջին որոնումներ</string>
@ -226,6 +234,7 @@
<string name="dismiss">Չեղարկել</string>
<string name="see_new_posts">Նոր գրառումներ</string>
<string name="follows_you">Հետեւում է ձեզ</string>
<string name="current_account">Ընթացիկ հաշիվ</string>
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
<plurals name="x_followers">
<item quantity="one">%,d հետեւորդ</item>
@ -241,13 +250,32 @@
</plurals>
<string name="timestamp_via_app">%1$s %2$s-ի միջոցով</string>
<string name="time_now">նոր</string>
<string name="post_info_reblogs">Տարածումներ</string>
<string name="post_info_favorites">Հավանումներ</string>
<string name="edit_history">Խմբագրել պատմությունը</string>
<string name="last_edit_at_x">Վերջին խմբագրում՝ %s</string>
<string name="time_just_now">հենց նոր</string>
<plurals name="x_seconds_ago">
<item quantity="one">%d վայրկյան առաջ</item>
<item quantity="other">%d վայրկյան առաջ</item>
</plurals>
<plurals name="x_minutes_ago">
<item quantity="one">%d րոպե առաջ</item>
<item quantity="other">%d րոպե առաջ</item>
</plurals>
<string name="edit_text_edited">Տեքստը փոփոխվել է</string>
<string name="edit_poll_added">Հարցումն ավելացել է</string>
<string name="edit_poll_edited">Հարցումը խմբագրվել է</string>
<string name="edit_poll_removed">Հարցումը հեռացվել է</string>
<string name="edit_media_added">Մեդիան ավելացվել է</string>
<string name="edit_media_removed">Մեդիան հեռացվել է</string>
<string name="edit_marked_sensitive">Նշվել է որպես դյուրազգաց</string>
<string name="file_size_bytes">%d բայթ</string>
<string name="file_size_kb">%.2f ԿԲ</string>
<string name="file_size_mb">%.2f ՄԲ</string>
<string name="file_size_gb">%.2f ԳԲ</string>
<string name="file_upload_progress">%1$s՝ %2$s-ից</string>
<string name="file_upload_time_remaining">մնացել է %s</string>
<!-- %s is version like 1.2.3 -->
<string name="update_available">%s տարբերակը պատրաստ է ներբեռնման։</string>
<!-- %s is version like 1.2.3 -->
@ -255,6 +283,7 @@
<!-- %s is file size -->
<string name="download_update">Ներբեռնել (%s)</string>
<string name="install_update">Տեղադրել</string>
<string name="privacy_policy_title">Ձեր գաղտնիությունը</string>
<string name="i_agree">Համաձայն եմ</string>
<string name="empty_list">Ցանկը դատարկ է</string>
<string name="instance_signup_closed">Սպասարկիչը գրանցումներ չի ընդունում։</string>
@ -274,9 +303,25 @@
<string name="not_accepting_new_members">Չի ընդունում նոր անդամներ</string>
<string name="signup_passwords_dont_match">Գաղտնաբառը չի համապատասխանում</string>
<string name="pick_server_for_me">Ընտրել իմ համար</string>
<string name="privacy_policy_explanation">Կարճ ասած՝ մենք ոչինչ չենք հավաքում։</string>
<!-- %s is server domain -->
<string name="profile_bio">Կենսագրություն</string>
<!-- Shown in a progress dialog when you tap "follow all" -->
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="signup_email_domain_blocked">%1$s-ը %2$s-ից գրանցումներ չի ընդունում։ Փորձեք ուրիշը կամ &lt;a&gt;ընտրեք ուրիշ սերվեր&lt;/a&gt;։</string>
<string name="signup_username_taken">Օգտանունը զբաղված է։</string>
<string name="spoiler_show">Ցույց տալ</string>
<string name="spoiler_hide">Թաքցնել</string>
<string name="save_changes">Պահպանել</string>
<string name="profile_timeline">Հոսք</string>
<string name="view_all">Դիտել բոլորը</string>
<string name="profile_endorsed_accounts">Հաշիվներ</string>
<string name="verified_link">Հաստատված հղում</string>
<string name="show">Ցույց տալ</string>
<string name="hide">Թաքցնել</string>
<string name="join_default_server">Միանալ %s-ին</string>
<string name="pick_server">Ընտրել ուրիշ սերվեր</string>
<string name="signup_or_login">կամ</string>
<string name="learn_more">Իմանալ ավելին</string>
<string name="welcome_to_mastodon">Բարի գալուստ Մաստոդոն</string>
<string name="welcome_paragraph1">Մաստոդոնը ապակենտրոնացված սոցցանց է, այսինքն՝ այն չի պատկանում մի ընկերության։ Այն բաղկացած է բազմաթիվ անկախ և կապակցված սերվերներից։</string>

View File

@ -276,4 +276,21 @@
<string name="sk_settings_confirm_before_reblog">Konfirmasi sebelum membagikan ulang</string>
<string name="sk_reacted_with">bereaksi dengan %s</string>
<string name="sk_reacted">bereaksi</string>
<string name="sk_content_type_plain">Teks biasa</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Aktifkan pemformatan kiriman</string>
<string name="sk_settings_default_content_type">Jenis konten bawaan</string>
<string name="sk_settings_default_content_type_explanation">Ini memungkinkan Anda untuk menerapkan jenis konten yang sudah ditentukan saat membuat kiriman baru, menimpa nilai yang ditetapkan dalam “Preferensi kiriman”.</string>
<string name="sk_content_type">Jenis konten</string>
<string name="sk_content_type_unspecified">Tidak ditentukan</string>
<string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string>
<string name="sk_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman 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>

View File

@ -8,14 +8,14 @@
<string name="error">Fout</string>
<string name="not_a_mastodon_instance">%s lijkt geen Mastodonserver te zijn.</string>
<string name="ok">OK</string>
<string name="preparing_auth">Verificatie aan het voorbereiden…</string>
<string name="preparing_auth">Verificatie voorbereiden…</string>
<string name="finishing_auth">Verificatie afronden…</string>
<string name="user_boosted">%s boostte</string>
<string name="in_reply_to">Als reactie op %s</string>
<string name="notifications">Meldingen</string>
<string name="user_followed_you">volgt jou</string>
<string name="user_sent_follow_request">heeft je een volgverzoek gestuurd</string>
<string name="user_favorited">markeerde als favoriet</string>
<string name="user_sent_follow_request">wil jou graag volgen</string>
<string name="user_favorited">markeerde bericht als favoriet</string>
<string name="notification_boosted">boostte jouw bericht</string>
<string name="poll_ended">poll is beëindigd</string>
<string name="time_seconds">%ds</string>
@ -47,20 +47,20 @@
<string name="button_follow">Volgen</string>
<string name="button_following">Volgend</string>
<string name="edit_profile">Profiel bewerken</string>
<string name="mention_user">Vermelden</string>
<string name="mention_user">%s vermelden</string>
<string name="share_user">%s delen</string>
<string name="mute_user">Negeren</string>
<string name="unmute_user">Niet langer negeren</string>
<string name="block_user">Blokkeren</string>
<string name="unblock_user">Deblokkeren</string>
<string name="mute_user">%s negeren</string>
<string name="unmute_user">%s niet langer negeren</string>
<string name="block_user">%s blokkeren</string>
<string name="unblock_user">%s deblokkeren</string>
<string name="report_user">%s rapporteren</string>
<string name="block_domain">Blokkeren</string>
<string name="unblock_domain">Deblokkeren</string>
<string name="block_domain">%s blokkeren</string>
<string name="unblock_domain">%s deblokkeren</string>
<plurals name="x_posts">
<item quantity="one">%,d bericht</item>
<item quantity="other">%,d berichten</item>
</plurals>
<string name="profile_joined">Geregistreerd op</string>
<string name="profile_joined">Geregistreerd</string>
<string name="done">Klaar</string>
<string name="loading">Aan het laden…</string>
<string name="field_label">Label</string>
@ -98,23 +98,23 @@
<item quantity="other">%d dagen resterend</item>
</plurals>
<plurals name="x_voters">
<item quantity="one">%,d persoon</item>
<item quantity="other">%,d mensen</item>
<item quantity="one">%,d stem</item>
<item quantity="other">%,d stemmen</item>
</plurals>
<string name="poll_closed">Gesloten</string>
<string name="confirm_mute_title">Account negeren</string>
<string name="confirm_mute">Bevestig om %s te negeren</string>
<string name="confirm_mute">Het negeren van %s bevestigen</string>
<string name="do_mute">Negeren</string>
<string name="confirm_unmute_title">Account niet langer negeren</string>
<string name="confirm_unmute">Bevestig om %s niet langer te negeren</string>
<string name="confirm_unmute">Het niet langer negeren van %s bevestigen</string>
<string name="do_unmute">Niet langer negeren</string>
<string name="confirm_block_title">Account blokkeren</string>
<string name="confirm_block_domain_title">Domein blokkeren</string>
<string name="confirm_block">Bevestig om %s te blokkeren</string>
<string name="confirm_block">Het blokkeren van %s bevestigen</string>
<string name="do_block">Blokkeren</string>
<string name="confirm_unblock_title">Account deblokkeren</string>
<string name="confirm_unblock_domain_title">Domein deblokkeren</string>
<string name="confirm_unblock">Bevestig om %s te deblokkeren</string>
<string name="confirm_unblock">Het deblokkeren van %s bevestigen</string>
<string name="do_unblock">Deblokkeren</string>
<string name="button_muted">Genegeerd</string>
<string name="button_blocked">Geblokkeerd</string>
@ -175,7 +175,7 @@
<string name="instance_catalog_subtitle">Kies een server gebaseerd op je interesses, regio, of voor algemene doeleinden. Je kunt nog steeds met iedereen in contact komen, ongeacht de server.</string>
<string name="search_communities">Servernaam of URL</string>
<string name="instance_rules_title">Serverregels</string>
<string name="instance_rules_subtitle">Door verder te gaan, ga je akkoord met het volgen van de regels ingesteld door de %s-moderators.</string>
<string name="instance_rules_subtitle">Door verder te gaan, ga je akkoord met het volgen van de regels ingesteld door de %s-moderatoren.</string>
<string name="signup_title">Account registreren</string>
<string name="edit_photo">bewerken</string>
<string name="display_name">Naam</string>
@ -193,14 +193,14 @@
<string name="category_games">Games</string>
<string name="category_general">Algemeen</string>
<string name="category_journalism">Journalistiek</string>
<string name="category_lgbt">LGBT</string>
<string name="category_lgbt">LHBTQIA+</string>
<string name="category_music">Muziek</string>
<string name="category_regional">Regionaal</string>
<string name="category_tech">Tech</string>
<string name="confirm_email_title">Controleer je Postvak In</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">Klik op de link die we je hebben gestuurd om %s te verifiëren. We wachten op je.</string>
<string name="confirm_email_didnt_get">Geen link gekregen?</string>
<string name="confirm_email_didnt_get">Geen verificatielink ontvangen?</string>
<string name="resend">Opnieuw verzenden</string>
<string name="open_email_app">E-mail-app openen</string>
<string name="resent_email">Bevestigingsmail verzonden</string>
@ -211,7 +211,7 @@
<string name="edit_image">Afbeelding bewerken</string>
<string name="save">Opslaan</string>
<string name="add_alt_text">Alt-tekst toevoegen</string>
<string name="alt_text_subtitle">Alt-tekst beschrijft uw foto\'s voor mensen met weinig of geen zicht. Probeer alleen genoeg details toe te voegen om de context te begrijpen.</string>
<string name="alt_text_subtitle">Alt-tekst beschrijft jouw foto\'s voor blinde of slechtziende mensen. Probeer alleen genoeg details toe te voegen om de context te begrijpen.</string>
<string name="alt_text_hint">bijv.: een hond die met versmalde ogen verdacht rondkijkt naar de camera.</string>
<string name="visibility_public">Openbaar</string>
<string name="visibility_followers_only">Alleen volgers</string>
@ -240,17 +240,17 @@
<string name="theme_dark">Donker</string>
<string name="theme_true_black">Echt zwart gebruiken</string>
<string name="settings_behavior">Gedrag</string>
<string name="settings_gif">Geanimeerde avatars en emoji\'s afspelen</string>
<string name="settings_gif">Geanimeerde profielfoto\'s en emoji\'s afspelen</string>
<string name="settings_custom_tabs">In-appbrowser gebruiken</string>
<string name="settings_notifications">Meldingen</string>
<string name="notify_me_when">Melding tonen wanneer</string>
<string name="notify_anyone">iedereen</string>
<string name="notify_anyone">iemand</string>
<string name="notify_follower">een volger</string>
<string name="notify_followed">iemand die ik volg</string>
<string name="notify_none">niemand</string>
<string name="notify_favorites">Mijn bericht als favoriet markeert</string>
<string name="notify_follow">Mij volgt</string>
<string name="notify_reblog">Boost mijn bericht</string>
<string name="notify_reblog">Mijn bericht boost</string>
<string name="notify_mention">Mij vermeldt</string>
<string name="settings_boring">De saaie zone</string>
<string name="settings_account">Accountinstellingen</string>
@ -272,7 +272,7 @@
<string name="new_post">Nieuw bericht</string>
<string name="button_reply">Reageren</string>
<string name="button_reblog">Boosten</string>
<string name="button_favorite">Toevoegen aan favorieten</string>
<string name="button_favorite">Als favoriet markeren</string>
<string name="button_share">Delen</string>
<string name="media_no_description">Media zonder beschrijving</string>
<string name="add_media">Media toevoegen</string>
@ -286,7 +286,7 @@
<string name="unfollowed_user">%s ontvolgd</string>
<string name="followed_user">Je volgt %s nu</string>
<string name="following_user_requested">Je volgverzoek is aan %s verstuurd</string>
<string name="open_in_browser">Openen in browser</string>
<string name="open_in_browser">In browser openen</string>
<string name="hide_boosts_from_user">Boosts van %s verbergen</string>
<string name="show_boosts_from_user">Boosts van %s tonen</string>
<string name="signup_reason">Waarom wil je je hier registreren?</string>
@ -309,7 +309,7 @@
<string name="trending_links_info_banner">Dit zijn nieuwsartikelen die populair zijn op jouw Mastodon-server.</string>
<string name="local_timeline_info_banner">Dit zijn de meest recente berichten van mensen die ook op jouw Mastodon-server zitten.</string>
<string name="dismiss">Sluiten</string>
<string name="see_new_posts">Nieuwe berichten bekijken</string>
<string name="see_new_posts">Nieuwe berichten</string>
<string name="load_missing_posts">Resterende berichten laden</string>
<string name="follow_back">Terugvolgen</string>
<string name="button_follow_pending">In afwachting</string>
@ -383,7 +383,7 @@
<string name="download_update">Downloaden (%s)</string>
<string name="install_update">Installeren</string>
<string name="privacy_policy_title">Jouw privacy</string>
<string name="privacy_policy_subtitle">Hoewel de Mastodon-app geen gegevens verzamelt, kan de server waar je je aanmeldt een ander beleid hebben.\n\nAls je het niet eens bent met het beleid voor %s, kun je teruggaan en een andere server kiezen.</string>
<string name="privacy_policy_subtitle">Hoewel de Mastodon-app geen gegevens verzamelt, kan de server waar je je aanmeldt een ander beleid hebben.\n\nAls je niet akkoord gaat met het beleid voor %s, kun je teruggaan en een andere server kiezen.</string>
<string name="i_agree">Ik ga akkoord</string>
<string name="empty_list">Deze lijst is leeg</string>
<string name="instance_signup_closed">Deze server accepteert geen nieuwe registraties.</string>
@ -416,10 +416,10 @@
<string name="profile_setup_explanation">Je kunt tot vier profielvelden toevoegen voor alles wat je wilt. Locatie, links, voornaamwoorden de mogelijkheden zijn eindeloos.</string>
<string name="popular_on_mastodon">Populair op Mastodon</string>
<string name="follow_all">Volg iedereen</string>
<string name="server_rules_disagree">Oneens</string>
<string name="server_rules_disagree">Ik ga niet akkoord</string>
<string name="privacy_policy_explanation">TL;DR: We verzamelen of verwerken niets.</string>
<!-- %s is server domain -->
<string name="server_policy_disagree">Oneens met %s</string>
<string name="server_policy_disagree">Niet akkoord met %s</string>
<string name="profile_bio">Bio</string>
<!-- Shown in a progress dialog when you tap "follow all" -->
<string name="sending_follows">Gevolgde gebruikers…</string>
@ -428,20 +428,21 @@
<string name="signup_username_taken">Deze gebruikersnaam wordt al gebruikt.</string>
<string name="spoiler_show">Alsnog tonen</string>
<string name="spoiler_hide">Opnieuw verbergen</string>
<string name="poll_multiple_choice">Selecteer een of meer</string>
<string name="poll_multiple_choice">Maak een of meerdere keuzes</string>
<string name="save_changes">Wijzigingen opslaan</string>
<string name="profile_featured">Aanbevolen</string>
<string name="profile_timeline">Tijdlijn</string>
<string name="view_all">Alles bekijken</string>
<string name="profile_endorsed_accounts">Accounts</string>
<string name="verified_link">Geverifieerde koppeling</string>
<string name="verified_link">Geverifieerde link</string>
<string name="show">Tonen</string>
<string name="hide">Verbergen</string>
<string name="join_default_server">Deelnemen aan %s</string>
<string name="join_default_server">Registreren op %s</string>
<string name="pick_server">Kies een andere server</string>
<string name="signup_or_login">of</string>
<string name="learn_more">Meer informatie</string>
<string name="welcome_to_mastodon">Welkom bij Mastodon</string>
<string name="welcome_paragraph1">Mastodon is een gedecentraliseerd sociaal netwerk, wat betekent dat geen enkel bedrijf het controleert. Het bestaat uit veel onafhankelijk opererende servers, allemaal verbonden met elkaar.</string>
<string name="welcome_paragraph1">Mastodon is een gedecentraliseerd sociaal netwerk, wat betekent dat geen enkel bedrijf het controleert. Het bestaat uit veel onafhankelijk opererende servers, allemaal met elkaar verbonden.</string>
<string name="what_are_servers">Wat zijn servers?</string>
<string name="welcome_paragraph2"><![CDATA[Elk Mastodon account wordt gehost op een server - elk met zijn eigen waarden, regels & admins. Het maakt niet uit welke server je kiest, je kunt mensen op elke server volgen en ermee communiceren.]]></string>
<string name="welcome_paragraph2"><![CDATA[Elk Mastodonaccount wordt op een server gehost - elk met diens eigen waarden, regels en beheerders. Het maakt niet uit welke server je kiest, je kunt mensen op elke server volgen en ermee communiceren.]]></string>
</resources>

View File

@ -251,8 +251,7 @@
<string name="sk_settings_hide_interaction">Verberg interactie knoppen</string>
<string name="sk_follow_as">Volgen met ander account</string>
<string name="sk_followed_as">Gevolgd met %s</string>
<string name="sk_settings_reply_visibility">Reactie zichtbaarheid</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>
<string name="sk_changelog">Changelog</string>
<string name="sk_settings_show_new_posts_button">Knop \"Toon nieuwe berichten\"</string>
</resources>

View File

@ -2,80 +2,47 @@
<resources>
<string name="sk_app_name">Megalodon</string>
<string name="sk_pinned_posts">Fixado</string>
<string name="sk_content_type_unspecified">Não especifícado</string>
<string name="sk_content_type_plain">Texto simples</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_default_content_type">Tipo de conteúdo predefinido</string>
<string name="sk_confirm_pin_post">Queres fixar esta publicação no teu perfil\?</string>
<string name="sk_confirm_unpin_post_title">Desfixar publicação do perfil</string>
<string name="sk_settings_load_new_posts">Carregar novas publicações automaticamente</string>
<string name="sk_user_post_notifications_on">Ligar notificações da publicação para %s</string>
<string name="sk_disable_marquee">Desativar deslocação de texto nas barras de título</string>
<string name="sk_poll_allow_multiple">Permitir escolha múltipla</string>
<string name="sk_language_name">%1$s (%2$s)</string>
<string name="sk_confirm_clear_recent_languages">De certeza que queres apagar os teus idiomas usados recentemente\?</string>
<string name="sk_settings_auth">Configurações de segurança</string>
<string name="sk_settings_rules">Regras</string>
<string name="sk_delete_notification_confirm">De certeza que queres apagar esta notificação\?</string>
<string name="sk_settings_publish_button_text_title">Personalizar o texto do botão de publicar</string>
<string name="sk_reblog_with_visibility">Partilhar com visibilidade</string>
<string name="sk_confirm_delete_draft">De certeza que queres apagar este rascunho\?</string>
<string name="sk_scheduled_too_soon_title">Hora agendada é demasiado cedo</string>
<string name="sk_schedule_post">Publicação agendada</string>
<string name="sk_settings_about_instance">Sobre a instância</string>
<string name="sk_remove_follower_confirm">Remover %s como seguidor bloqueando e desbloqueando imediatamente\?</string>
<string name="sk_notify_posts_info_banner">Se ativares as notificações de publicação para algumas pessoas as suas publicações vão aparecer aqui.</string>
<string name="sk_icon_language">Idioma</string>
<string name="sk_icon_aperture">Abertura</string>
<string name="sk_icon_briefcase">Pasta</string>
<string name="sk_edit_timelines">Editar linhas do tempo</string>
<string name="sk_save_draft_message">Queres guardar as alterações a este rascunho ou publicar agora\?</string>
<string name="sk_settings_hide_fab">Esconder automaticamente o botão Escrever</string>
<string name="sk_content_type">Tipo de conteúdo</string>
<string name="sk_settings_content_types">Permitir formatação da publicação</string>
<string name="sk_settings_content_types_explanation">Permitir configurar um tipo de conteúdo como Markdown ao criar uma publicação. Lembra-te que nem todas as instâncias suportam isto.</string>
<string name="sk_settings_default_content_type_explanation">Isto permite ter um tipo de conteúdo pré-selecionado ao criar publicações novas, substituindo o valor configurado nas \"Preferências de publicação\".</string>
<string name="sk_delete_and_redraft">Apagar e reescrever</string>
<string name="sk_confirm_delete_and_redraft_title">Apagar e reescrever publicação</string>
<string name="sk_confirm_delete_and_redraft">De certeza que querem apagar e reescrever esta publicação\?</string>
<string name="sk_pin_post">Fixar no perfil</string>
<string name="sk_confirm_pin_post_title">Fixar publicação no perfil</string>
<string name="sk_pinning">A fixar publicação…</string>
<string name="sk_unpin_post">Desfixar do perfil</string>
<string name="sk_confirm_unpin_post">De certeza que queres desfixar esta publicação\?</string>
<string name="sk_filtered">Filtrado: %s</string>
<string name="sk_expand">Expandir</string>
<string name="sk_collapse">Esconder</string>
<string name="sk_settings_collapse_long_posts">Esconder publicações muito grandes</string>
<string name="sk_unfinished_attachments">Reparar anexos\?</string>
<string name="sk_translate_post">Traduzir</string>
<string name="sk_translate_show_original">Mostrar original</string>
<string name="sk_post_language">Idioma: %s</string>
<string name="sk_available_languages">Idiomas disponíveis</string>
<string name="sk_unpinning">A desfixar publicação…</string>
<string name="sk_image_description">Descrição da imagem</string>
<string name="sk_visibility_unlisted">Não listado</string>
<string name="sk_settings_show_replies">Mostrar respostas</string>
<string name="sk_quoting_user">Citar %s</string>
<string name="sk_settings_reply_visibility">Visibilidade de resposta</string>
<string name="sk_settings_reply_visibility_all">Todas as respostas</string>
<string name="sk_settings_reply_visibility_following">Responder aos meus seguidores</string>
<string name="sk_settings_reply_visibility_self">Respostas a mim</string>
<string name="sk_settings_show_boosts">Mostrar partilhas</string>
<string name="sk_settings_show_interaction_counts">Mostrar contagem de interações</string>
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
<string name="sk_mark_media_as_sensitive">Marcar conteúdo como sensível</string>
<string name="sk_user_post_notifications_off">Desligar notificações para publicação para %s</string>
<string name="sk_settings_load_new_posts">Carregar publicações novas automaticamente</string>
<string name="sk_unpin_post">Desfixar do perfil</string>
<string name="sk_delete_and_redraft">Apagar e reescrever</string>
<string name="sk_confirm_delete_and_redraft_title">Apagar e reescrever publicação</string>
<string name="sk_confirm_delete_and_redraft">De certeza que quer apagar e reescrever a publicação\?</string>
<string name="sk_pin_post">Fixar no perfil</string>
<string name="sk_confirm_pin_post_title">Fixar publicação no perfil</string>
<string name="sk_confirm_pin_post">Queres fixar esta publicação ao teu perfil\?</string>
<string name="sk_pinning">A fixar publicação…</string>
<string name="sk_federated_timeline">Federação</string>
<string name="sk_federated_timeline_info_banner">Estas são as publicações mais recentes das pessoas na tua federação.</string>
<string name="sk_update_available">Megalodon %s está pronto a descarregar.</string>
<string name="sk_update_ready">Megalodon %s descarregado e pronto a instalar.</string>
<string name="sk_check_for_update">A verificar atualizações</string>
<string name="sk_no_update_available">Sem atualizações disponíveis</string>
<string name="sk_list_timelines">Listas</string>
<string name="sk_update_available">Megalodon %s pronto a descarregar.</string>
<string name="sk_follow_requests">Pedidos para seguir</string>
<string name="sk_accept_follow_request">Aceitar pedido para seguir</string>
<string name="sk_reject_follow_request">Rejeitar pedido para seguir</string>
<string name="sk_lists_with_user">Listas com %s</string>
<string name="sk_settings_always_reveal_content_warnings">Mostrar sempre avisos de conteúdo</string>
<string name="sk_example_domain">exemplo.social</string>
<string name="sk_disable_marquee">Desligar deslocamento de texto nas barras de título</string>
<string name="sk_settings_contribute">Contribuir para o Megalodon</string>
<string name="sk_settings_show_federated_timeline">Mostrar linha do tempo federada</string>
<string name="sk_notification_type_status">Publicações</string>
<string name="sk_notify_posts">Notificações da publicação</string>
<string name="sk_bookmark_as">Guardar com outra conta</string>
<string name="sk_bookmarked_as">Guardado como %s</string>
<string name="sk_already_bookmarked">Já guardado</string>
<string name="sk_favorite_as">Adicionar aos favoritos com outra conta</string>
<string name="sk_settings_profile">Configurar perfil</string>
<string name="sk_settings_filters">Configurar filtros</string>
<string name="sk_settings_auth">Configurações de segurança</string>
<string name="sk_settings_rules">Regras</string>
<string name="sk_settings_about">Sobre a aplicação</string>
<string name="sk_settings_donate">Doar</string>
<string name="sk_tabs_disable_swipe">Desligar deslocação entre separadores</string>
<string name="sk_settings_color_palette">Paleta de cores</string>
<string name="sk_color_palette_material3">Sistema</string>
<string name="sk_color_palette_pink">Rosa</string>
@ -85,107 +52,52 @@
<string name="sk_color_palette_brown">Castanho</string>
<string name="sk_color_palette_red">Vermelho</string>
<string name="sk_color_palette_yellow">Amarelo</string>
<string name="sk_translate_post">Traduzir</string>
<string name="sk_translate_show_original">Mostrar original</string>
<string name="sk_translated_using">Traduzido com %s</string>
<string name="sk_post_language">Idioma: %s</string>
<string name="sk_available_languages">Idiomas disponíveis</string>
<string name="sk_clear_recent_languages">Apagar idiomas usados recentemente</string>
<string name="sk_welcome_title">Bem-vindo/a!</string>
<string name="sk_welcome_text">Saudações do Tubarão! Para começar introduz o domínio da tua instância nativa abaixo.</string>
<string name="sk_example_domain">exemplo.social</string>
<string name="sk_tabs_disable_swipe">Desativar deslizar entre separadores</string>
<string name="sk_settings_profile">Configurar perfil</string>
<string name="sk_settings_posting">Preferências de notificação</string>
<string name="sk_settings_filters">Configurar filtros</string>
<string name="sk_settings_about">Sobre a aplicação</string>
<string name="sk_settings_donate">Doar</string>
<string name="sk_delete_notification">Apagar notificação</string>
<string name="sk_delete_notification_confirm_action">Apagar notificação</string>
<string name="sk_clear_all_notifications">Apagar todas as notificações</string>
<string name="sk_clear_all_notifications_confirm_action">Apagar tudo</string>
<string name="sk_clear_all_notifications_confirm">De certeza que queres apagar todas as notificações\?</string>
<string name="sk_enable_delete_notifications">Ativar apagar notificações</string>
<string name="sk_delete_notification_confirm">De certeza que quer apagar esta notificação\?</string>
<string name="sk_enable_delete_notifications">Ligar apagar notificações</string>
<string name="sk_settings_publish_button_text">Texto do botão de publicar</string>
<string name="sk_settings_translate_only_opened">Traduzir apenas publicações abertas</string>
<string name="sk_settings_translation_availability_note_available">%s suporta tradução!</string>
<string name="sk_settings_translation_availability_note_unavailable">%s não parece suportar tradução.</string>
<string name="sk_clear_all_notifications_confirm_action">Apagar tudo</string>
<string name="sk_clear_all_notifications_confirm">De certeza que quer apagar todas as notificações\?</string>
<string name="sk_loading_fediverse_resource_title">A procurar no Fediverso</string>
<string name="sk_loading_resource_on_instance_title">A procurar em %s</string>
<string name="sk_quote_post">Escrever sobre isto</string>
<string name="sk_hashtags_you_follow">Hashtags que segues</string>
<string name="sk_copy_link_to_post">Copiar ligação para a publicação</string>
<string name="sk_reblog_with_visibility">Partilhar com visibilidade</string>
<string name="sk_quote_post">Publicar sobre isto</string>
<string name="sk_open_with_account">Abrir com outra conta</string>
<string name="sk_resource_not_found">O recurso não foi encontrado</string>
<string name="sk_bookmark_as">Guardar com outra conta</string>
<string name="sk_bookmarked_as">Guardado como %s</string>
<string name="sk_already_bookmarked">Já guardado</string>
<string name="sk_favorite_as">Adicionar aos favoritos com outra conta</string>
<string name="sk_favorited_as">Adicionado aos favoritos como %s</string>
<string name="sk_already_favorited">Já adicionado aos favoritos</string>
<string name="sk_reply_as">Responder com outra conta</string>
<string name="sk_settings_uniform_icon_for_notifications">Ícone uniforme para todas as notificações</string>
<string name="sk_forward_report_to">Encaminhar para %s</string>
<string name="sk_unsent_posts">Publicações não enviadas</string>
<string name="sk_draft">Rascunho</string>
<string name="sk_schedule">Horário</string>
<string name="sk_confirm_delete_draft_title">Apagar rascunho</string>
<string name="sk_confirm_delete_scheduled_post_title">Apagar publicação agendada</string>
<string name="sk_confirm_delete_scheduled_post">De certeza que queres apagar esta publicação agendada\?</string>
<string name="sk_draft_or_schedule">Rascunho ou agendar</string>
<string name="sk_compose_draft">A publicação será guardada como rascunho.</string>
<string name="sk_compose_scheduled">Agendado para</string>
<string name="sk_confirm_delete_scheduled_post">De certeza que quer apagar esta publicação agendada\?</string>
<string name="sk_draft_or_schedule">Rascunho ou horário</string>
<string name="sk_compose_draft">Publicação guardada como rascunho.</string>
<string name="sk_draft_saved">Rascunho guardado</string>
<string name="sk_post_scheduled">Publicação agendada</string>
<string name="sk_scheduled_too_soon">A publicação tem de ser agendada com pelo menos 10 minutos de avanço.</string>
<string name="sk_confirm_save_draft">Guardar rascunho\?</string>
<string name="sk_scheduled_too_soon_title">Horário agendado é demasiado cedo</string>
<string name="sk_compose_scheduled">Agendada para</string>
<string name="sk_confirm_save_changes">Guardar alterações\?</string>
<string name="sk_mark_as_draft">Marcar como rascunho</string>
<string name="sk_schedule_or_draft">Agendar ou rascunho</string>
<string name="sk_schedule_post">Agendar publicação</string>
<string name="sk_compose_no_schedule">Não agendar</string>
<string name="sk_compose_no_draft">Não criar rascunho</string>
<string name="sk_settings_reduce_motion">Reduzir movimento nas animações</string>
<string name="sk_announcements">Anúncios</string>
<string name="sk_compose_no_draft">Sem rascunho</string>
<string name="sk_content_type_unspecified">Não especificado</string>
<string name="sk_content_type_plain">Texto simples</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Marcador</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Permitir formatação de publicação</string>
<string name="sk_settings_default_content_type">Tipo de conteúdo predefinido</string>
<string name="sk_settings_default_content_type_explanation">Permite pré-selecionar um tipo de conteúdo quando se criam novas publicações, substituindo o valor definido em \"Preferências de publicação\".</string>
<string name="sk_mark_as_read">Marcar como lido</string>
<string name="sk_settings_about_instance">Sobre a instância</string>
<string name="sk_settings_single_notification">Mostrar apenas uma notificação</string>
<string name="sk_create">Criar</string>
<string name="sk_create_list_title">Criar lista</string>
<string name="sk_list_name_hint">Nome da lista</string>
<string name="sk_list_replies_policy">Mostrar respostas a</string>
<string name="sk_list_replies_policy_list">Membros da lista</string>
<string name="sk_list_replies_policy_followed">utilizadores seguidos</string>
<string name="sk_list_replies_policy_none">ninguém</string>
<string name="sk_delete_list">Apagar lista</string>
<string name="sk_delete_list_confirm">De certeza que queres apagar a lista \"%s\"\?</string>
<string name="sk_edit_list_title">Editar lista</string>
<string name="sk_your_lists">As tuas listas</string>
<string name="sk_timeline_home">Página principal</string>
<string name="sk_timeline_local">Local</string>
<string name="sk_timeline_federated">Federação</string>
<string name="sk_recent_searches_placeholder">Escrever para começar a procurar</string>
<string name="sk_remove_follower">Remover como seguidor</string>
<string name="sk_remove_follower_confirm">Remover %s como seguidor bloqueando e logo a seguir desbloqueando\?</string>
<string name="sk_do_remove_follower">Remover</string>
<string name="sk_remove_follower_success">Seguidor removido com sucesso</string>
<string name="sk_changelog">Registo de alterações</string>
<string name="sk_alt_text_missing_title">Sem descrição de imagem</string>
<string name="sk_alt_text_missing">Pelo menos um anexo não contém uma descrição.</string>
<string name="sk_changelog">Registo de mudanças</string>
<string name="sk_alt_text_missing">Pelo menos um anexo não contém descrição.</string>
<string name="sk_publish_anyway">Publicar assim mesmo</string>
<string name="sk_settings_disable_alt_text_reminder">Desativar o lembrete para adicionar descrição de imagem</string>
<string name="sk_settings_disable_alt_text_reminder">Desligar lembrete para adicionar texto alternativo</string>
<string name="sk_timelines">Linhas do tempo</string>
<string name="sk_timeline_posts">Publicações</string>
<string name="sk_timelines_add">Adicionar</string>
<string name="sk_timeline">Linha do tempo</string>
<string name="sk_list">Lista</string>
<string name="sk_hashtag">Hashtag</string>
<string name="sk_pin_timeline">Fixar linha do tempo</string>
<string name="sk_unpin_timeline">Desfixar linha do tempo</string>
<string name="sk_pinned_timeline">Fixar à página principal</string>
<string name="sk_unpinned_timeline">Desfixar da página principal</string>
<string name="sk_remove">Remover</string>
<string name="sk_timeline_icon">Ícone</string>
<string name="sk_icon_heart">Coração</string>
<string name="sk_icon_star">Estrela</string>
<string name="sk_icon_city">Cidade</string>
<string name="sk_icon_cat">Gato</string>
<string name="sk_icon_dog">Cão</string>
<string name="sk_icon_rabbit">Coelho</string>
@ -193,6 +105,7 @@
<string name="sk_icon_balloon">Balão</string>
<string name="sk_icon_image">Imagem</string>
<string name="sk_icon_bot">Robô</string>
<string name="sk_icon_language">Idioma</string>
<string name="sk_icon_location">Localização</string>
<string name="sk_icon_megaphone">Megafone</string>
<string name="sk_icon_microphone">Microfone</string>
@ -201,6 +114,125 @@
<string name="sk_icon_coffee">Café</string>
<string name="sk_icon_laugh">Riso</string>
<string name="sk_icon_news">Notícias</string>
<string name="sk_no_results">Sem resultados</string>
<string name="sk_save_draft">Guardar rascunho\?</string>
<string name="sk_no_alt_text">Sem texto alternativo disponível</string>
<string name="sk_settings_show_alt_indicator">Indicador de textos alternativos</string>
<string name="sk_settings_show_no_alt_indicator">Indicador para textos alternativos ausentes</string>
<string name="sk_updater_enable_pre_releases">Ligar versões beta</string>
<string name="sk_inline_direct">apenas-mencionados</string>
<string name="sk_separator">.</string>
<string name="sk_instance_features">Recursos da instância</string>
<string name="sk_settings_support_local_only">O servidor só suporta publicações locais</string>
<string name="sk_settings_glitch_instance">Glitch em modo local</string>
<string name="sk_settings_glitch_mode_explanation">Liga isto se a tua instância nativa corre no Glitch. Não é necessário para Hometown ou Akkoma.</string>
<string name="sk_signed_up">inscrito</string>
<string name="sk_reported">reportado</string>
<string name="sk_sign_ups">Inscrições</string>
<string name="sk_notify_poll_results">Resultados do inquérito</string>
<string name="sk_follow_as">Seguir com outra conta</string>
<string name="sk_confirm_unpin_post">Tem a certeza que quer desfixar esta publicação\?</string>
<string name="sk_user_post_notifications_off">Desligar notificações de publicação para %s</string>
<string name="sk_federated_timeline_info_banner">Estas são as publicações mais recentes das pessoas na tua federação.</string>
<string name="sk_list_timelines">Listas</string>
<string name="sk_reject_follow_request">Rejeitar pedido para seguir</string>
<string name="sk_lists_with_user">Listas com %s</string>
<string name="sk_settings_always_reveal_content_warnings">Mostrar sempre avisos de conteúdo</string>
<string name="sk_poll_allow_multiple">Permitir escolhas múltiplas</string>
<string name="sk_translated_using">Traduzido por %s</string>
<string name="sk_clear_recent_languages">Apagar idiomas usados recentemente</string>
<string name="sk_confirm_clear_recent_languages">De certeza que quer apagar os seus idiomas usados recentemente\?</string>
<string name="sk_welcome_title">Bem-vindo/a!</string>
<string name="sk_welcome_text">Saudações do tubarão! Para começar, introduz o nome do domínio da tua instância nativa abaixo.</string>
<string name="sk_settings_posting">Preferências de publicação</string>
<string name="sk_delete_notification">Apagar notificação</string>
<string name="sk_delete_notification_confirm_action">Apagar notificação</string>
<string name="sk_clear_all_notifications">Apagar todas as notificações</string>
<string name="sk_settings_publish_button_text_title">Personalizar o texto do botão de publicar</string>
<string name="sk_settings_translate_only_opened">Traduzir apenas publicações abertas</string>
<string name="sk_settings_translation_availability_note_available">%s suporta tradução!</string>
<string name="sk_settings_translation_availability_note_unavailable">%s não parece suportar tradução.</string>
<string name="sk_loading_resource_on_instance_title">A procurar em %s</string>
<string name="sk_undo_reblog">Desfazer partilha</string>
<string name="sk_hashtags_you_follow">Hastags que segues</string>
<string name="sk_copy_link_to_post">Copiar ligação para publicação</string>
<string name="sk_forward_report_to">Encaminhar para %s</string>
<string name="sk_confirm_delete_draft">De certeza que pretende apagar este rascunho\?</string>
<string name="sk_post_scheduled">Publicação agendada</string>
<string name="sk_scheduled_too_soon">A publicação tem de ser agendada pelo menos 10 minutos no futuro.</string>
<string name="sk_confirm_save_draft">Guardar rascunho\?</string>
<string name="sk_schedule_or_draft">Horário ou rascunho</string>
<string name="sk_settings_reduce_motion">Reduzir movimento em animações</string>
<string name="sk_announcements">Anúncios</string>
<string name="sk_recent_searches_placeholder">Escrever para iniciar a procura</string>
<string name="sk_remove_follower">Remover como seguidor</string>
<string name="sk_alt_text_missing_title">Texto alternativo em falta</string>
<string name="sk_notify_posts_info_banner">Se ligar as notificações de publicação para algumas pessoas, as suas publicações novas aparecerão aqui.</string>
<string name="sk_pin_timeline">Fixar linha do tempo</string>
<string name="sk_icon_city">Cidade</string>
<string name="sk_icon_gauge">Régua</string>
<string name="sk_icon_pin">Pionés</string>
<string name="sk_notification_type_update">Publicações editadas</string>
<string name="sk_searching">À procura…</string>
<string name="sk_save_draft_message">Guardar alterações ou publicar o rascunho agora\?</string>
<string name="sk_settings_see_new_posts_button">Botão \"Ver publicações novas\"</string>
<string name="sk_inline_local_only">apenas-local</string>
<string name="sk_local_only">Apenas instância local</string>
<string name="sk_settings_local_only_explanation">A tua instância nativa tem de suportar publicações locais para isto funcionar. As versões modificadas do Mastodon suportam mas o Mastodon não.</string>
<string name="sk_settings_server_version">Versão do servidor: %s</string>
<string name="sk_settings_hide_interaction">Esconder botões interativos</string>
<string name="sk_settings_prefix_reply_cw_with_re">Adicionar \"re\" a respostas com AC</string>
<string name="sk_in_reply">Em resposta</string>
<string name="sk_content_type">Tipo de conteúdo</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_settings_content_types_explanation">Permite configurar um tipo de conteúdo como Markdown na criação de uma publicação. Lembra-te que nem todas as instâncias permitem isto.</string>
<string name="sk_confirm_unpin_post_title">Desfixar publicação do perfil</string>
<string name="sk_quoting_user">Citar %s</string>
<string name="sk_settings_reply_visibility">Visibilidade da resposta</string>
<string name="sk_settings_reply_visibility_all">Todas as respostas</string>
<string name="sk_settings_reply_visibility_following">Respostas para os meus seguidores</string>
<string name="sk_settings_reply_visibility_self">Respostas para mim</string>
<string name="sk_settings_show_interaction_counts">Mostrar número de interações</string>
<string name="sk_settings_app_version">Megalodon %1$s (%2$d)</string>
<string name="sk_mark_media_as_sensitive">Marcar conteúdo como sensível</string>
<string name="sk_user_post_notifications_on">Ligar notificações de publicação para %s</string>
<string name="sk_update_ready">Megalodon %s descarregado e pronto a instalar.</string>
<string name="sk_check_for_update">A verificar atualizações</string>
<string name="sk_no_update_available">Sem atualizações disponíveis</string>
<string name="sk_settings_show_federated_timeline">Mostrar linha do tempo unificada</string>
<string name="sk_notification_type_status">Publicações</string>
<string name="sk_notify_posts">Notificações de publicação</string>
<string name="sk_resource_not_found">Recurso não encontrado</string>
<string name="sk_favorited_as">Adicionado aos favoritos como %s</string>
<string name="sk_already_favorited">Já adicionado aos favoritos</string>
<string name="sk_reblog_as">Partilhar com outra conta</string>
<string name="sk_reply_as">Responder com outra conta</string>
<string name="sk_settings_uniform_icon_for_notifications">Ícone uniforme para todas as notificações</string>
<string name="sk_list_name_hint">Nome da Lista</string>
<string name="sk_list_replies_policy">Mostrar respostas a</string>
<string name="sk_list_replies_policy_list">Membros da lista</string>
<string name="sk_list_replies_policy_followed">Utilizadores seguidos</string>
<string name="sk_list_replies_policy_none">ninguém</string>
<string name="sk_delete_list">Apagar lista</string>
<string name="sk_delete_list_confirm">De certeza que quer apagar a lista \"%s\"\?</string>
<string name="sk_edit_list_title">Editar lista</string>
<string name="sk_your_lists">As tuas listas</string>
<string name="sk_timeline_home">Página principal</string>
<string name="sk_timeline_local">Local</string>
<string name="sk_timeline_federated">Federação</string>
<string name="sk_timeline_posts">Publicações</string>
<string name="sk_timelines_add">Adicionar</string>
<string name="sk_timeline">Linha do tempo</string>
<string name="sk_list">Lista</string>
<string name="sk_hashtag">Hashtag</string>
<string name="sk_unpin_timeline">Desfixar linha do tempo</string>
<string name="sk_pinned_timeline">Fixado à página principal</string>
<string name="sk_unpinned_timeline">Desfixado da página principal</string>
<string name="sk_reply_line_above_avatar">Linha \"Em resposta a\" por cima da foto de perfil</string>
<string name="sk_remove">Remover</string>
<string name="sk_timeline_icon">ícone</string>
<string name="sk_icon_heart">Coração</string>
<string name="sk_icon_star">Estrela</string>
<string name="sk_icon_pi">Pi</string>
<string name="sk_icon_color_palette">Paleta de cores</string>
<string name="sk_icon_academic_cap">Chapéu académico</string>
@ -213,7 +245,8 @@
<string name="sk_icon_train">Comboio</string>
<string name="sk_icon_clapper_board">Claquete</string>
<string name="sk_icon_leaves">Folhas</string>
<string name="sk_icon_sport">Desporto</string>
<string name="sk_icon_sport">Desposto</string>
<string name="sk_icon_aperture">Abertura</string>
<string name="sk_icon_music">Música</string>
<string name="sk_icon_people">Pessoas</string>
<string name="sk_icon_health">Saúde</string>
@ -225,64 +258,31 @@
<string name="sk_icon_map">Mapa</string>
<string name="sk_icon_math_formula">Fórmula matemática</string>
<string name="sk_icon_backpack">Mochila</string>
<string name="sk_icon_briefcase">Pasta</string>
<string name="sk_icon_fire">Fogo</string>
<string name="sk_icon_bug">Inseto</string>
<string name="sk_icon_pizza">Pizza</string>
<string name="sk_icon_gavel">Martelo</string>
<string name="sk_icon_gauge">Régua</string>
<string name="sk_icon_headphones">Auscultadores</string>
<string name="sk_icon_human">Humano</string>
<string name="sk_icon_globe">Globo</string>
<string name="sk_icon_pin">Pionés</string>
<string name="sk_edit_timeline">Editar linha do tempo</string>
<string name="sk_edit_timelines">Editar linhas do tempo</string>
<string name="sk_alt_button">ALT</string>
<string name="sk_post_edited">editado</string>
<string name="sk_notification_type_update">Publicações editadas</string>
<string name="sk_attach_file">Anexar ficheiro</string>
<string name="sk_searching">À procura…</string>
<string name="sk_no_results">Sem resultados</string>
<string name="sk_save_draft">Guardar rascunho\?</string>
<string name="sk_no_alt_text">Sem descrição de imagem</string>
<string name="sk_settings_show_alt_indicator">Indicador para descrições de imagem</string>
<string name="sk_settings_show_no_alt_indicator">Indicador para descrições de imagem ausentes</string>
<string name="sk_updater_enable_pre_releases">Ativar versões beta</string>
<string name="sk_inline_local_only">apenas local</string>
<string name="sk_inline_direct">apenas mencionado</string>
<string name="sk_separator">.</string>
<string name="sk_local_only">Apenas instância local</string>
<string name="sk_instance_features">Recursos da instância</string>
<string name="sk_settings_support_local_only">O servidor suporta publicações locais</string>
<string name="sk_settings_local_only_explanation">A tua instância nativa tem de suportar publicações locais para isto funcionar. A maioria das versões modificadas do Mastodon suportam mas o Mastodon não.</string>
<string name="sk_settings_glitch_instance">Modo local do Glitch</string>
<string name="sk_settings_glitch_mode_explanation">Ativar isto se a tua instância nativa corre no Glitch. Não é necessário para Hometown ou Akkoma.</string>
<string name="sk_signed_up">inscrito</string>
<string name="sk_reported">denunciado</string>
<string name="sk_follow_as">Seguir com outra conta</string>
<string name="sk_followed_as">A seguir com %s</string>
<string name="sk_settings_show_new_posts_button">Botão \"Mostrar novas publicações\"</string>
<string name="sk_undo_reblog">Desfazer partilha</string>
<string name="sk_reblog_as">Partilhar com outra conta</string>
<string name="sk_reblogged_as">Partilhado com %s</string>
<string name="sk_already_reblogged">Já partilhado</string>
<string name="sk_notify_update">Edita uma publicação partilhada</string>
<string name="sk_reacted">reagiu</string>
<string name="sk_new_reports">Novas denuncias</string>
<string name="sk_filtered">Filtrado: %s</string>
<string name="sk_sign_ups">Registos</string>
<string name="sk_reacted_with">reagiu com %s</string>
<string name="sk_settings_server_version">Versão do servidor: %s</string>
<string name="sk_notify_poll_results">Resultados do inquérito</string>
<string name="sk_settings_prefix_reply_cw_with_re">Mostrar \"re:\" ao responder a AC</string>
<string name="sk_expand">Expandir</string>
<string name="sk_collapse">Esconder</string>
<string name="sk_settings_collapse_long_posts">Esconder publicações muito longas</string>
<string name="sk_unfinished_attachments">Corrigir anexos\?</string>
<string name="sk_unfinished_attachments_message">Alguns anexos ainda não acabaram de carregar.</string>
<string name="sk_settings_hide_interaction">Esconder botões de interação</string>
<string name="sk_notification_action_replied">Resposta enviada a %s</string>
<string name="sk_in_reply">Em resposta</string>
<string name="sk_reply_line_above_avatar">Linha \"Em resposta a\" acima da foto de perfil</string>
<string name="sk_show_thread">Mostrar fio</string>
<string name="sk_compact_reblog_reply_line">Linha partilhar/responder compacta</string>
<string name="sk_settings_confirm_before_reblog">Confirmar antes de partilhar</string>
<string name="sk_language_name">%1$s (%2$s)</string>
<string name="sk_reblogged_as">Partilhado como %s</string>
<string name="sk_already_reblogged">Já partilhado</string>
<string name="sk_notify_update">Editar uma publicação partilhada</string>
<string name="sk_compact_reblog_reply_line">Linha compacta partilhar/responder</string>
<string name="sk_reacted_with">reagiu com %s</string>
<string name="sk_reacted">reagiu</string>
<string name="sk_new_reports">Novas denúncias</string>
<string name="sk_unfinished_attachments_message">Alguns anexos ainda não carregaram.</string>
<string name="sk_followed_as">Seguido com %s</string>
<string name="sk_settings_hide_fab">Esconder automaticamente o botão Escrever</string>
<string name="sk_show_thread">Mostrar fio</string>
</resources>

View File

@ -1,2 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="sk_app_name">Megalodon</string>
<string name="sk_pinned_posts">Fixado</string>
<string name="sk_delete_and_redraft">Apagar e reescrever</string>
<string name="sk_confirm_delete_and_redraft">Tem a certeza que pretende apagar e reescrever esta publicação\?</string>
<string name="sk_confirm_unpin_post">Tem a certeza que deseja desafixar esta publicação\?</string>
<string name="sk_settings_reply_visibility_all">Todas as respostas</string>
<string name="sk_settings_load_new_posts">Carregar novas publicações automaticamente</string>
<string name="sk_user_post_notifications_off">Desligar as notificações de publicação para %s</string>
<string name="sk_federated_timeline_info_banner">Estas são as publicações mais recentes das pessoas na tua federação.</string>
<string name="sk_confirm_delete_and_redraft_title">Apagar e reescrever publicação</string>
<string name="sk_pin_post">Fixar no perfil</string>
<string name="sk_confirm_pin_post_title">Fixar publicação no perfil</string>
<string name="sk_confirm_pin_post">Deseja fixar esta publicação ao seu perfil\?</string>
<string name="sk_pinning">A fixar a publicação…</string>
<string name="sk_unpin_post">Desafixar do perfil</string>
<string name="sk_confirm_unpin_post_title">Desafixar publicação do perfil</string>
<string name="sk_unpinning">A desafixar publicação…</string>
<string name="sk_image_description">Descrição da imagem</string>
<string name="sk_visibility_unlisted">Não listado</string>
<string name="sk_settings_show_replies">Mostrar respostas</string>
<string name="sk_quoting_user">Citação %s</string>
<string name="sk_settings_reply_visibility">Visibilidade da resposta</string>
<string name="sk_clear_recent_languages">Apagar idiomas usados recentemente</string>
<string name="sk_settings_reply_visibility_following">Respostas aos meus comentários</string>
<string name="sk_settings_reply_visibility_self">Respostas a mim</string>
<string name="sk_settings_show_boosts">Mostrar impulsionamentos</string>
<string name="sk_settings_show_interaction_counts">Mostrar contagem de interações</string>
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
<string name="sk_mark_media_as_sensitive">Marcar conteúdo como sensível</string>
<string name="sk_user_post_notifications_on">Ligar as notificações de publicação para %s</string>
<string name="sk_federated_timeline">Federação</string>
<string name="sk_update_available">Megalodon %s pronto a descarregar.</string>
<string name="sk_update_ready">Megalodon %s descarregado e pronto a instalar.</string>
<string name="sk_check_for_update">A verificar atualizações</string>
<string name="sk_no_update_available">Sem atualizações disponíveis</string>
<string name="sk_list_timelines">Listas</string>
<string name="sk_follow_requests">Pedidos para seguir</string>
<string name="sk_accept_follow_request">Aceitar pedido para seguir</string>
<string name="sk_reject_follow_request">Rejeitar pedido para seguir</string>
<string name="sk_lists_with_user">Listas com %s</string>
<string name="sk_settings_always_reveal_content_warnings">Mostrar sempre avisos de conteúdo</string>
</resources>

View File

@ -472,7 +472,7 @@
<string name="spoiler_hide">Спрятать повторно</string>
<string name="poll_multiple_choice">Выберите один или более</string>
<string name="save_changes">Сохранить изменения</string>
<string name="profile_featured">Возможности</string>
<string name="profile_featured">Избранное</string>
<string name="profile_timeline">Лента</string>
<string name="view_all">Посмотреть все</string>
<string name="profile_endorsed_accounts">Учётные записи</string>
@ -480,6 +480,7 @@
<string name="show">Показать</string>
<string name="hide">Скрыть</string>
<string name="join_default_server">Присоединиться к %s</string>
<string name="pick_server">Выбрать другой сервер</string>
<string name="signup_or_login">или</string>
<string name="learn_more">Узнать больше</string>
<string name="welcome_to_mastodon">Добро пожаловать в Mastodon</string>

View File

@ -412,4 +412,7 @@
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="signup_email_domain_blocked">%1$s tillåter inte registrering från %2$s. Prova en annan eller &lt;a&gt;välj en annan server&lt;/a&gt;.</string>
<string name="signup_username_taken">Det här användarnamnet är redan taget.</string>
<string name="save_changes">Spara ändringar</string>
<string name="signup_or_login">eller</string>
<string name="welcome_to_mastodon">Välkommen till Mastodon</string>
</resources>

View File

@ -275,4 +275,21 @@
<string name="sk_settings_confirm_before_reblog">Підтверджувати поширення</string>
<string name="sk_reacted">реагує</string>
<string name="sk_reacted_with">реагує з %s</string>
<string name="sk_content_type_unspecified">Не визначено</string>
<string name="sk_content_type_plain">Звичайний текст</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">Увімкнути форматування допису</string>
<string name="sk_settings_default_content_type">Типовий тип вмісту</string>
<string name="sk_content_type">Тип вмісту</string>
<string name="sk_settings_default_content_type_explanation">Це дозволяє вам попередньо вибрати тип вмісту під час написання нових дописів, замінивши значення, встановлене в «Налаштуваннях постингу».</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_settings_content_types_explanation">Дозволяє налаштувати тип вмісту, наприклад, Markdown, під час написання допису. Зауважте, що не всі сервери підтримують цю функцію.</string>
<string name="sk_open_in_app">Відкрити у застосунку</string>
<string name="sk_external_share_title">Поділитися через обліковий запис</string>
<string name="sk_bubble_timeline_info_banner">Це найновіші дописи людей у бульбашці вашого сервера 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>

View File

@ -4,33 +4,57 @@
<color name="m3_navigation_bar_bg">@android:color/system_neutral1_50</color>
<color name="m3_gray_1000">@android:color/system_neutral1_1000</color>
<color name="m3_gray_900">@android:color/system_neutral1_900</color>
<color name="m3_gray_800t">@android:color/system_neutral1_800</color>
<color name="m3_gray_800">@android:color/system_neutral1_800</color>
<color name="m3_gray_700">@android:color/system_neutral1_700</color>
<color name="m3_gray_600">@android:color/system_neutral1_600</color>
<color name="m3_gray_500">@android:color/system_neutral1_500</color>
<color name="m3_gray_400">@android:color/system_neutral1_400</color>
<color name="m3_gray_300">@android:color/system_neutral1_300</color>
<color name="m3_gray_200">@android:color/system_neutral1_200</color>
<color name="m3_gray_100">@android:color/system_neutral1_100</color>
<color name="m3_gray_50t">@android:color/system_neutral1_50</color>
<color name="m3_gray_50">@android:color/system_neutral1_50</color>
<color name="m3_gray_25">@android:color/system_neutral1_10</color>
<color name="m3_neutral1_900">@android:color/system_neutral1_900</color>
<color name="m3_neutral1_800t">@android:color/system_neutral1_800</color>
<color name="m3_neutral1_800">@android:color/system_neutral1_800</color>
<color name="m3_neutral1_700">@android:color/system_neutral1_700</color>
<color name="m3_neutral1_600">@android:color/system_neutral1_600</color>
<color name="m3_neutral1_500">@android:color/system_neutral1_500</color>
<color name="m3_neutral1_400">@android:color/system_neutral1_400</color>
<color name="m3_neutral1_300">@android:color/system_neutral1_300</color>
<color name="m3_neutral1_200">@android:color/system_neutral1_200</color>
<color name="m3_neutral1_100">@android:color/system_neutral1_100</color>
<color name="m3_neutral1_50t">@android:color/system_neutral1_50</color>
<color name="m3_neutral1_50">@android:color/system_neutral1_50</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_primary_50">@android:color/system_accent1_50</color>
<color name="m3_primary_100">@android:color/system_accent1_100</color>
<color name="m3_primary_200">@android:color/system_accent1_200</color>
<color name="m3_primary_300">@android:color/system_accent1_300</color>
<color name="m3_primary_400">@android:color/system_accent1_400</color>
<color name="m3_primary_500">@android:color/system_accent1_500</color>
<color name="m3_primary_600">@android:color/system_accent1_600</color>
<color name="m3_primary_700">@android:color/system_accent1_700</color>
<color name="m3_primary_800">@android:color/system_accent1_800</color>
<color name="m3_primary_900">@android:color/system_accent1_900</color>
<color name="m3_accent1_25">@android:color/system_accent1_10</color>
<color name="m3_accent1_50">@android:color/system_accent1_50</color>
<color name="m3_accent1_100">@android:color/system_accent1_100</color>
<color name="m3_accent1_200">@android:color/system_accent1_200</color>
<color name="m3_accent1_300">@android:color/system_accent1_300</color>
<color name="m3_accent1_400">@android:color/system_accent1_400</color>
<color name="m3_accent1_500">@android:color/system_accent1_500</color>
<color name="m3_accent1_600">@android:color/system_accent1_600</color>
<color name="m3_accent1_700">@android:color/system_accent1_700</color>
<color name="m3_accent1_800">@android:color/system_accent1_800</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 -->
<color name="m3_sys_light_primary">@android:color/system_accent1_600</color>

View File

@ -11,7 +11,7 @@
<string name="preparing_auth">Chuẩn bị xác thực…</string>
<string name="finishing_auth">Hoàn tất xác thực…</string>
<string name="user_boosted">%s đăng lại</string>
<string name="in_reply_to">%s viết tiếp</string>
<string name="in_reply_to">Trả lời %s</string>
<string name="notifications">Thông báo</string>
<string name="user_followed_you">đã theo dõi bạn</string>
<string name="user_sent_follow_request">đã yêu cầu theo dõi bạn</string>
@ -127,7 +127,7 @@
<item quantity="other">%d người đang thảo luận</item>
</plurals>
<plurals name="discussed_x_times">
<item quantity="other">Đề cập %d lần</item>
<item quantity="other">Chia sẻ %d lượt</item>
</plurals>
<string name="report_title">Báo cáo %s</string>
<string name="report_choose_reason">Có vấn đề gì với tút này?</string>
@ -150,8 +150,8 @@
<string name="sending_report">Đang gửi báo cáo…</string>
<string name="report_sent_title">Cảm ơn đã báo cáo, chúng tôi sẽ xem xét kỹ.</string>
<string name="report_sent_subtitle">Trong lúc chờ chúng tôi xem xét, bạn có thể áp dụng hành động với @%s.</string>
<string name="unfollow_user">Ngưng theo dõi %s</string>
<string name="unfollow">Ngưng theo dõi</string>
<string name="unfollow_user">Bỏ theo dõi %s</string>
<string name="unfollow">Bỏ theo dõi</string>
<string name="mute_user_explain">Bạn sẽ không thấy tút hoặc lượt đăng lại của họ trên bảng tin. Họ không biết rằng bạn ẩn họ.</string>
<string name="block_user_explain">Họ sẽ không thể theo dõi hoặc đọc tút của bạn, nhưng họ có thể hiểu bạn đã chặn họ.</string>
<string name="report_personal_title">Không muốn xem thứ này?</string>
@ -223,7 +223,7 @@
<string name="theme_auto">Tự động</string>
<string name="theme_light">Sáng</string>
<string name="theme_dark">Tối</string>
<string name="theme_true_black">Chế độ tối chân thật</string>
<string name="theme_true_black">Chế độ tương phản cao</string>
<string name="settings_behavior">Thao tác</string>
<string name="settings_gif">Ảnh đại diện và emoji động</string>
<string name="settings_custom_tabs">Dùng trình duyệt tích hợp</string>
@ -268,7 +268,7 @@
<string name="my_profile">Hồ sơ của tôi</string>
<string name="media_viewer">Xem media</string>
<string name="follow_user">Theo dõi %s</string>
<string name="unfollowed_user">Ngưng theo dõi %s</string>
<string name="unfollowed_user">Bỏ theo dõi %s</string>
<string name="followed_user">Bạn đã theo dõi %s</string>
<string name="following_user_requested">Yêu cầu theo dõi %s</string>
<string name="open_in_browser">Mở trong trình duyệt</string>
@ -317,8 +317,8 @@
</plurals>
<string name="timestamp_via_app">%1$s qua %2$s</string>
<string name="time_now">vừa xong</string>
<string name="post_info_reblogs">Lượt đăng lại</string>
<string name="post_info_favorites">Lượt thích</string>
<string name="post_info_reblogs">Đăng lại</string>
<string name="post_info_favorites">Thích</string>
<string name="edit_history">Lịch sử chỉnh sửa</string>
<string name="last_edit_at_x">Sửa lần cuối %s</string>
<string name="time_just_now">vừa xong</string>
@ -369,14 +369,14 @@
<string name="text_copied">Đã sao chép vào bộ nhớ tạm</string>
<string name="add_bookmark">Lưu</string>
<string name="remove_bookmark">Bỏ lưu</string>
<string name="bookmarks">Tút đã lưu</string>
<string name="your_favorites">Lượt thích</string>
<string name="bookmarks">Những tút đã lưu</string>
<string name="your_favorites">Những tút đã thích</string>
<string name="login_title">Chào mừng quay trở lại!</string>
<string name="login_subtitle">Đăng nhập với máy chủ nơi bạn tạo tài khoản.</string>
<string name="server_url">URL Máy chủ</string>
<string name="signup_random_server_explain">Chúng tôi sẽ chọn một máy chủ dựa trên ngôn ngữ của bạn nếu bạn tiếp tục mà không lựa chọn.</string>
<string name="server_filter_any_language">Mọi ngôn ngữ</string>
<string name="server_filter_instant_signup">Đăng ký nhanh</string>
<string name="server_filter_instant_signup">Duyệt tự động</string>
<string name="server_filter_manual_review">Duyệt thủ công</string>
<string name="server_filter_any_signup_speed">Mọi hình thức duyệt</string>
<string name="server_filter_region_europe">Châu Âu</string>
@ -385,7 +385,7 @@
<string name="server_filter_region_africa">Châu Phi</string>
<string name="server_filter_region_asia">Châu Á</string>
<string name="server_filter_region_oceania">Châu Đại Dương</string>
<string name="not_accepting_new_members">Tạm ngưng đăng ký mới</string>
<string name="not_accepting_new_members">Tạm dừng đăng ký mới</string>
<string name="category_special_interests">Sở thích đặc biệt</string>
<string name="signup_passwords_dont_match">Mật khẩu không khớp</string>
<string name="pick_server_for_me">Chọn giúp tôi</string>

View File

@ -381,6 +381,7 @@
<string name="server_filter_region_africa">非洲</string>
<string name="server_filter_region_asia">亚洲</string>
<string name="server_filter_region_oceania">大洋洲</string>
<string name="not_accepting_new_members">不接受新成员</string>
<string name="signup_passwords_dont_match">两次输入密码不匹配</string>
<string name="pick_server_for_me">帮我挑选</string>
<string name="profile_add_row">添加</string>
@ -396,9 +397,11 @@
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="profile_timeline">时间轴</string>
<string name="profile_endorsed_accounts">帐号</string>
<string name="verified_link">已验证链接</string>
<string name="show">显示</string>
<string name="hide">隐藏</string>
<string name="join_default_server">加入 %s</string>
<string name="pick_server">选择其他服务器</string>
<string name="signup_or_login">或者</string>
<string name="learn_more">了解更多</string>
<string name="welcome_to_mastodon">欢迎来到Mastodon</string>

View File

@ -108,4 +108,42 @@
<attr name="colorGray800" format="color" />
<attr name="colorGray800t" 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>

View File

@ -32,7 +32,6 @@
<color name="primary_800">#ae218a</color>
<color name="primary_900">#6d1556</color>
<color name="error_25">#FFFBFA</color>
<color name="error_50">#FEF3F2</color>
<color name="error_100">#FEE4E2</color>
@ -104,31 +103,69 @@
<!-- M3 dynamic colors -->
<color name="m3_navigation_bar_bg">@color/gray_50</color>
<color name="m3_gray_900">@color/gray_900</color>
<color name="m3_gray_800t">@color/gray_800t</color>
<color name="m3_gray_800">@color/gray_800</color>
<color name="m3_gray_700">@color/gray_700</color>
<color name="m3_gray_600">@color/gray_600</color>
<color name="m3_gray_500">@color/gray_500</color>
<color name="m3_gray_400">@color/gray_400</color>
<color name="m3_gray_300">@color/gray_300</color>
<color name="m3_gray_200">@color/gray_200</color>
<color name="m3_gray_100">@color/gray_100</color>
<color name="m3_gray_50t">@color/gray_50t</color>
<color name="m3_gray_50">@color/gray_50</color>
<color name="m3_gray_25">@color/gray_25</color>
<color name="m3_neutral1_900">@color/gray_900</color>
<color name="m3_neutral1_800t">@color/gray_800t</color>
<color name="m3_neutral1_800">@color/gray_800</color>
<color name="m3_neutral1_700">@color/gray_700</color>
<color name="m3_neutral1_600">@color/gray_600</color>
<color name="m3_neutral1_500">@color/gray_500</color>
<color name="m3_neutral1_400">@color/gray_400</color>
<color name="m3_neutral1_300">@color/gray_300</color>
<color name="m3_neutral1_200">@color/gray_200</color>
<color name="m3_neutral1_100">@color/gray_100</color>
<color name="m3_neutral1_50t">@color/gray_50t</color>
<color name="m3_neutral1_50">@color/gray_50</color>
<color name="m3_neutral1_25">@color/gray_25</color>
<color name="m3_primary_25">@color/purple_primary_25</color>
<color name="m3_primary_50">@color/purple_primary_50</color>
<color name="m3_primary_100">@color/purple_primary_100</color>
<color name="m3_primary_200">@color/purple_primary_200</color>
<color name="m3_primary_300">@color/purple_primary_300</color>
<color name="m3_primary_400">@color/purple_primary_400</color>
<color name="m3_primary_500">@color/purple_primary_500</color>
<color name="m3_primary_600">@color/purple_primary_600</color>
<color name="m3_primary_700">@color/purple_primary_700</color>
<color name="m3_primary_800">@color/purple_primary_800</color>
<color name="m3_primary_900">@color/purple_primary_900</color>
<color name="m3_neutral2_900">@color/gray_900</color>
<color name="m3_neutral2_800t">@color/gray_800t</color>
<color name="m3_neutral2_800">@color/gray_800</color>
<color name="m3_neutral2_700">@color/gray_700</color>
<color name="m3_neutral2_600">@color/gray_600</color>
<color name="m3_neutral2_500">@color/gray_500</color>
<color name="m3_neutral2_400">@color/gray_400</color>
<color name="m3_neutral2_300">@color/gray_300</color>
<color name="m3_neutral2_200">@color/gray_200</color>
<color name="m3_neutral2_100">@color/gray_100</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 -->
<color name="m3_sys_light_primary">#6750A4</color>

Some files were not shown because too many files have changed in this diff Show More