diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 514c47306..f3764ab86 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -81,7 +81,6 @@ android { versionNameSuffix '-play' } githubRelease { initWith release } - playRelease { initWith release } fdroidRelease { initWith release } } compileOptions { diff --git a/mastodon/src/androidTest/java/org/joinmastodon/android/ui/utils/UiUtilsTest.java b/mastodon/src/androidTest/java/org/joinmastodon/android/ui/utils/UiUtilsTest.java index 4728a4861..d9ea49e14 100644 --- a/mastodon/src/androidTest/java/org/joinmastodon/android/ui/utils/UiUtilsTest.java +++ b/mastodon/src/androidTest/java/org/joinmastodon/android/ui/utils/UiUtilsTest.java @@ -257,5 +257,9 @@ public class UiUtilsTest { assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount( makeField("pronouns", "-- * (asterisk) --") )).orElseThrow()); + + assertEquals("they/(she?)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount( + makeField("pronouns", "they/(she?)...") + )).orElseThrow()); } } \ No newline at end of file diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index cb06d87a4..0625962ae 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -39,18 +39,44 @@ import org.joinmastodon.android.utils.ProvidesAssistContent; import org.parceler.Parcels; import androidx.annotation.Nullable; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.Instant; + import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent { + private static final String TAG="MainActivity"; + @Override protected void onCreate(@Nullable Bundle savedInstanceState){ AccountSession session=getCurrentSession(); UiUtils.setUserPreferredTheme(this, session); super.onCreate(savedInstanceState); + Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler((t, e)->{ + File file=new File(MastodonApp.context.getFilesDir(), "crash.log"); + try(FileOutputStream out=new FileOutputStream(file)){ + PrintWriter writer=new PrintWriter(out); + writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")"); + writer.println(Instant.now().toString()); + writer.println(); + e.printStackTrace(writer); + writer.flush(); + }catch(IOException x){ + Log.e(TAG, "Error writing crash.log", x); + }finally{ + defaultHandler.uncaughtException(t, e); + } + }); + if(savedInstanceState==null){ restartHomeFragment(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java index f62883b35..522f9a62c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java @@ -53,7 +53,9 @@ public class MastodonAPIController{ .registerTypeAdapter(Status.class, new Status.StatusDeserializer()) .create(); private static WorkerThread thread=new WorkerThread("MastodonAPIController"); - private static OkHttpClient httpClient=new OkHttpClient.Builder().build(); + private static OkHttpClient httpClient=new OkHttpClient.Builder() + .readTimeout(30, TimeUnit.SECONDS) + .build(); private AccountSession session; private static List badDomains = new ArrayList<>(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/AkkomaTranslateStatus.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/AkkomaTranslateStatus.java new file mode 100644 index 000000000..f47f7c2c1 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/AkkomaTranslateStatus.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.AkkomaTranslation; + +public class AkkomaTranslateStatus extends MastodonAPIRequest{ + public AkkomaTranslateStatus(String id, String lang){ + super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toUpperCase(), AkkomaTranslation.class); + } +} + diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java index 52b93809b..6a241aa45 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java @@ -48,6 +48,8 @@ public class CreateStatus extends MastodonAPIRequest{ public String quoteId; public ContentType contentType; + public boolean preview; + public static class Poll{ public ArrayList options=new ArrayList<>(); public int expiresIn; diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 3f9f1fe26..5d70e0b23 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -260,11 +260,13 @@ public class AccountSession{ } private boolean isFilteredType(Status s){ + AccountLocalPreferences localPreferences = getLocalPreferences(); return (!localPreferences.showReplies && s.inReplyToId != null) || (!localPreferences.showBoosts && s.reblog != null); } public void filterStatusContainingObjects(List objects, Function extractor, FilterContext context, Account profile){ + AccountLocalPreferences localPreferences = getLocalPreferences(); if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){ Status s=extractor.apply(obj); if(s!=null && s.filtered!=null){ @@ -307,7 +309,7 @@ public class AccountSession{ if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC)) return true; // Even with server-side filters, clients are expected to remove statuses that match a filter that hides them - if(localPreferences.serverSideFiltersSupported){ + if(getLocalPreferences().serverSideFiltersSupported){ for(FilterResult filter : s.filtered){ if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE) return true; diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index ba90f5d80..33bd83ff3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -1,5 +1,7 @@ package org.joinmastodon.android.api.session; +import static org.unifiedpush.android.connector.UnifiedPush.getDistributor; + import android.app.Activity; import android.app.NotificationManager; import android.content.ComponentName; @@ -34,6 +36,7 @@ import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Token; +import org.unifiedpush.android.connector.UnifiedPush; import java.io.File; import java.io.FileInputStream; @@ -112,6 +115,7 @@ public class AccountSessionManager{ } public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){ + Context context = MastodonApp.context; instances.put(instance.uri, instance); AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo); sessions.put(session.getID(), session); @@ -124,7 +128,14 @@ public class AccountSessionManager{ MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri)); updateMoreInstanceInfo(instance, instance.uri); - if(PushSubscriptionManager.arePushNotificationsAvailable()){ + if (!UnifiedPush.getDistributor(context).isEmpty()) { + UnifiedPush.registerApp( + context, + session.getID(), + new ArrayList<>(), + context.getPackageName() + ); + } else if(PushSubscriptionManager.arePushNotificationsAvailable()){ session.getPushSubscriptionManager().registerAccountForPush(null); } maybeUpdateShortcuts(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 26a8e6549..126054077 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -23,13 +23,16 @@ import androidx.recyclerview.widget.RecyclerView; import org.joinmastodon.android.E; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.polls.SubmitPollVote; +import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus; import org.joinmastodon.android.api.requests.statuses.TranslateStatus; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.AkkomaTranslation; import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Relationship; @@ -53,6 +56,7 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem; import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; +import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.PreviewlessMediaAttachmentViewController; import org.joinmastodon.android.ui.utils.UiUtils; @@ -66,6 +70,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import androidx.annotation.NonNull; @@ -441,12 +446,14 @@ public abstract class BaseStatusListFragment exten } }); list.addItemDecoration(new StatusListItemDecoration()); + list.addItemDecoration(new InsetStatusItemDecoration(this)); ((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){ private Rect tmpRect=new Rect(); @Override public void getSelectorBounds(View view, Rect outRect){ - boolean hasDescendant = false, hasAncestor = false, isWarning = false; - int lastIndex = -1, firstIndex = -1; + if(list!=view.getParent()) return; + boolean hasDescendant=false, hasAncestor=false, isWarning=false; + int lastIndex=-1, firstIndex=-1; if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){ list.getDecoratedBoundsWithMargins(view, outRect); }else{ @@ -576,7 +583,7 @@ public abstract class BaseStatusListFragment exten spoilerFooterIndex=spoilerItem.contentItems.indexOf(pollItems.get(pollItems.size()-1)); } pollItems.clear(); - StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems, status); + StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems); if(spoilerItem!=null){ spoilerItem.contentItems.subList(spoilerFirstOptionIndex, spoilerFooterIndex+1).clear(); spoilerItem.contentItems.addAll(spoilerFirstOptionIndex, pollItems); @@ -647,7 +654,8 @@ public abstract class BaseStatusListFragment exten public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){ Status status=holder.getItem().status; - toggleSpoiler(status, holder.getItemID()); + boolean isForQuote=holder.getItem().isForQuote; + toggleSpoiler(status, isForQuote, holder.getItemID()); } public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) { @@ -669,15 +677,16 @@ public abstract class BaseStatusListFragment exten else notifyItemChangedBefore(holder.getItem(), HeaderStatusDisplayItem.class); } - protected void toggleSpoiler(Status status, String itemID){ + protected void toggleSpoiler(Status status, boolean isForQuote, String itemID){ status.spoilerRevealed=!status.spoilerRevealed; if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) status.sensitiveRevealed = false; - SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class); + List spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class); + SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0); if(spoiler!=null) spoiler.rebind(); else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class); - SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class)); + SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem()); int index=displayItems.indexOf(spoilerItem); if(status.spoilerRevealed){ @@ -941,37 +950,52 @@ public abstract class BaseStatusListFragment exten status.translationState=Status.TranslationState.SHOWN; }else{ status.translationState=Status.TranslationState.LOADING; - new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()) - .setCallback(new Callback<>(){ + Consumer successCallback=(result)->{ + status.translation=result; + status.translationState=Status.TranslationState.SHOWN; + updateTranslation(itemID); + }; + MastodonAPIRequest req=isInstanceAkkoma() + ? new AkkomaTranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){ + @Override + public void onSuccess(AkkomaTranslation result){ + if(getActivity()!=null) successCallback.accept(result.toTranslation()); + } + @Override + public void onError(ErrorResponse error){ + if(getActivity()!=null) translationCallbackError(status, itemID); + } + }) + : new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){ @Override public void onSuccess(Translation result){ - if(getActivity()==null) - return; - status.translation=result; - status.translationState=Status.TranslationState.SHOWN; - updateTranslation(itemID); + if(getActivity()!=null) successCallback.accept(result); } @Override public void onError(ErrorResponse error){ - if(getActivity()==null) - return; - status.translationState=Status.TranslationState.HIDDEN; - updateTranslation(itemID); - new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.error) - .setMessage(R.string.translation_failed) - .setPositiveButton(R.string.ok, null) - .show(); + if(getActivity()!=null) translationCallbackError(status, itemID); } - }) - .exec(accountID); + }); + + // 1 minute + req.setTimeout(60000).exec(accountID); } } } updateTranslation(itemID); } + private void translationCallbackError(Status status, String itemID) { + status.translationState=Status.TranslationState.HIDDEN; + updateTranslation(itemID); + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.error) + .setMessage(R.string.translation_failed) + .setPositiveButton(R.string.ok, null) + .show(); + } + private void updateTranslation(String itemID) { TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); if(text!=null){ @@ -981,6 +1005,9 @@ public abstract class BaseStatusListFragment exten notifyItemChanged(itemID, TextStatusDisplayItem.class); } + if(isInstanceAkkoma()) + return; + SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class); if(spoiler!=null){ spoiler.rebind(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 455887473..2f32b63df 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -801,7 +801,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS || (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id))) && !status.spoilerText.startsWith("re: ") ? "re: " : ""; - spoilerEdit.setText(prefix + replyTo.spoilerText); + spoilerEdit.setText(prefix + status.spoilerText); spoilerBtn.setSelected(true); } if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language); @@ -890,19 +890,22 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr charCounter.setText(String.valueOf(charLimit)); } -// draftsBtn = wrap.findViewById(R.id.drafts_btn); - draftOptionsPopup = new PopupMenu(getContext(), draftsBtn); +// draftsBtn=wrap.findViewById(R.id.drafts_btn); + draftOptionsPopup=new PopupMenu(getContext(), draftsBtn); draftOptionsPopup.inflate(R.menu.compose_more); - draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft); - undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft); - scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule); - unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule); + Menu draftOptionsMenu=draftOptionsPopup.getMenu(); + draftMenuItem=draftOptionsMenu.findItem(R.id.draft); + undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft); + scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule); + unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule); + draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma()); draftOptionsPopup.setOnMenuItemClickListener(i->{ - int id = i.getItemId(); - if (id == R.id.draft) updateScheduledAt(getDraftInstant()); - else if (id == R.id.schedule) pickScheduledDateTime(); - else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null); - else navigateToUnsentPosts(); + int id=i.getItemId(); + if(id==R.id.draft) updateScheduledAt(getDraftInstant()); + else if(id==R.id.schedule) pickScheduledDateTime(); + else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null); + else if(id==R.id.drafts) navigateToUnsentPosts(); + else if(id==R.id.preview) publish(true); return true; }); UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup); @@ -917,6 +920,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } return false; }); + if (!GlobalUserPreferences.relocatePublishButton): + publishButton.post(()->publishButton.setMinimumWidth(publishButton.getWidth())); (GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setOnClickListener(v->{ Consumer draftCheckComplete=(isDraft)->{ @@ -1140,6 +1145,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } private void publish(){ + publish(false); + } + + private void publish(boolean preview){ sendingOverlay=new View(getActivity()); WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams(); overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; @@ -1153,10 +1162,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr (GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false); V.setVisibilityAnimated(sendProgress, View.VISIBLE); - mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError); + mediaViewController.saveAltTextsBeforePublishing( + ()->actuallyPublish(preview), + this::handlePublishError); } - private void actuallyPublish(){ + private void actuallyPublish(boolean preview){ String text=mainEditText.getText().toString(); CreateStatus.Request req=new CreateStatus.Request(); if("bottom".equals(postLang.encoding)){ @@ -1174,6 +1185,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr req.sensitive=sensitive; req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType; req.scheduledAt=scheduledAt; + req.preview=preview; if(!mediaViewController.isEmpty()){ req.mediaIds=mediaViewController.getAttachmentIDs(); if(editingStatus != null){ @@ -1201,7 +1213,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr Callback resCallback=new Callback<>(){ @Override public void onSuccess(Status result){ - maybeDeleteScheduledPost(() -> { + if(preview){ + openPreview(result); + return; + } + + maybeDeleteScheduledPost(()->{ wm.removeView(sendingOverlay); sendingOverlay=null; if(editingStatus==null || redraftStatus){ @@ -1223,10 +1240,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } E.post(new StatusUpdatedEvent(editedStatus)); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()){ Nav.finish(ComposeFragment.this); } - if (getArguments().getBoolean("navigateToStatus", false)) { + if(getArguments().getBoolean("navigateToStatus", false)){ Bundle args=new Bundle(); args.putString("account", accountID); args.putParcelable("status", Parcels.wrap(result)); @@ -1242,11 +1259,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } }; - if(editingStatus!=null && !redraftStatus){ + if(editingStatus!=null && !redraftStatus && !preview){ new EditStatus(req, editingStatus.id) .setCallback(resCallback) .exec(accountID); - }else if(req.scheduledAt == null){ + }else if(req.scheduledAt == null || preview){ new CreateStatus(req, uuid) .setCallback(resCallback) .exec(accountID); @@ -1299,6 +1316,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } } + private void openPreview(Status result){ + result.preview=true; + wm.removeView(sendingOverlay); + sendingOverlay=null; + publishButton.setEnabled(true); + V.setVisibilityAnimated(sendProgress, View.GONE); + InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class); + imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0); + + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("status", Parcels.wrap(result)); + if(replyTo!=null){ + args.putParcelable("inReplyTo", Parcels.wrap(replyTo)); + args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo.account)); + } + Nav.go(getActivity(), ThreadFragment.class, args); + } + private void updateRecentLanguages() { if (postLang == null || postLang.language == null) return; String language = postLang.language.getLanguage(); @@ -1665,6 +1701,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } contentTypePopup.setOnMenuItemClickListener(i->{ + uuid=null; int index=i.getItemId(); contentType=ContentType.values()[index]; btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal()); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java index 3243244df..31c100e5c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java @@ -67,86 +67,86 @@ import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class EditTimelinesFragment extends MastodonRecyclerFragment implements ScrollableToTop { - private String accountID; - private TimelinesAdapter adapter; - private final ItemTouchHelper itemTouchHelper; - private Menu optionsMenu; - private boolean updated; - private final Map timelineByMenuItem = new HashMap<>(); - private final List listTimelines = new ArrayList<>(); - private final List hashtags = new ArrayList<>(); - private MenuItem addHashtagItem; +public class EditTimelinesFragment extends MastodonRecyclerFragment implements ScrollableToTop{ + private String accountID; + private TimelinesAdapter adapter; + private final ItemTouchHelper itemTouchHelper; + private Menu optionsMenu; + private boolean updated; + private final Map timelineByMenuItem=new HashMap<>(); + private final List listTimelines=new ArrayList<>(); + private final List hashtags=new ArrayList<>(); + private MenuItem addHashtagItem; private final List localTimelines = new ArrayList<>(); - public EditTimelinesFragment() { - super(10); - ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ; - itemTouchHelper = new ItemTouchHelper(itemTouchCallback); - } + public EditTimelinesFragment(){ + super(10); + ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback(); + itemTouchHelper=new ItemTouchHelper(itemTouchCallback); + } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - setTitle(R.string.sk_timelines); - accountID = getArguments().getString("account"); + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + setTitle(R.string.sk_timelines); + accountID=getArguments().getString("account"); - new GetLists().setCallback(new Callback<>() { - @Override - public void onSuccess(List result) { - listTimelines.addAll(result); - updateOptionsMenu(); - } + new GetLists().setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + listTimelines.addAll(result); + updateOptionsMenu(); + } - @Override - public void onError(ErrorResponse error) { - error.showToast(getContext()); - } - }).exec(accountID); + @Override + public void onError(ErrorResponse error){ + error.showToast(getContext()); + } + }).exec(accountID); - new GetFollowedHashtags().setCallback(new Callback<>() { - @Override - public void onSuccess(HeaderPaginationList result) { - hashtags.addAll(result); - updateOptionsMenu(); - } + new GetFollowedHashtags().setCallback(new Callback<>(){ + @Override + public void onSuccess(HeaderPaginationList result){ + hashtags.addAll(result); + updateOptionsMenu(); + } - @Override - public void onError(ErrorResponse error) { - error.showToast(getContext()); - } - }).exec(accountID); - } + @Override + public void onError(ErrorResponse error){ + error.showToast(getContext()); + } + }).exec(accountID); + } - @Override - protected void onShown(){ - super.onShown(); - if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData(); - } + @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); - itemTouchHelper.attachToRecyclerView(list); - refreshLayout.setEnabled(false); - list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16)); - } + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + itemTouchHelper.attachToRecyclerView(list); + refreshLayout.setEnabled(false); + list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16)); + } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - this.optionsMenu = menu; - updateOptionsMenu(); - } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + this.optionsMenu=menu; + updateOptionsMenu(); + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.menu_back) { - updateOptionsMenu(); - optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0); - return true; - } - if (item.getItemId() == R.id.menu_add_local_timelines) { + @Override + public boolean onOptionsItemSelected(MenuItem item){ + if(item.getItemId()==R.id.menu_back){ + updateOptionsMenu(); + optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0); + return true; + } + if (item.getItemId() == R.id.menu_add_local_timelines) { addNewLocalTimeline(); return true; } @@ -161,14 +161,14 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment addTimelineToOptions(tl, timelinesMenu)); - listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu)); - addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular); - hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu)); + TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu)); + listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu)); + addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular); + hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu)); - timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0); - listsMenu.getItem().setVisible(listsMenu.size() > 0); - hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0); + timelinesMenu.getItem().setVisible(timelinesMenu.size()>0); + listsMenu.getItem().setVisible(listsMenu.size()>0); + hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0); - UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline); - } + UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline); + } - private void saveTimelines() { - updated=true; + private void saveTimelines(){ + updated=true; AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences(); if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE); prefs.timelines=data; prefs.save(); } - private void removeTimeline(int position) { - data.remove(position); - adapter.notifyItemRemoved(position); - saveTimelines(); - updateOptionsMenu(); - } + private void removeTimeline(int position){ + data.remove(position); + adapter.notifyItemRemoved(position); + saveTimelines(); + updateOptionsMenu(); + } - @Override - protected void doLoadData(int offset, int count){ - onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines); - updateOptionsMenu(); - } + @Override + protected void doLoadData(int offset, int count){ + onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines); + updateOptionsMenu(); + } - @Override - protected RecyclerView.Adapter getAdapter() { - return adapter = new TimelinesAdapter(); - } + @Override + protected RecyclerView.Adapter getAdapter(){ + return adapter=new TimelinesAdapter(); + } - @Override - public void scrollToTop() { - smoothScrollRecyclerViewToTop(list); - } + @Override + public void scrollToTop(){ + smoothScrollRecyclerViewToTop(list); + } - @Override - public void onDestroy() { - super.onDestroy(); - if (updated) UiUtils.restartApp(); - } + @Override + public void onDestroy(){ + super.onDestroy(); + if(updated) UiUtils.restartApp(); + } - private boolean setTagListContent(NachoTextView editText, @Nullable List tags) { - if (tags == null || tags.isEmpty()) return false; + private boolean setTagListContent(NachoTextView editText, @Nullable List tags){ + if(tags==null || tags.isEmpty()) return false; editText.setText(tags); editText.chipifyAllUnterminatedTokens(); - return true; - } + return true; + } - private NachoTextView prepareChipTextView(NachoTextView nacho) { + private NachoTextView prepareChipTextView(NachoTextView nacho){ //I’ll Be Back nacho.setChipTerminators( Map.of( @@ -289,223 +289,228 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment nacho.chipifyAllUnterminatedTokens()); - return nacho; - } + nacho.enableEditChipOnTouch(true, true); + nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens()); + return nacho; + } - @SuppressLint("ClickableViewAccessibility") - protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer onSave, Runnable onRemove) { - Context ctx = getContext(); - View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false); + @SuppressLint("ClickableViewAccessibility") + protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer onSave, Runnable onRemove){ + Context ctx=getContext(); + View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false); - View divider = view.findViewById(R.id.divider); - Button advancedBtn = view.findViewById(R.id.advanced); - EditText editText = view.findViewById(R.id.input); - if (item != null) editText.setText(item.getCustomTitle()); - editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag)); + View divider=view.findViewById(R.id.divider); + Button advancedBtn=view.findViewById(R.id.advanced); + EditText editText=view.findViewById(R.id.input); + if(item!=null) editText.setText(item.getCustomTitle()); + editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag)); - LinearLayout tagWrap = view.findViewById(R.id.tag_wrap); - boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG; - advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE); - advancedBtn.setOnClickListener(l -> { - advancedBtn.setSelected(!advancedBtn.isSelected()); + LinearLayout tagWrap=view.findViewById(R.id.tag_wrap); + boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG; + advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE); + advancedBtn.setOnClickListener(l->{ + advancedBtn.setSelected(!advancedBtn.isSelected()); advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show); divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE); - tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE); + tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE); UiUtils.beginLayoutTransition((ViewGroup) view); - }); + }); - Switch localOnlySwitch = view.findViewById(R.id.local_only_switch); - view.findViewById(R.id.local_only) - .setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked())); + Switch localOnlySwitch=view.findViewById(R.id.local_only_switch); + view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked())); - EditText tagMain = view.findViewById(R.id.tag_main); - NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any)); - NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all)); - NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none)); - if (item != null) { - tagMain.setText(item.getHashtagName()); - boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle()); - hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced; - hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced; - hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced; - if (item.isHashtagLocalOnly()) { - localOnlySwitch.setChecked(true); - hasAdvanced = true; - } - if (hasAdvanced) { - advancedBtn.setSelected(true); - advancedBtn.setText(R.string.sk_advanced_options_hide); + EditText tagMain=view.findViewById(R.id.tag_main); + NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any)); + NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all)); + NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none)); + + if(item!=null && hashtagOptionsAvailable){ + tagMain.setText(item.getHashtagName()); + boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle()); + hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced; + hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced; + hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced; + if(item.isHashtagLocalOnly()){ + localOnlySwitch.setChecked(true); + hasAdvanced=true; + } + if(hasAdvanced){ + advancedBtn.setSelected(true); + advancedBtn.setText(R.string.sk_advanced_options_hide); tagWrap.setVisibility(View.VISIBLE); divider.setVisibility(View.VISIBLE); - } - } + } + } - ImageButton btn = view.findViewById(R.id.button); - PopupMenu popup = new PopupMenu(ctx, btn); - TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG; - btn.setImageResource(currentIcon.iconRes); - btn.setTag(currentIcon.ordinal()); - btn.setContentDescription(ctx.getString(currentIcon.nameRes)); - btn.setOnTouchListener(popup.getDragToOpenListener()); - btn.setOnClickListener(l -> popup.show()); + ImageButton btn=view.findViewById(R.id.button); + PopupMenu popup=new PopupMenu(ctx, btn); + TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG; + btn.setImageResource(currentIcon.iconRes); + btn.setTag(currentIcon.ordinal()); + btn.setContentDescription(ctx.getString(currentIcon.nameRes)); + btn.setOnTouchListener(popup.getDragToOpenListener()); + btn.setOnClickListener(l->popup.show()); - Menu menu = popup.getMenu(); - TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG; - menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes); - if (!currentIcon.equals(defaultIcon)) { - menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes); - } - for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) { - if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue; - menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes); - } - UiUtils.enablePopupMenuIcons(ctx, popup); + Menu menu=popup.getMenu(); + TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG; + menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes); + if(!currentIcon.equals(defaultIcon)){ + menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes); + } + for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){ + if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue; + menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes); + } + UiUtils.enablePopupMenuIcons(ctx, popup); - popup.setOnMenuItemClickListener(menuItem -> { - TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()]; - btn.setImageResource(icon.iconRes); - btn.setTag(menuItem.getItemId()); - btn.setContentDescription(ctx.getString(icon.nameRes)); - return true; - }); + popup.setOnMenuItemClickListener(menuItem->{ + TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()]; + btn.setImageResource(icon.iconRes); + btn.setTag(menuItem.getItemId()); + btn.setContentDescription(ctx.getString(icon.nameRes)); + return true; + }); - AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx) - .setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline) - .setView(view) - .setPositiveButton(R.string.save, (d, which) -> { - tagsAny.chipifyAllUnterminatedTokens(); - tagsAll.chipifyAllUnterminatedTokens(); - tagsNone.chipifyAllUnterminatedTokens(); - String name = editText.getText().toString().trim(); - String mainHashtag = tagMain.getText().toString().trim(); - if (TextUtils.isEmpty(mainHashtag)) { - mainHashtag = name; - name = null; - } - if (TextUtils.isEmpty(mainHashtag) && (item != null && item.getType() == TimelineDefinition.TimelineType.HASHTAG)) { - Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show(); - onSave.accept(null); - return; - } + AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx) + .setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline) + .setView(view) + .setPositiveButton(R.string.save, (d, which)->{ + String name=editText.getText().toString().trim(); - TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name); - TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()]; - tl.setIcon(icon); - tl.setTitle(name); - tl.setTagOptions( - mainHashtag, - tagsAny.getChipValues(), - tagsAll.getChipValues(), - tagsNone.getChipValues(), - localOnlySwitch.isChecked() - ); - onSave.accept(tl); - }) - .setNegativeButton(R.string.cancel, (d, which) -> {}); + String mainHashtag=tagMain.getText().toString().trim(); + if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){ + tagsAny.chipifyAllUnterminatedTokens(); + tagsAll.chipifyAllUnterminatedTokens(); + tagsNone.chipifyAllUnterminatedTokens(); + if(TextUtils.isEmpty(mainHashtag)){ + mainHashtag=name; + name=null; + } + if(TextUtils.isEmpty(mainHashtag) && (item!=null && item.getType()==TimelineDefinition.TimelineType.HASHTAG)){ + Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show(); + onSave.accept(null); + return; + } + } - if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run()); + TimelineDefinition tl=item!=null ? item : TimelineDefinition.ofHashtag(name); + TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[(int) btn.getTag()]; + tl.setIcon(icon); + tl.setTitle(name); + if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){ + tl.setTagOptions( + mainHashtag, + tagsAny.getChipValues(), + tagsAll.getChipValues(), + tagsNone.getChipValues(), + localOnlySwitch.isChecked() + ); + } + onSave.accept(tl); + }) + .setNegativeButton(R.string.cancel, (d, which)->{}); - builder.show(); - btn.requestFocus(); - } + if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run()); - private class TimelinesAdapter extends RecyclerView.Adapter{ - @NonNull - @Override - public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new TimelineViewHolder(); - } + builder.show(); + btn.requestFocus(); + } - @Override - public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) { - holder.bind(data.get(position)); - } + private class TimelinesAdapter extends RecyclerView.Adapter{ + @NonNull + @Override + public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new TimelineViewHolder(); + } - @Override - public int getItemCount() { - return data.size(); - } - } + @Override + public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){ + holder.bind(data.get(position)); + } - private class TimelineViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ - private final TextView title; - private final ImageView dragger; + @Override + public int getItemCount(){ + return data.size(); + } + } - public TimelineViewHolder(){ - super(getActivity(), R.layout.item_text, list); - title=findViewById(R.id.title); - dragger=findViewById(R.id.dragger_thingy); - } + private class TimelineViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ + private final TextView title; + private final ImageView dragger; - @SuppressLint("ClickableViewAccessibility") - @Override - public void onBind(TimelineDefinition item) { - title.setText(item.getTitle(getContext())); - title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null); - dragger.setVisibility(View.VISIBLE); - dragger.setOnTouchListener((View v, MotionEvent event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - itemTouchHelper.startDrag(this); - return true; - } - return false; - }); - } + public TimelineViewHolder(){ + super(getActivity(), R.layout.item_text, list); + title=findViewById(R.id.title); + dragger=findViewById(R.id.dragger_thingy); + } - private void onSave(TimelineDefinition tl) { - saveTimelines(); - rebind(); - } + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBind(TimelineDefinition item){ + title.setText(item.getTitle(getContext())); + title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null); + dragger.setVisibility(View.VISIBLE); + dragger.setOnTouchListener((View v, MotionEvent event)->{ + if(event.getAction()==MotionEvent.ACTION_DOWN){ + itemTouchHelper.startDrag(this); + return true; + } + return false; + }); + } - private void onRemove() { - removeTimeline(getAbsoluteAdapterPosition()); - } + private void onSave(TimelineDefinition tl){ + saveTimelines(); + rebind(); + } - @SuppressLint("ClickableViewAccessibility") - @Override - public void onClick() { - makeTimelineEditor(item, this::onSave, this::onRemove); - } - } + private void onRemove(){ + removeTimeline(getAbsoluteAdapterPosition()); + } - private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback { - public ItemTouchHelperCallback() { - super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); - } + @SuppressLint("ClickableViewAccessibility") + @Override + public void onClick(){ + makeTimelineEditor(item, this::onSave, this::onRemove); + } + } - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - int fromPosition = viewHolder.getAbsoluteAdapterPosition(); - int toPosition = target.getAbsoluteAdapterPosition(); - if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) { - return false; - } else { - Collections.swap(data, fromPosition, toPosition); - adapter.notifyItemMoved(fromPosition, toPosition); - saveTimelines(); - return true; - } - } + private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{ + public ItemTouchHelperCallback(){ + super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT); + } - @Override - public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { - if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) { - viewHolder.itemView.animate().alpha(0.65f); - } - } + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){ + int fromPosition=viewHolder.getAbsoluteAdapterPosition(); + int toPosition=target.getAbsoluteAdapterPosition(); + if(Math.max(fromPosition, toPosition)>=data.size() || Math.min(fromPosition, toPosition)<0){ + return false; + }else{ + Collections.swap(data, fromPosition, toPosition); + adapter.notifyItemMoved(fromPosition, toPosition); + saveTimelines(); + return true; + } + } - @Override - public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - viewHolder.itemView.animate().alpha(1f); - } + @Override + public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){ + if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){ + viewHolder.itemView.animate().alpha(0.65f); + } + } - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - int position = viewHolder.getAbsoluteAdapterPosition(); - removeTimeline(position); - } - } + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){ + super.clearView(recyclerView, viewHolder); + viewHolder.itemView.animate().alpha(1f); + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){ + int position=viewHolder.getAbsoluteAdapterPosition(); + removeTimeline(position); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index 3bd858938..7ad134b7e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -229,7 +229,6 @@ public class NotificationsListFragment extends BaseStatusListFragment { - if (!noteEdit.getText().toString().trim().equals(note)) { - savePrivateNote(); - } - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); + noteSaveBtn.setOnClickListener((v->{ + savePrivateNote(noteEdit.getText().toString()); + InputMethodManager imm=(InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0); noteEdit.clearFocus(); + noteSaveBtn.clearFocus(); })); - noteEdit.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - fab.setVisibility(View.INVISIBLE); - TranslateAnimation animate = new TranslateAnimation( - 0, - 0, - 0, - fab.getHeight() * 2); - animate.setDuration(300); - fab.startAnimation(animate); - - noteEditConfirm.setVisibility(View.VISIBLE); - noteEditConfirm.animate() - .alpha(1.0f) - .setDuration(700); - } else { - fab.setVisibility(View.VISIBLE); - TranslateAnimation animate = new TranslateAnimation( - 0, - 0, - fab.getHeight() * 2, - 0); - animate.setDuration(300); - fab.startAnimation(animate); - - noteEditConfirm.animate() - .alpha(0.0f) - .setDuration(700); - noteEditConfirm.setVisibility(View.INVISIBLE); + noteEdit.setOnFocusChangeListener((v, hasFocus)->{ + if(hasFocus){ + hideFab(); + V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE); + noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + }else if(!noteSaveBtn.hasFocus()){ + showFab(); + hideNoteSaveBtnIfNotDirty(); } }); - noteEditConfirm.setOnClickListener((v -> { - if (!noteEdit.getText().toString().trim().equals(note)) { - savePrivateNote(); + noteEdit.addTextChangedListener(new TextWatcher(){ + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + if(relationship!=null && noteSaveBtn.getVisibility()!=View.VISIBLE && !s.toString().equals(relationship.note)) + V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE); } - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0); - noteEdit.clearFocus(); - })); + + @Override + public void afterTextChanged(Editable s){} + }); + + noteSaveBtn.setOnFocusChangeListener((v, hasFocus)->{ + if(!hasFocus && !noteEdit.hasFocus()){ + showFab(); + hideNoteSaveBtnIfNotDirty(); + } + }); FrameLayout sizeWrapper=new FrameLayout(getActivity()){ @Override @@ -498,6 +493,46 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList return sizeWrapper; } + private void hideNoteSaveBtnIfNotDirty(){ + if(noteEdit.getText().toString().equals(relationship.note)){ + V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE); + } + } + + private void showPrivateNote(){ + noteWrap.setVisibility(View.VISIBLE); + noteEdit.setText(relationship.note); + } + + private void hidePrivateNote(){ + noteWrap.setVisibility(View.GONE); + noteEdit.setText(null); + } + + private void savePrivateNote(String note){ + if(note!=null && note.equals(relationship.note)){ + updateRelationship(); + invalidateOptionsMenu(); + return; + } + V.setVisibilityAnimated(noteSaveProgress, View.VISIBLE); + V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE); + new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() { + @Override + public void onSuccess(Relationship result) { + updateRelationship(result); + invalidateOptionsMenu(); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(getContext()); + V.setVisibilityAnimated(noteSaveProgress, View.GONE); + V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE); + } + }).exec(accountID); + } + private void onAccountLoaded(Account result) { account=result; isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); @@ -524,25 +559,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList V.setVisibilityAnimated(fab, View.VISIBLE); } - public void setNote(String note){ - this.note=note; - noteWrap.setVisibility(View.VISIBLE); - noteEdit.setVisibility(View.VISIBLE); - noteEdit.setText(note); - } - - private void savePrivateNote(){ - new SetPrivateNote(profileAccountID, noteEdit.getText().toString()).setCallback(new Callback<>() { - @Override - public void onSuccess(Relationship result) {} - - @Override - public void onError(ErrorResponse error) { - error.showToast(getActivity()); - } - }).exec(accountID); - } - @Override protected void doLoadData(){ if (remoteAccount != null) { @@ -884,6 +900,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList }else{ blockDomain.setVisible(false); } + menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty()) + ? R.string.sk_add_note : R.string.sk_delete_note); } @Override @@ -948,12 +966,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList final Bundle args=new Bundle(); args.putString("account", accountID); args.putParcelable("targetAccount", Parcels.wrap(account)); - Nav.go(getActivity(), MutesListFragment.class, args); + Nav.go(getActivity(), MutedAccountsListFragment.class, args); }else if(id==R.id.blocked_accounts){ final Bundle args=new Bundle(); args.putString("account", accountID); args.putParcelable("targetAccount", Parcels.wrap(account)); - Nav.go(getActivity(), BlocksListFragment.class, args); + Nav.go(getActivity(), BlockedAccountsListFragment.class, args); }else if(id==R.id.followed_hashtags){ Bundle args=new Bundle(); args.putString("account", accountID); @@ -965,6 +983,26 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList }else if(id==R.id.save){ if(isInEditMode) saveAndExitEditMode(); + }else if(id==R.id.edit_note){ + if(noteWrap.getVisibility()==View.GONE){ + showPrivateNote(); + UiUtils.beginLayoutTransition(scrollableContent); + noteEdit.requestFocus(); + noteEdit.postDelayed(()->{ + InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class); + imm.showSoftInput(noteEdit, 0); + }, 100); + }else if(relationship.note.isEmpty()){ + hidePrivateNote(); + UiUtils.beginLayoutTransition(scrollableContent); + }else{ + new M3AlertDialogBuilder(getActivity()) + .setMessage(getContext().getString(R.string.sk_private_note_confirm_delete, account.getDisplayUsername())) + .setPositiveButton(R.string.delete, (dlg, btn)->savePrivateNote(null)) + .setNegativeButton(R.string.cancel, null) + .show(); + } + invalidateOptionsMenu(); } return true; } @@ -990,20 +1028,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private void updateRelationship(){ if(getActivity()==null) return; + if(relationship.note!=null && !relationship.note.isEmpty()) showPrivateNote(); + else hidePrivateNote(); invalidateOptionsMenu(); actionButton.setVisibility(View.VISIBLE); notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE); UiUtils.setRelationshipToActionButtonM3(relationship, actionButton); actionProgress.setIndeterminateTintList(actionButton.getTextColors()); notifyProgress.setIndeterminateTintList(notifyButton.getTextColors()); + noteSaveProgress.setIndeterminateTintList(noteEdit.getTextColors()); followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE); notifyButton.setSelected(relationship.notifying); notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username)); - - if (!isOwnProfile) { - setNote(relationship.note); -// aboutFragment.setNote(relationship.note, accountID, profileAccountID); - } + noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + V.setVisibilityAnimated(noteSaveProgress, View.GONE); + V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE); + UiUtils.beginLayoutTransition(scrollableContent); } public ImageButton getFab() { @@ -1315,7 +1355,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList @Override public boolean onBackPressed(){ if(noteEdit.hasFocus()) { - savePrivateNote(); + savePrivateNote(noteEdit.getText().toString()); } if(isInEditMode){ if(savingEdits) @@ -1576,7 +1616,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList title.setText(item.parsedName); value.setText(item.parsedValue); if(item.verifiedAt!=null){ - int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63; + int textColor=UiUtils.getThemeColor(getContext(), R.attr.colorM3Success); value.setTextColor(textColor); value.setLinkTextColor(textColor); Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java index 1f5fdff9f..819f58a77 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java @@ -12,6 +12,7 @@ import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; +import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.joinmastodon.android.ui.utils.UiUtils; @@ -86,7 +87,8 @@ public class StatusEditHistoryFragment extends StatusListFragment{ EnumSet changes=EnumSet.noneOf(StatusEditChangeType.class); Status prev=data.get(idx+1); - if(!Objects.equals(s.content, prev.content)){ + // if only formatting was changed, don't even try to create a diff text + if(!Objects.equals(HtmlParser.text(s.content), HtmlParser.text(prev.content))){ changes.add(StatusEditChangeType.TEXT_CHANGED); //update status content to display a diffs s.content=createDiffText(prev.content, s.content); @@ -156,12 +158,6 @@ public class StatusEditHistoryFragment extends StatusListFragment{ return items; } - @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - list.addItemDecoration(new InsetStatusItemDecoration(this)); - } - @Override public boolean isItemEnabled(String id){ return false; @@ -178,24 +174,24 @@ public class StatusEditHistoryFragment extends StatusListFragment{ } private String createDiffText(String original, String modified) { - diff_match_patch dmp = new diff_match_patch(); - LinkedList diffs = dmp.diff_main(original, modified); + diff_match_patch dmp=new diff_match_patch(); + LinkedList diffs=dmp.diff_main(original, modified); dmp.diff_cleanupSemantic(diffs); - StringBuilder stringBuilder = new StringBuilder(); - for(diff_match_patch.Diff diff: diffs){ + StringBuilder stringBuilder=new StringBuilder(); + for(diff_match_patch.Diff diff : diffs){ switch(diff.operation){ - case DELETE -> { - stringBuilder.append(""); + case DELETE->{ + stringBuilder.append(""); stringBuilder.append(diff.text); - stringBuilder.append(""); + stringBuilder.append(""); } - case INSERT -> { - stringBuilder.append(""); + case INSERT->{ + stringBuilder.append(""); stringBuilder.append(diff.text); - stringBuilder.append(""); + stringBuilder.append(""); } - default -> stringBuilder.append(diff.text); + default->stringBuilder.append(diff.text); } } return stringBuilder.toString(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java index 47ebee9ad..c9f65ac5d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -89,8 +89,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment @Override public void onItemClick(String id){ Status status=getContentStatusByID(id); - if(status==null) - return; + if(status==null || status.preview) return; if(status.isRemote){ UiUtils.lookupStatus(getContext(), status, accountID, null, status1 -> { status1.filterRevealed = true; @@ -392,6 +391,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment Status contentStatus=status.getContentStatus(); if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){ updatePoll(status.id, contentStatus, ev.poll); + AccountSessionManager.get(accountID).getCacheController().updateStatus(contentStatus); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java index 5fcfa34c4..e6067e2ec 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java @@ -54,21 +54,25 @@ import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.utils.V; public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent { - protected Status mainStatus, updatedStatus; + protected Status mainStatus, updatedStatus, replyTo; private final HashMap ancestryMap = new HashMap<>(); private StatusContext result; - protected boolean contextInitiallyRendered, transitionFinished; + protected boolean contextInitiallyRendered, transitionFinished, preview; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); mainStatus=Parcels.unwrap(getArguments().getParcelable("status")); + replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo")); Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount")); + refreshing=contextInitiallyRendered=getArguments().getBoolean("refresh", false); if(inReplyToAccount!=null) knownAccounts.put(inReplyToAccount.id, inReplyToAccount); data.add(mainStatus); onAppendItems(Collections.singletonList(mainStatus)); - setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis)); + preview=mainStatus.preview; + if(preview) setRefreshEnabled(false); + setTitle(preview ? getString(R.string.sk_post_preview) : HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis)); transitionFinished = getArguments().getBoolean("noTransition", false); E.register(this); @@ -155,11 +159,21 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist @Override protected void doLoadData(int offset, int count){ - if (refreshing) loadMainStatus(); - currentRequest=new GetStatusContext(mainStatus.id) + if(preview && replyTo==null){ + result=new StatusContext(); + result.descendants=Collections.emptyList(); + result.ancestors=Collections.emptyList(); + return; + } + if(refreshing && !preview) loadMainStatus(); + currentRequest=new GetStatusContext(preview ? replyTo.id : mainStatus.id) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(StatusContext result){ + if(preview){ + result.descendants=Collections.emptyList(); + result.ancestors.add(replyTo); + } ThreadFragment.this.result = result; maybeApplyContext(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java index dabbf898d..13e074e84 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java @@ -1,8 +1,6 @@ package org.joinmastodon.android.fragments.report; import android.app.Activity; -import android.graphics.Canvas; -import android.graphics.Paint; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; @@ -25,6 +23,7 @@ import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -97,8 +96,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ .exec(accountID); } - @Override - public void onItemClick(String id){ + public void onToggleItem(String id){ if(selectedIDs.contains(id)) selectedIDs.remove(id); else @@ -121,13 +119,20 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder) return; - outRect.left=V.dp(40); + boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL; + if(isRTL) outRect.right=V.dp(40); + else outRect.left=V.dp(40); if(holder instanceof AudioStatusDisplayItem.Holder){ outRect.bottom=V.dp(16); }else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){ - outRect.bottom=V.dp(16); - outRect.left+=V.dp(16); - outRect.right=V.dp(16); + outRect.bottom=V.dp(8); + if(isRTL){ + outRect.right+=V.dp(16); + outRect.left=V.dp(16); + }else{ + outRect.left+=V.dp(16); + outRect.right=V.dp(16); + } } } }); @@ -155,9 +160,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ return adapter; } - protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){ - } - private void onButtonClick(View v){ Bundle args=new Bundle(); args.putString("account", accountID); @@ -201,7 +203,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ @Override protected List buildDisplayItems(Status s){ - return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN); + List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN); + items.add(new DummyStatusDisplayItem(s.getID(), this)); + return items; } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportReasonChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportReasonChoiceFragment.java index 7834867d0..ede23f10a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportReasonChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportReasonChoiceFragment.java @@ -204,14 +204,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{ float off=paint.getStrokeWidth()/2f; c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), paint); } - - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ - RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); - if(holder instanceof StatusDisplayItem.Holder){ - outRect.left=outRect.right=V.dp(16); - } - } }); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java index 18e9a2080..e564a7eb4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java @@ -2,20 +2,39 @@ package org.joinmastodon.android.fragments.settings; import android.app.Activity; import android.os.Bundle; +import android.util.Log; import android.view.Gravity; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import org.joinmastodon.android.BuildConfig; +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.HasAccountID; +import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.updater.GithubSelfUpdater; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +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.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,10 +45,13 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; -public class SettingsAboutAppFragment extends BaseSettingsFragment implements HasAccountID{ - private ListItem mediaCacheItem; +public class SettingsAboutAppFragment extends BaseSettingsFragment{ + private static final String TAG="SettingsAboutAppFragment"; + private ListItem mediaCacheItem, copyCrashLogItem; + private CheckableListItem enablePreReleasesItem; private AccountSession session; private boolean timelineCacheCleared=false; + private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log"); // MOSHIDON private ListItem clearRecentEmojisItem; @@ -38,22 +60,35 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment impleme super.onCreate(savedInstanceState); setTitle(getString(R.string.about_app, getString(R.string.mo_app_name))); session=AccountSessionManager.get(accountID); - onDataLoaded(List.of( + + String lastModified=crashLogFile.exists() + ? DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT).withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(crashLogFile.lastModified())) + : getString(R.string.sk_settings_crash_log_unavailable); + List> items=new ArrayList<>(List.of( new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.mo_donate_url))), new ListItem<>(R.string.mo_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.mo_repo_url))), new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")), new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true), clearRecentEmojisItem=new ListItem<>(R.string.mo_clear_recent_emoji, 0, this::onClearRecentEmojisClick), mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick), - new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick) + new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick), + copyCrashLogItem=new ListItem<>(getString(R.string.sk_settings_copy_crash_log), lastModified, 0, this::onCopyCrashLog) )); + if(GithubSelfUpdater.needSelfUpdating()){ + items.add(enablePreReleasesItem=new CheckableListItem<>(R.string.sk_updater_enable_pre_releases, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enablePreReleases, i->toggleCheckableItem(enablePreReleasesItem))); + } + + copyCrashLogItem.isEnabled=crashLogFile.exists(); + onDataLoaded(items); updateMediaCacheItem(); } @Override protected void onHidden(){ super.onHidden(); + GlobalUserPreferences.enablePreReleases=enablePreReleasesItem!=null && enablePreReleasesItem.checked; + GlobalUserPreferences.save(); if(timelineCacheCleared) getActivity().recreate(); } @@ -110,4 +145,17 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment impleme public String getAccountID(){ return accountID; } + + private void onCopyCrashLog(ListItem item){ + if(!crashLogFile.exists()) return; + try(InputStream is=new FileInputStream(crashLogFile)){ + BufferedReader reader=new BufferedReader(new InputStreamReader(is)); + StringBuilder sb=new StringBuilder(); + String line; + while ((line=reader.readLine())!=null) sb.append(line).append("\n"); + UiUtils.copyText(list, sb.toString()); + } catch(IOException e){ + Log.e(TAG, "Error reading crash log", e); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java index b87a0c1f2..edaf6b6d6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java @@ -333,13 +333,15 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment{ return; } - UnifiedPush.unregisterApp( - getContext(), - accountID - ); + for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()) { + UnifiedPush.unregisterApp( + getContext(), + accountSession.getID() + ); - //re-register to fcm - AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().registerAccountForPush(getPushSubscription()); + //re-register to fcm + accountSession.getPushSubscriptionManager().registerAccountForPush(getPushSubscription()); + } unifiedPushItem.toggle(); rebindItem(unifiedPushItem); } @@ -349,12 +351,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment{ (dialog, which)->{ String userDistrib = distributors.get(which); UnifiedPush.saveDistributor(getContext(), userDistrib); - UnifiedPush.registerApp( - getContext(), - accountID, - new ArrayList<>(), - getContext().getPackageName() - ); + for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){ + UnifiedPush.registerApp( + getContext(), + accountSession.getID(), + new ArrayList<>(), + getContext().getPackageName() + ); + } unifiedPushItem.toggle(); rebindItem(unifiedPushItem); }).setOnCancelListener(d->rebindItem(unifiedPushItem)).show(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/AkkomaTranslation.java b/mastodon/src/main/java/org/joinmastodon/android/model/AkkomaTranslation.java new file mode 100644 index 000000000..fd024a654 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/AkkomaTranslation.java @@ -0,0 +1,14 @@ +package org.joinmastodon.android.model; + +public class AkkomaTranslation extends BaseModel{ + public String text; + public String detectedLanguage; + + public Translation toTranslation() { + Translation translation=new Translation(); + translation.content=text; + translation.detectedSourceLanguage=detectedLanguage; + translation.provider="Akkoma"; + return translation; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java index f90dfd4bd..aacf41b9a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java @@ -161,11 +161,15 @@ public class Instance extends BaseModel{ case BUBBLE_TIMELINE -> pleromaFeatures .map(f -> f.contains("bubble_timeline")) .orElse(false); + case MACHINE_TRANSLATION -> pleromaFeatures + .map(f -> f.contains("akkoma:machine_translation")) + .orElse(false); }; } public enum Feature { - BUBBLE_TIMELINE + BUBBLE_TIMELINE, + MACHINE_TRANSLATION } @Parcel diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java index 5134bc7de..eec327e16 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -6,6 +6,7 @@ import static org.joinmastodon.android.api.MastodonAPIController.gsonWithoutDese import androidx.annotation.Nullable; import android.text.TextUtils; +import android.util.Pair; import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.RequiredField; @@ -101,6 +102,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ public transient TranslationState translationState=TranslationState.HIDDEN; public transient Translation translation; public transient boolean fromStatusCreated; + public transient boolean preview; public Status(){} @@ -128,6 +130,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ if(filtered!=null) for(FilterResult fr:filtered) fr.postprocess(); + if(quote!=null) + quote.postprocess(); spoilerRevealed=!hasSpoiler(); if(!spoilerRevealed) sensitive=true; @@ -226,16 +230,17 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48"); public boolean isEligibleForTranslation(AccountSession session){ - Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain); - boolean translateEnabled = instanceInfo != null && - instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && - instanceInfo.v2.configuration.translation.enabled; + Instance instanceInfo=AccountSessionManager.getInstance().getInstanceInfo(session.domain); + boolean translateEnabled=instanceInfo!=null && ( + (instanceInfo.v2!=null && instanceInfo.v2.configuration.translation!=null && instanceInfo.v2.configuration.translation.enabled) || + (instanceInfo.isAkkoma() && instanceInfo.hasFeature(Instance.Feature.MACHINE_TRANSLATION)) + ); try { - String bottomText = BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find() + Pair> decoded=BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find() ? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN) : null; - if(bottomText==null || bottomText.length()==0 || bottomText.equals("\u0005")) bottomText=null; + String bottomText=decoded==null || decoded.second.stream().allMatch(s->s.trim().isEmpty()) ? null : decoded.first; if(bottomText!=null){ translation=new Translation(); translation.content=bottomText; @@ -272,7 +277,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ s.visibility=StatusPrivacy.PUBLIC; s.reactions=List.of(); s.mentions=List.of(); - s.tags =List.of(); + s.tags=List.of(); s.emojis=List.of(); s.filtered=List.of(); return s; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java b/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java index ac896d5b1..5495681a0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java @@ -14,6 +14,8 @@ 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.BookmarkedStatusListFragment; +import org.joinmastodon.android.fragments.FavoritedStatusListFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.HomeTimelineFragment; import org.joinmastodon.android.fragments.ListTimelineFragment; @@ -147,6 +149,8 @@ public class TimelineDefinition { case LIST -> listTitle; case HASHTAG -> hashtagName; case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble); + case BOOKMARKS -> ctx.getString(R.string.bookmarks); + case FAVORITES -> ctx.getString(R.string.your_favorites); case CUSTOM_LOCAL_TIMELINE -> domain; }; } @@ -161,6 +165,8 @@ public class TimelineDefinition { case HASHTAG -> Icon.HASHTAG; case CUSTOM_LOCAL_TIMELINE -> Icon.CUSTOM_LOCAL_TIMELINE; case BUBBLE -> Icon.BUBBLE; + case BOOKMARKS -> Icon.BOOKMARKS; + case FAVORITES -> Icon.FAVORITES; }; } @@ -174,6 +180,8 @@ public class TimelineDefinition { case POST_NOTIFICATIONS -> new NotificationsListFragment(); case BUBBLE -> new BubbleTimelineFragment(); case CUSTOM_LOCAL_TIMELINE -> new CustomLocalTimelineFragment(); + case BOOKMARKS -> new BookmarkedStatusListFragment(); + case FAVORITES -> new FavoritedStatusListFragment(); }; } @@ -244,7 +252,19 @@ public class TimelineDefinition { return args; } - public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, CUSTOM_LOCAL_TIMELINE, BUBBLE } + public enum TimelineType { + HOME, + LOCAL, + FEDERATED, + POST_NOTIFICATIONS, + LIST, + HASHTAG, + BUBBLE, + + // not really timelines, but some people want it, so,, + BOOKMARKS, + FAVORITES + } public enum Icon { HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart), @@ -309,6 +329,13 @@ public class TimelineDefinition { DOCTOR(R.drawable.ic_fluent_doctor_24_regular, R.string.sk_icon_doctor), DIAMOND(R.drawable.ic_fluent_premium_24_regular, R.string.sk_icon_diamond), UMBRELLA(R.drawable.ic_fluent_umbrella_24_regular, R.string.sk_icon_umbrella), + WATER(R.drawable.ic_fluent_water_24_regular, R.string.sk_icon_water), + SUN(R.drawable.ic_fluent_weather_sunny_24_regular, R.string.sk_icon_sun), + SUNSET(R.drawable.ic_fluent_weather_sunny_low_24_regular, R.string.sk_icon_sunset), + CLOUD(R.drawable.ic_fluent_cloud_24_regular, R.string.sk_icon_cloud), + THUNDERSTORM(R.drawable.ic_fluent_weather_thunderstorm_24_regular, R.string.sk_icon_thunderstorm), + RAIN(R.drawable.ic_fluent_weather_rain_24_regular, R.string.sk_icon_rain), + SNOWFLAKE(R.drawable.ic_fluent_weather_snowflake_24_regular, R.string.sk_icon_snowflake), HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true), LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true), @@ -318,7 +345,9 @@ public class TimelineDefinition { EXCLUSIVE_LIST(R.drawable.ic_fluent_rss_24_regular, R.string.sk_exclusive_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), - BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true); + BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true), + BOOKMARKS(R.drawable.ic_fluent_bookmark_multiple_24_regular, R.string.bookmarks, true), + FAVORITES(R.drawable.ic_fluent_star_24_regular, R.string.your_favorites, true); public final int iconRes, nameRes; public final boolean hidden; @@ -338,6 +367,8 @@ 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 BOOKMARKS_TIMELINE = new TimelineDefinition(TimelineType.BOOKMARKS); + public static final TimelineDefinition FAVORITES_TIMELINE = new TimelineDefinition(TimelineType.FAVORITES); public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) { @Override public boolean isCompatible(AccountSession session) { @@ -382,6 +413,8 @@ public class TimelineDefinition { LOCAL_TIMELINE, FEDERATED_TIMELINE, POSTS_TIMELINE, - BUBBLE_TIMELINE + BUBBLE_TIMELINE, + BOOKMARKS_TIMELINE, + FAVORITES_TIMELINE ); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java index 764a27e01..1333d76d0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java @@ -32,7 +32,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; public class AudioStatusDisplayItem extends StatusDisplayItem{ - public final Status status; public final Attachment attachment; private final ImageLoaderRequest imageRequest; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CheckableHeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CheckableHeaderStatusDisplayItem.java index d444a89d0..999303f5e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CheckableHeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/CheckableHeaderStatusDisplayItem.java @@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.CheckBox; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.report.ReportAddPostsChoiceFragment; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Notification; -import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.views.CheckableRelativeLayout; @@ -34,8 +34,16 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{ public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_header_checkable, parent); checkbox=findViewById(R.id.checkbox); - view=(CheckableRelativeLayout) itemView; + view=findViewById(R.id.checkbox_wrap); checkbox.setBackground(new CheckBox(activity).getButtonDrawable()); + view.setOnClickListener(this::onToggle); + view.setAccessibilityDelegate(new View.AccessibilityDelegate(){ + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info){ + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(CheckBox.class.getName()); + } + }); } @Override @@ -46,6 +54,12 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{ } } + private void onToggle(View v){ + if(item.parentFragment instanceof ReportAddPostsChoiceFragment reportFragment){ + reportFragment.onToggleItem(item.parentID); + } + } + public void setIsChecked(Predicate isChecked){ this.isChecked=isChecked; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java index d874df595..f75d17987 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java @@ -58,7 +58,6 @@ import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { - public final Status status; private final Drawable placeholder; private final boolean hideEmpty, forAnnouncement, playGifs; private final String accountID; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java index b55f8a8bc..c851a9e07 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java @@ -37,7 +37,6 @@ import androidx.annotation.PluralsRes; import me.grishka.appkit.Nav; public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ - public final Status status; public final String accountID; private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); @@ -131,6 +130,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ } private void startAccountListFragment(Class cls){ + if(item.status.preview) return; Bundle args=new Bundle(); args.putString("account", item.parentFragment.getAccountID()); args.putParcelable("status", Parcels.wrap(item.status)); @@ -138,6 +138,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ } private void startEditHistoryFragment(){ + if(item.status.preview) return; Bundle args=new Bundle(); args.putString("account", item.parentFragment.getAccountID()); args.putString("id", item.status.id); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java index 651dae830..76a2c79e6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -176,6 +176,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private boolean onButtonTouch(View v, MotionEvent event){ + if(item.status.preview) return false; boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame && parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled()); int action = event.getAction(); @@ -198,6 +199,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private void onReplyClick(View v){ + if(item.status.preview) return; if(item.status.isRemote){ UiUtils.lookupStatus(v.getContext(), item.status, item.accountID, null, @@ -219,6 +221,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private boolean onReplyLongClick(View v) { + if(item.status.preview) return false; if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> { Bundle args=new Bundle(); @@ -234,6 +237,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private void onBoostClick(View v){ + if(item.status.preview) return; if (GlobalUserPreferences.confirmBoost) { UiUtils.opacityIn(v); onBoostLongClick(v); @@ -263,6 +267,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private boolean onBoostLongClick(View v){ + if(item.status.preview) return false; Context ctx = itemView.getContext(); View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null); Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create(); @@ -358,6 +363,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private void onFavoriteClick(View v){ + if(item.status.preview) return; if(item.status.isRemote){ UiUtils.lookupStatus(v.getContext(), item.status, item.accountID, null, @@ -389,6 +395,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private boolean onFavoriteLongClick(View v) { + if(item.status.preview) return false; if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; UiUtils.pickInteractAs(v.getContext(), item.accountID, item.status, @@ -403,6 +410,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private void onBookmarkClick(View v){ + if(item.status.preview) return; if(item.status.isRemote){ UiUtils.lookupStatus(v.getContext(), item.status, item.accountID, null, @@ -426,6 +434,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private boolean onBookmarkLongClick(View v) { + if(item.status.preview) return false; if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; UiUtils.pickInteractAs(v.getContext(), item.accountID, item.status, @@ -440,6 +449,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private void onShareClick(View v){ + if(item.status.preview) return; UiUtils.opacityIn(v); Intent intent=new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); @@ -448,6 +458,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } private boolean onShareLongClick(View v){ + if(item.status.preview) return false; UiUtils.copyText(v, item.status.url); return true; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GapStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GapStatusDisplayItem.java index 4d0a087e8..9ff796fb9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GapStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GapStatusDisplayItem.java @@ -19,7 +19,6 @@ import me.grishka.appkit.utils.V; public class GapStatusDisplayItem extends StatusDisplayItem{ public boolean loading; - private final Status status; public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){ super(parentID, parentFragment); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index b5c447d0e..1d7070880 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -78,7 +78,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ private String accountID; private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private SpannableStringBuilder parsedName; - public final Status status; public boolean hasVisibilityToggle; boolean needBottomPadding; private CharSequence extraText; @@ -458,6 +457,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ } private void onMoreClick(View v){ + if(item.status.preview) return; updateOptionsMenu(); optionsMenu.show(); if(relationship==null && currentRelationshipRequest==null){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java index afba9b2fe..c10570454 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java @@ -24,7 +24,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; public class LinkCardStatusDisplayItem extends StatusDisplayItem{ - private final Status status; private final UrlImageLoaderRequest imgRequest; public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, boolean showImagePreview){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java index bc1d01689..bebf42351 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java @@ -61,7 +61,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{ private final List attachments; private final Map> translatedAttachments = new HashMap<>(); private final ArrayList requests=new ArrayList<>(); - public final Status status; public String sensitiveTitle; public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List attachments, Status status){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java index 096b1f18d..84df641a1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java @@ -43,7 +43,6 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ public boolean needBottomPadding; ReblogOrReplyLineStatusDisplayItem extra; CharSequence fullText; - Status status; public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) { this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java index e8feadaae..050988508 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java @@ -24,7 +24,6 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; public class SpoilerStatusDisplayItem extends StatusDisplayItem{ - public final Status status; public final ArrayList contentItems=new ArrayList<>(); private final CharSequence parsedTitle; private CharSequence translatedTitle; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 5eade5bab..2e7cfaf97 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -59,13 +59,15 @@ import me.grishka.appkit.views.UsableRecyclerView; public abstract class StatusDisplayItem{ public final String parentID; public final BaseStatusListFragment parentFragment; + public Status status; public boolean inset; public int index; public boolean hasDescendantNeighbor=false, hasAncestoringNeighbor=false, isMainStatus=true, - isDirectDescendant=false; + isDirectDescendant=false, + isForQuote=false; public static final int FLAG_INSET=1; public static final int FLAG_NO_FOOTER=1 << 1; @@ -74,7 +76,8 @@ public abstract class StatusDisplayItem{ public static final int FLAG_NO_HEADER=1 << 4; public static final int FLAG_NO_TRANSLATE=1 << 5; public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6; - public static final int FLAG_NO_MEDIA_PREVIEW=1 << 7; + public static final int FLAG_IS_FOR_QUOTE=1 << 7; + public static final int FLAG_NO_MEDIA_PREVIEW=1 << 8; public void setAncestryInfo( boolean hasDescendantNeighbor, @@ -238,27 +241,28 @@ public abstract class StatusDisplayItem{ if(statusForContent.hasSpoiler()){ if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true; SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER); + if((flags & FLAG_IS_FOR_QUOTE)!=0){ + for(StatusDisplayItem item:spoilerItem.contentItems){ + item.isForQuote=true; + } + } items.add(spoilerItem); contentItems=spoilerItem.contentItems; }else{ contentItems=items; } - if (statusForContent.quote != null) { - boolean hasQuoteInlineTag = statusForContent.content.contains(""); - if (!hasQuoteInlineTag) { - String quoteUrl = statusForContent.quote.url; - String quoteInline = String.format("%sRE: %s", - statusForContent.content.endsWith("

") ? "" : "

", quoteUrl, quoteUrl); - statusForContent.content += quoteInline; - } + if(statusForContent.quote!=null) { + int quoteInlineIndex=statusForContent.content.lastIndexOf("

RE:"); + if (quoteInlineIndex!=-1) + statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex); } boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText); if(!TextUtils.isEmpty(statusForContent.content)){ - SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID); + SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()); HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); - TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); + TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); contentItems.add(text); }else if(!hasSpoiler && header!=null){ header.needBottomPadding=true; @@ -276,9 +280,11 @@ public abstract class StatusDisplayItem{ } PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent); - if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0) + if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){ mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden); - else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia) + statusForContent.sensitiveRevealed=false; + statusForContent.sensitive=true; + } else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia) statusForContent.sensitiveRevealed=true; contentItems.add(mediaGrid); } @@ -295,17 +301,23 @@ public abstract class StatusDisplayItem{ } } if(statusForContent.poll!=null){ - buildPollItems(parentID, fragment, statusForContent.poll, contentItems, statusForContent); + buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems, statusForContent); } - if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){ + if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null){ contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags & FLAG_NO_MEDIA_PREVIEW)==0)); } + if(statusForContent.quote!=null && !(parentObject instanceof Notification)){ + if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment + contentItems.add(new DummyStatusDisplayItem(parentID, fragment)); + contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE)); + } if(contentItems!=items && statusForContent.spoilerRevealed){ items.addAll(contentItems); } AccountLocalPreferences lp=fragment.getLocalPrefs(); - if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && lp.emojiReactionsEnabled && - (lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment)){ + if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled && + (lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) && + statusForContent.reactions!=null){ boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id); boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus; items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false)); @@ -317,8 +329,9 @@ public abstract class StatusDisplayItem{ items.add(footer); } boolean inset=(flags & FLAG_INSET)!=0; + boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0; // add inset dummy so last content item doesn't clip out of inset bounds - if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0){ + if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){ items.add(new DummyStatusDisplayItem(parentID, fragment)); // in case we ever need the dummy to display a margin for the media grid again: // (i forgot why we apparently don't need this anymore) @@ -330,12 +343,22 @@ public abstract class StatusDisplayItem{ items.add(gap=new GapStatusDisplayItem(parentID, fragment, status)); int i=1; for(StatusDisplayItem item:items){ - item.inset=inset; + if(inset) + item.inset=true; + if(isForQuote){ + item.status=statusForContent; + item.isForQuote=true; + } item.index=i++; } if(items!=contentItems && !statusForContent.spoilerRevealed){ for(StatusDisplayItem item:contentItems){ - item.inset=inset; + if(inset) + item.inset=true; + if(isForQuote){ + item.status=statusForContent; + item.isForQuote=true; + } item.index=i++; } } @@ -353,7 +376,7 @@ public abstract class StatusDisplayItem{ ); } - public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List items, Status status){ + public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List items, Status status){ int i=0; for(Poll.Option opt:poll.options){ items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment, status)); @@ -390,12 +413,15 @@ public abstract class StatusDisplayItem{ } public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ + private Context context; + public Holder(View itemView){ super(itemView); } public Holder(Context context, int layout, ViewGroup parent){ super(context, layout, parent); + this.context=context; } public String getItemID(){ @@ -404,6 +430,16 @@ public abstract class StatusDisplayItem{ @Override public void onClick(){ + if(item.isForQuote){ + item.status.filterRevealed=true; + Bundle args=new Bundle(); + args.putString("account", item.parentFragment.getAccountID()); + args.putParcelable("status", Parcels.wrap(item.status.clone())); + args.putBoolean("refresh", true); + Nav.go((Activity) context, ThreadFragment.class, args); + return; + } + item.parentFragment.onItemClick(item.parentID); } @@ -435,13 +471,13 @@ public abstract class StatusDisplayItem{ public boolean isLastDisplayItemForStatus(){ return getNextVisibleDisplayItem() - .map(n->!n.parentID.equals(item.parentID)) + .map(next->!next.parentID.equals(item.parentID) || item.inset && !next.inset) .orElse(true); } @Override public boolean isEnabled(){ - return item.parentFragment.isItemEnabled(item.parentID); + return item.parentFragment.isItemEnabled(item.parentID) || item.isForQuote; } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 5a562cb43..b1bc84ae2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -42,7 +42,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ public boolean textSelectable; public boolean reduceTopPadding; public boolean disableTranslate; - public final Status status; public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){ super(parentID, parentFragment); @@ -116,7 +115,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ text.setText(item.text); } text.setTextIsSelectable(false); - if(item.textSelectable) itemView.post(() -> text.setTextIsSelectable(true)); + if(item.textSelectable && !item.isForQuote) itemView.post(() -> text.setTextIsSelectable(true)); text.setInvalidateOnEveryFrame(false); itemView.setClickable(false); itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom()); @@ -127,8 +126,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null); if(next!=null && !next.parentID.equals(item.parentID)) next=null; - int bottomPadding=next instanceof FooterStatusDisplayItem ? V.dp(6) - : item.inset ? V.dp(12) + int bottomPadding=item.inset ? V.dp(12) + : next instanceof FooterStatusDisplayItem ? V.dp(6) : (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0 : V.dp(12); itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding); @@ -195,7 +194,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ public void updateTranslation(boolean updateText){ if(item.status==null) return; - boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()); + boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()) && !item.isForQuote; if(translationFooter==null && translateEnabled){ translationFooter=translationFooterStub.inflate(); translationInfo=findViewById(R.id.translation_info_text); @@ -214,8 +213,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null; String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language; Locale locale=lang!=null ? Locale.forLanguageTag(lang) : null; - translationButton.setText(locale!=null - ? item.parentFragment.getString(R.string.translate_post, locale.getDisplayLanguage()) + String displayLang=locale==null || locale.getDisplayLanguage().isBlank() ? lang : locale.getDisplayLanguage(); + translationButton.setText(displayLang!=null + ? item.parentFragment.getString(R.string.translate_post, displayLang) : item.parentFragment.getString(R.string.sk_translate_post)); translationButton.setClickable(true); translationButton.animate().alpha(1).setDuration(100).start(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java index a8e6d4751..37b9d5ec2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java @@ -18,7 +18,6 @@ import java.util.List; // Mind the gap! public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{ public boolean loading; - public final Status status; public List filteredItems; public LegacyFilter applyingFilter; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/DiffRemovedSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/DiffRemovedSpan.java index 6f58dc121..481377181 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/DiffRemovedSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/DiffRemovedSpan.java @@ -3,21 +3,21 @@ package org.joinmastodon.android.ui.text; import android.text.TextPaint; import android.text.style.CharacterStyle; -import org.joinmastodon.android.ui.utils.UiUtils; - public class DiffRemovedSpan extends CharacterStyle { private final String text; + private final int color; - public DiffRemovedSpan(String text){ + public DiffRemovedSpan(String text, int color){ this.text=text; + this.color=color; } @Override public void updateDrawState(TextPaint tp) { tp.setStrikeThruText(true); - tp.setColor(0xFFCA5B63); + tp.setColor(color); } public String getText() { diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index b4b58d120..d47942054 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -70,6 +70,10 @@ public class HtmlParser{ private HtmlParser(){} + public static SpannableStringBuilder parse(String source, List emojis, List mentions, List tags, String accountID){ + return parse(source, emojis, mentions, tags, accountID, null); + } + /** * Parse HTML and custom emoji into a spanned string for display. * Supported tags:
    @@ -82,7 +86,7 @@ public class HtmlParser{ * @param emojis Custom emojis that are present in source as :code: * @return a spanned string */ - public static SpannableStringBuilder parse(String source, List emojis, List mentions, List tags, String accountID){ + public static SpannableStringBuilder parse(String source, List emojis, List mentions, List tags, String accountID, Context context){ class SpanInfo{ public Object span; public int start; @@ -107,6 +111,9 @@ public class HtmlParser{ Map tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity())); final SpannableStringBuilder ssb=new SpannableStringBuilder(); + int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success); + int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error); + Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ private final ArrayList openSpans=new ArrayList<>(); @@ -172,9 +179,9 @@ public class HtmlParser{ } case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el)); case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el)); - //fake elements for the edit history diff view - case "edit_diff_added" -> openSpans.add(new SpanInfo(new ForegroundColorSpan(UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63), ssb.length(), el)); - case "edit_diff_removed" -> openSpans.add(new SpanInfo(new DiffRemovedSpan(el.text()), ssb.length(), el)); + // fake elements for the edit history diff view + case "edit-diff-insert" -> openSpans.add(new SpanInfo(new ForegroundColorSpan(colorInsert), ssb.length(), el)); + case "edit-diff-delete" -> openSpans.add(new SpanInfo(new DiffRemovedSpan(el.text(), colorDelete), ssb.length(), el)); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index dcd0070fe..ac0029175 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -1720,12 +1720,14 @@ public class UiUtils { "pronouns.page/" }; - private static final Pattern trimPronouns=Pattern.compile("[^\\w*]*([\\w*].*[\\w*]|[\\w*])\\W*"); + private static final String PRONOUN_CHARS="\\w*¿¡!?"; + private static final Pattern trimPronouns= + Pattern.compile("[^"+PRONOUN_CHARS+"]*(["+PRONOUN_CHARS+"].*["+PRONOUN_CHARS+"]|["+PRONOUN_CHARS+"])\\W*"); private static String extractPronounsFromField(String localizedPronouns, AccountField field) { if(!field.name.toLowerCase().contains(localizedPronouns) && !field.name.toLowerCase().contains("pronouns")) return null; String text=HtmlParser.text(field.value); - if(field.value.toLowerCase().contains("https://")){ + if(text.toLowerCase().contains("https://")){ for(String pronounUrl : pronounsUrls){ int index=text.indexOf(pronounUrl); int beginPronouns=index+pronounUrl.length(); @@ -1744,13 +1746,20 @@ public class UiUtils { Matcher matcher=trimPronouns.matcher(text); if(!matcher.find()) return null; String pronouns=matcher.group(1); - // crude fix to allow for pronouns like "it(/she)" - int missingClosingParens=0; + + // crude fix to allow for pronouns like "it(/she)" or "(de) sie/ihr" + int missingParens=0, missingBrackets=0; for(char c : pronouns.toCharArray()){ - if(c=='(') missingClosingParens++; - if(c==')') missingClosingParens--; + if(c=='(') missingParens++; + else if(c=='[') missingBrackets++; + else if(c==')') missingParens--; + else if(c==']') missingBrackets--; } - pronouns+=")".repeat(Math.max(0, missingClosingParens)); + if(missingParens > 0) pronouns+=")".repeat(missingParens); + else if(missingParens < 0) pronouns="(".repeat(missingParens*-1)+pronouns; + if(missingBrackets > 0) pronouns+="]".repeat(missingBrackets); + else if(missingBrackets < 0) pronouns="[".repeat(missingBrackets*-1)+pronouns; + // if ends with an un-closed custom emoji if(pronouns.matches("^.*\\s+:[a-zA-Z_]+$")) pronouns+=':'; return pronouns; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java index efd238071..caa89f797 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java @@ -217,6 +217,7 @@ public class AccountViewHolder extends BindableViewHolder impl Menu menu=contextMenu.getMenu(); Account account=item.account; + menu.findItem(R.id.edit_note).setVisible(false); menu.findItem(R.id.manage_user_lists).setTitle(fragment.getString(R.string.sk_lists_with_user, account.getShortUsername())); MenuItem mute=menu.findItem(R.id.mute); mute.setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername())); diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java index 12f3d4891..7bf862178 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java @@ -1,9 +1,9 @@ package org.joinmastodon.android.utils; -import android.text.TextUtils; - -import org.joinmastodon.android.fragments.ComposeFragment; +import android.util.Pair; +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; import java.util.regex.MatchResult; import java.util.regex.Matcher; @@ -40,19 +40,22 @@ public class StatusTextEncoder { } // prettiest almost-exact replica of a pretty function - public String decode(String content, Pattern regex) { - Matcher m = regex.matcher(content); - StringBuilder decodedString = new StringBuilder(); - int previousEnd = 0; + public Pair> decode(String content, Pattern regex) { + Matcher m=regex.matcher(content); + StringBuilder decodedString=new StringBuilder(); + List decodedParts=new ArrayList<>(); + int previousEnd=0; while (m.find()) { - MatchResult res = m.toMatchResult(); + MatchResult res=m.toMatchResult(); // everything before the match - do not decode decodedString.append(content.substring(previousEnd, res.start())); - previousEnd = res.end(); + previousEnd=res.end(); // the match - do decode - decodedString.append(fn.apply(res.group())); + String decoded=fn.apply(res.group()); + decodedParts.add(decoded); + decodedString.append(decoded); } decodedString.append(content.substring(previousEnd)); - return decodedString.toString(); + return Pair.create(decodedString.toString(), decodedParts); } } diff --git a/mastodon/src/main/res/drawable/ic_fluent_cloud_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_cloud_24_regular.xml new file mode 100644 index 000000000..e7bdc8906 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_cloud_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_person_note_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_person_note_24_regular.xml new file mode 100644 index 000000000..ad4287690 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_person_note_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_receipt_sparkles_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_receipt_sparkles_24_regular.xml new file mode 100644 index 000000000..80cd0471c --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_receipt_sparkles_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_water_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_water_24_regular.xml new file mode 100644 index 000000000..bf8cb5814 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_water_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_weather_rain_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_weather_rain_24_regular.xml new file mode 100644 index 000000000..b34e1d9b0 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_weather_rain_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_weather_snowflake_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_weather_snowflake_24_regular.xml new file mode 100644 index 000000000..e2f13dec3 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_weather_snowflake_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_weather_sunny_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_weather_sunny_24_regular.xml new file mode 100644 index 000000000..9292857cf --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_weather_sunny_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_weather_sunny_low_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_weather_sunny_low_24_regular.xml new file mode 100644 index 000000000..06112f857 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_weather_sunny_low_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_weather_thunderstorm_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_weather_thunderstorm_24_regular.xml new file mode 100644 index 000000000..3a6b081a7 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_weather_thunderstorm_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/display_item_header_checkable.xml b/mastodon/src/main/res/layout/display_item_header_checkable.xml index 7b51fa150..5c8ffc70c 100644 --- a/mastodon/src/main/res/layout/display_item_header_checkable.xml +++ b/mastodon/src/main/res/layout/display_item_header_checkable.xml @@ -1,40 +1,39 @@ - - + android:layout_width="56dp" + android:layout_height="match_parent" + android:paddingTop="16dp"> - + - + + + + + - \ No newline at end of file + diff --git a/mastodon/src/main/res/layout/fragment_profile.xml b/mastodon/src/main/res/layout/fragment_profile.xml index 12e0f3021..909a2e91d 100644 --- a/mastodon/src/main/res/layout/fragment_profile.xml +++ b/mastodon/src/main/res/layout/fragment_profile.xml @@ -13,7 +13,7 @@ @@ -80,11 +80,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/cover" - android:layout_alignParentEnd="true"> + android:layout_alignParentEnd="true" + android:clipChildren="false"> @@ -95,7 +97,8 @@ style="@style/Widget.Mastodon.M3.Button.Tonal" android:background="@drawable/bg_button_m3_tonal_circle_selector" android:paddingStart="12dp" - android:drawableStart="@drawable/ic_fluent_alert_24_selector" /> + android:drawableStart="@drawable/ic_fluent_alert_24_selector" + tools:ignore="RtlSymmetry" /> @@ -219,7 +223,7 @@ android:layout_below="@id/username" android:id="@+id/note_edit_wrap" android:layout_marginTop="4dp" - android:layout_marginBottom="16dp" + android:layout_marginBottom="12dp" android:layout_marginHorizontal="16dp" android:visibility="gone"> @@ -227,32 +231,44 @@ android:id="@+id/note_edit" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingVertical="16dp" - android:inputType="textMultiLine|textCapSentences" + android:minHeight="52dp" + android:paddingVertical="15dp" + android:textColor="?colorM3OnSurface" + android:inputType="text|textMultiLine|textCapSentences" android:singleLine="false" - android:drawablePadding="12dp" - android:drawableTint="?android:textColorSecondary" android:background="@drawable/bg_note_edit" - android:paddingEnd="48dp" - android:paddingHorizontal="16dp" + android:paddingEnd="52dp" + android:paddingStart="20dp" android:elevation="0dp" - android:visibility="gone" - android:hint="@string/mo_personal_note"/> + android:hint="@string/mo_personal_note" + tools:ignore="RtlSymmetry" /> + + + + + + + + - - - - - - - - - - - - + tools:text="Eugen" /> diff --git a/mastodon/src/main/res/menu/compose_more.xml b/mastodon/src/main/res/menu/compose_more.xml index a47109276..3671739ff 100644 --- a/mastodon/src/main/res/menu/compose_more.xml +++ b/mastodon/src/main/res/menu/compose_more.xml @@ -5,4 +5,5 @@ + diff --git a/mastodon/src/main/res/menu/profile.xml b/mastodon/src/main/res/menu/profile.xml index aaaf7cae8..6d93f8a60 100644 --- a/mastodon/src/main/res/menu/profile.xml +++ b/mastodon/src/main/res/menu/profile.xml @@ -1,6 +1,9 @@ + + + @@ -9,7 +12,7 @@ - + diff --git a/mastodon/src/main/res/values-de-rDE/strings_sk.xml b/mastodon/src/main/res/values-de-rDE/strings_sk.xml index 3543a1249..6cc62c53a 100644 --- a/mastodon/src/main/res/values-de-rDE/strings_sk.xml +++ b/mastodon/src/main/res/values-de-rDE/strings_sk.xml @@ -415,5 +415,23 @@ Standard-Sichtbarkeit für Posts Neue Follower_innen manuell genehmigen Start-Timeline geleert - Befreundet + Befreundet + Schneeflocke + Wolke + Sonnenuntergang + Private Notiz über dieses Profil hinzufügen + Wasser + Sonne + Regen + Gewitter + Notiz speichern fehlgeschlagen + Private Notiz löschen + Änderungen bestätigen + Keines verfügbar… noch + Absturzprotokoll kopiert + Private Notiz hinzufügen + Neuestes Absturzprotokoll kopieren + Vorschau öffnen + Vorschau + Private Notiz über %s löschen\? \ No newline at end of file diff --git a/mastodon/src/main/res/values-eo/strings_sk.xml b/mastodon/src/main/res/values-eo/strings_sk.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/mastodon/src/main/res/values-eo/strings_sk.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mastodon/src/main/res/values-es-rES/strings_sk.xml b/mastodon/src/main/res/values-es-rES/strings_sk.xml index d70600c80..c6c00e6cd 100644 --- a/mastodon/src/main/res/values-es-rES/strings_sk.xml +++ b/mastodon/src/main/res/values-es-rES/strings_sk.xml @@ -403,15 +403,28 @@ Estas noticias están dando que hablar en todo el Fediverso. Cuentas bloqueadas Cuentas silenciadas - Utilizar el corazón como icono favorito + Utilizar un corazón como icono de favorito Utilizado recientemente Establecer por defecto Por defecto (%s) - Enlaces subrayados + Subrayar enlaces Editar el texto alternativo - Visibilidad de la publicación predeterminada + Visibilidad de publicación predeterminada Aprobar nuevos seguidores manualmente - Caché de la línea de tiempo de inicio borrada - Borrar la caché de la línea de tiempo de inicio + Caché de la cronología de inicio borrada + Borrar la caché de la cronología de inicio Amigos + Copos de nieve + Confirmar los cambios en la nota + No se pudo guardar la nota + Nube + Borrar la nota personal + Puesta de sol + Añadir una nota personal sobre este perfil + Agua + Sol + Añadir una nota personal + Lluvia + Tormenta eléctrica + ¿Borrar la nota personal sobre %s\? \ No newline at end of file diff --git a/mastodon/src/main/res/values-fa/strings_sk.xml b/mastodon/src/main/res/values-fa/strings_sk.xml index d801c695d..bd1c9c675 100644 --- a/mastodon/src/main/res/values-fa/strings_sk.xml +++ b/mastodon/src/main/res/values-fa/strings_sk.xml @@ -403,4 +403,9 @@ حساب‌های خموش شده اخیرا مورد استفاده قرار گرفته ویرایش متن جایگزین + نمایانی فرسته پیش‌گزیده + تایید دستی پیگیران جدید + کش خط زمانی خانه پاک شد + متقابل + پاک کردن کش خط زمانی خانه \ No newline at end of file diff --git a/mastodon/src/main/res/values-fr-rFR/strings_sk.xml b/mastodon/src/main/res/values-fr-rFR/strings_sk.xml index 666ba8345..f7cd50e39 100644 --- a/mastodon/src/main/res/values-fr-rFR/strings_sk.xml +++ b/mastodon/src/main/res/values-fr-rFR/strings_sk.xml @@ -297,7 +297,7 @@ Charger des informations à partir d\'instances distantes informations distantes indisponibles Échec du chargement du profil via %s - Essayez de récupérer des listes plus précises pour les abonnés, les likes et les boosts en chargeant les informations à partir de l\'instance d\'origine. + Essaye de récupérer des listes plus précises pour les abonné·e·s, les favoris et les boosts en chargeant les informations à partir de l\'instance d\'origine. Révéler les CW identiques dans les réponses Jamais Réponses du même auteur @@ -412,7 +412,7 @@ Cache des messages vidé Effacer le cache du fil d\'accueil Visibilité de publication par défaut - Approuver manuellement les nouveaux abonnés + Approuver manuellement les nouveaux·elles abonné·e·s Cache du fil d\'accueil vidé - Amis + Suivi mutuel \ No newline at end of file diff --git a/mastodon/src/main/res/values-hr-rHR/strings_sk.xml b/mastodon/src/main/res/values-hr-rHR/strings_sk.xml index 0ca4617a6..605edb02d 100644 --- a/mastodon/src/main/res/values-hr-rHR/strings_sk.xml +++ b/mastodon/src/main/res/values-hr-rHR/strings_sk.xml @@ -157,7 +157,7 @@ Nenavedeno Liste s %s Prikaži objedinjenu vremensku traku - "Prevedeno uporabom %s" + Prevedeno uporabom %s Prevodi samo otvorene objave Pretraga na Fediversu Pretraga na %s diff --git a/mastodon/src/main/res/values-ja-rJP/strings_sk.xml b/mastodon/src/main/res/values-ja-rJP/strings_sk.xml index 3dab4f5ed..4ac517d26 100644 --- a/mastodon/src/main/res/values-ja-rJP/strings_sk.xml +++ b/mastodon/src/main/res/values-ja-rJP/strings_sk.xml @@ -4,7 +4,7 @@ 削除して再編集 投稿を削除して再編集 本当にこの投稿を削除して再編集しますか? - プロファイルに固定 + プロフィールに固定 投稿をプロフィールに固定 この投稿をあなたのプロフィールに固定しますか? 投稿を固定しています… @@ -154,7 +154,7 @@ 投稿は10分以上後の予約である必要があります。 UnifiedPush を使用 ディストリビューターを選択 - フォローされたユーザー + フォロー中のユーザー リストを削除 本当にリスト “%s” を削除しますか? リストを編集 @@ -288,7 +288,7 @@ ユーザーリストに代名詞を表示 UnifiedPush による通知を動作させるにはディストリビューターをインストールする必要があります。詳しくは https://unifiedpush.org/ をご覧ください リストのメンバー - サーバーはローカルのみの投稿をサポートしています + ローカルのみの投稿をサポートするサーバー コンテンツを表示 高度な設定を非表示にする 登録済み @@ -302,7 +302,7 @@ 報告済み デフォルトに設定 - お気に入りアイコンにハートを使用する + お気に入りアイコンにハートを使用 HTML デフォルト (%s) アプリで開く @@ -354,4 +354,34 @@ スレッドを表示 ホームタイムラインキャッシュをクリア ハッシュタグは空欄にできません + 発信元のインスタンスから情報を読み込んで、フォロワー、いいね、ブーストのより正確なリストを取得しましょう。 + 錠剤型のアクティブタブインジケーターを無効にする + 投稿を開いた時のみ + Glitchのローカル限定モード + リモートインスタンスから情報を読み込む + 投稿通知を有効にすると、その人の新しい投稿がここに表示されます。 + 全員の返信 + 空の絵文字リアクションを隠す + 絵文字リアクションをタイムラインに表示 + 投稿のデフォルト公開範囲 + 下線付きリンク + なし + 代替テキストがあることを示す表示 + なし + 新しいフォロワーを手動で承認 + ホームインスタンスがGlitchで動作している場合、これを有効にしてください。HometownやAkkomaでは不要です。 + 返信内にある同一のCWを自動で展開 + 代替テキストがないことを示す表示 + リモートの情報が利用できません + 同じ作成者の返信 + %s からのプロフィールの読み込みに失敗しました + これらの投稿は現在Fediverseで人気を集めています。 + 返信 + ブーストする前に確認 + 投稿に対する絵文字リアクションを表示、追加することができます。様々なFediverseサーバーがこれに対応していますが、Mastodonは対応していません。 + 最近使用 + 相互フォロー中 + 投票結果 + メディアを含む投稿 + 代替テキストを編集 \ No newline at end of file diff --git a/mastodon/src/main/res/values-ro-rRO/strings_sk.xml b/mastodon/src/main/res/values-ro-rRO/strings_sk.xml index efd39b47f..8b597a1a3 100644 --- a/mastodon/src/main/res/values-ro-rRO/strings_sk.xml +++ b/mastodon/src/main/res/values-ro-rRO/strings_sk.xml @@ -409,4 +409,5 @@ Aprobați manual urmăritori noi Memoria cache a cronologiei acasă a fost ștearsă Ștergeți memoria cache a cronologiei acasă + Prieteni \ No newline at end of file diff --git a/mastodon/src/main/res/values-uk-rUA/strings_sk.xml b/mastodon/src/main/res/values-uk-rUA/strings_sk.xml index 2347ed0cd..3102d328e 100644 --- a/mastodon/src/main/res/values-uk-rUA/strings_sk.xml +++ b/mastodon/src/main/res/values-uk-rUA/strings_sk.xml @@ -415,4 +415,24 @@ Усталена видимість дописів Затверджувати нових підписників уручну Кеш домашньої стрічки очищено + Друзі + Сніжинка + Підтвердити зміни + Не вдалося зберегти нотатку + Хмара + Видалити нотатку + Захід сонця + Додати нотатку для себе про цей профіль + Вода + Сонце + Додати нотатку + Дощ + Гроза + Видалити нотатку про %s\? + Підтвердити зміни + Немає… поки що + Журнал збоїв скопійовано + Копіювати останній журнал збоїв + Попередній перегляд допису + Попередній перегляд \ No newline at end of file diff --git a/mastodon/src/main/res/values-zh-rCN/strings_sk.xml b/mastodon/src/main/res/values-zh-rCN/strings_sk.xml index 045c359a1..01821c14b 100644 --- a/mastodon/src/main/res/values-zh-rCN/strings_sk.xml +++ b/mastodon/src/main/res/values-zh-rCN/strings_sk.xml @@ -2,29 +2,29 @@ 置顶 删除并重新编辑 - 删除并重新编辑帖文 - 确定要删除并重新编辑此帖文吗? + 删除并重新编辑嘟文 + 确定要删除并重新编辑此嘟文吗? 置顶 - 置顶帖文 - 你确定要在资料页置顶此帖文吗? - 正在置顶帖文… + 置顶嘟文 + 你确定要在资料页置顶此嘟文吗? + 正在置顶嘟文… 取消置顶 - 取消帖文置顶 - 你确定不再置顶此帖文吗? + 取消嘟文置顶 + 你确定不再置顶此嘟文吗? 正在取消置顶… 图片描述 不公开 联邦时间轴 - 这些是互联实例中最新发布的帖文。 + 这些是互联实例中最新发布的嘟文。 Megalodon 显示回复 显示转嘟 - 自动加载新帖文 + 自动加载新嘟文 显示互动次数 Megalodon v%1$s (%2$d) 标记为敏感媒体 - 启用 %s 的帖文通知 - 关闭 %s 的帖文通知 + 启用 %s 的嘟文通知 + 关闭 %s 的嘟文通知 Megalodon %s 已经可以下载了。 Megalodon %s 已下载,准备安装。 检查更新 @@ -45,8 +45,8 @@ - 帖文 - 帖文通知 + 嘟文 + 嘟文通知 翻译 显示原文 允许多选 @@ -84,8 +84,8 @@ 在联邦宇宙上查找 撤销转嘟 转嘟可见性 - 引用此帖文 - 复制帖文链接 + 引用此嘟文 + 复制嘟文链接 你关注的标签 在 %s 上查找 找不到资源 @@ -101,27 +101,27 @@ 已转嘟过 用其他账号回复 所有通知的统一图标 - 未发送的帖文 + 未发送的嘟文 删除草稿 草稿 定时 - 删除定时帖文 - 你确定要删除此定时帖文吗? + 删除定时嘟文 + 你确定要删除此定时嘟文吗? 草稿或定时 - 帖文将保存为草稿。 + 嘟文将保存为草稿。 定时于 草稿已保存 - 帖文已定时 + 嘟文已定时 转嘟给 %s - 你确定要删除此帖文草稿吗? + 你确定要删除此嘟文草稿吗? 定时时间过早 - 帖文只能设置为 10 分钟或更晚发送。 + 嘟文只能设置为 10 分钟或更晚发送。 保存为草稿? 保存更改? 保存草稿? 保存更改? 标记为草稿 - 定时帖文 + 定时嘟文 不要定时 不要标记为草稿 减少动画效果 @@ -151,14 +151,14 @@ 本站 至少有一个附件不包含描述。 仍然发布 - 如果你为某些人启用了帖文通知,其新帖文将显示在此处。 + 如果你为某些人启用了嘟文通知,其新嘟文将显示在此处。 时间线 编辑时间线 ALT 编辑 - 编辑帖文 + 编辑嘟文 缺少 ALT 文本 - 帖文 + 嘟文 添加 时间线 列表 @@ -223,7 +223,7 @@ 耳机 人类 地球 - 编辑已转嘟帖文 + 编辑已转嘟嘟文 钉子 通过屏蔽并立即解除屏蔽以移除%s的关注者身份? 拍板 @@ -248,7 +248,7 @@ 如果你的主实例运行 Glitch,请启用此功能。Hometown 或 Akkoma 不需要启用。 用户注册 新举报 - “查看新帖文” 按钮 + “查看新嘟文” 按钮 服务器版本: %s 投票结果 展开 @@ -256,8 +256,8 @@ 正在上传附件 部分附件尚未上传完毕。 已过滤:%s - 折叠很长的帖文 - 回复时在 CW 前加上 “re:” + 折叠很长的嘟文 + 回复时在内容警告信息前加上 “re:” 旁观模式 隐藏互动按钮 对我的回复 @@ -287,7 +287,7 @@ Markdown BBCode MFM - 启用帖文格式 + 启用嘟文格式 允许在创建文章时设置类似Markdown的内容类型。注意,不是所有的实例都支持这个。 默认的内容类型 Bubble @@ -307,7 +307,7 @@ …并包含其中全部 请注意,服务器会处理这些操作。可能不支持合并这些操作。 尝试从原实例加载信息,以获取更准确的关注者、点赞和转发列表。 - 在回复中自动显示相同的 CWs + 在回复中自动显示相同的内容警告 \"转发报告 \"开关默认值 排除列表的成员不会显示在你的主页时间线上--如果你的实例支持的话。 显示高级选项 @@ -319,7 +319,7 @@ 禁用药丸状的活跃选项卡指示器 全黑模式 在时间线上显示性别代词 - 显示对帖文的表情回应,并允许你添加自己的表情回应。许多 Fediverse 服务器支持此功能,但 Mastodon 不支持。 + 显示对嘟文的表情回应,并允许你添加自己的表情回应。许多 Fediverse 服务器支持此功能,但 Mastodon 不支持。 在时间线中显示表情回应 是否在时间线中显示表情回应。如果此选项为关闭,则只有在查看对话时才会显示表情回应。 用表情回应 @@ -349,12 +349,12 @@ 医生 钻石 雨伞 - 包含标签的帖文… + 包含标签的嘟文… …但都不包含 …或包含其中任何一个 输入标签… 输入标签… - 仅显示本地帖文? + 仅显示本地嘟文? 标签不可为空 GIF 回复至任何人 @@ -370,29 +370,29 @@ 个人资料 在导航栏中显示选项卡标签 %1$s 回应了 %2$s - 这些都是实例管理员从网络中最新精选出来的帖文。 + 这些都是实例管理员从网络中最新精选出来的嘟文。 搜索联邦宇宙 %d 秒 - 仅当帖文被打开时 + 仅当嘟文被打开时 %d 时 隐藏空的表情回应 在时间线中显示表情回应 - 帖文 + 嘟文 自杀 - 加载较新的帖文 + 加载较新的嘟文 %d 天 始终显示添加按钮 查找求助热线 下次不再显示 - 这些帖文正在联邦宇宙上引起关注。 - 加载较旧的帖文 + 这些嘟文正在联邦宇宙上引起关注。 + 加载较旧的嘟文 以防你遇到困难… %d 分 如果你正在寻找一个不自杀的迹象,这就是。如果你遇到困难,请考虑拨打当地的自杀热线。 这些新闻故事正在联邦宇宙上被讨论。 - 帖文包含媒体 + 嘟文包含媒体 已屏蔽账号 已静音账号 使用心形作为收藏图标 diff --git a/mastodon/src/main/res/values-zh-rTW/strings_sk.xml b/mastodon/src/main/res/values-zh-rTW/strings_sk.xml index 89d16caf2..f95febe76 100644 --- a/mastodon/src/main/res/values-zh-rTW/strings_sk.xml +++ b/mastodon/src/main/res/values-zh-rTW/strings_sk.xml @@ -60,4 +60,41 @@ 黃色 %s 似乎不支援翻譯. %s 所在的列表 + 預設 (%s) + + 嘟文 + + 引用自 %s + example.social + 所有回覆 + 回覆能見度 + 列表 + 包含媒體的嘟文 + 全部刪除 + 複製嘟文連結 + 標記為草稿 + 公告 + 允許刪除通知 + 清除所有通知 + 「顯示新嘟文」按鈕 + 編輯時間軸 + 在新嘟文中引述 + 已封鎖的帳號 + 您的清單 + 捐款 + 首頁 + 手動審查新的跟隨者 + 排定時間發佈 + 聯邦時間軸 + 站臺 + 此伺服器 + 只顯示此伺服器的嘟文? + 所有通知使用一致的圖示 + 已靜音的帳號 + 只顯示一則通知 + 未送出的嘟文 + 附加檔案 + 在聯邦宇宙中搜尋 + 通知 + 使用 UnifiedPush \ No newline at end of file diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml index f66758e1a..8df5c2256 100644 --- a/mastodon/src/main/res/values/attrs.xml +++ b/mastodon/src/main/res/values/attrs.xml @@ -38,6 +38,7 @@ + diff --git a/mastodon/src/main/res/values/palettes.xml b/mastodon/src/main/res/values/palettes.xml index c04938157..855cf4bc6 100644 --- a/mastodon/src/main/res/values/palettes.xml +++ b/mastodon/src/main/res/values/palettes.xml @@ -77,6 +77,7 @@ @color/bookmark_selected #14000000 #22000000 + #FF5b8e63 #1F1F1F1F #B3261E @@ -162,6 +163,7 @@ #F9DEDC #000 #80000000 + #FF89bb9c