Merge remote-tracking branch 'megalodon_main/main'

# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/SetPrivateNote.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/text/DiffRemovedSpan.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java
#	mastodon/src/main/res/drawable/bg_note_edit.xml
#	mastodon/src/main/res/layout/fragment_profile.xml
#	metadata/uk/full_description.txt
This commit is contained in:
LucasGGamerM
2023-11-17 16:02:23 -03:00
78 changed files with 1289 additions and 713 deletions

View File

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

View File

@@ -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<String> badDomains = new ArrayList<>();

View File

@@ -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<AkkomaTranslation>{
public AkkomaTranslateStatus(String id, String lang){
super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toUpperCase(), AkkomaTranslation.class);
}
}

View File

@@ -48,6 +48,8 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public String quoteId;
public ContentType contentType;
public boolean preview;
public static class Poll{
public ArrayList<String> options=new ArrayList<>();
public int expiresIn;

View File

@@ -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 <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> 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;

View File

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

View File

@@ -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<T extends DisplayItemsParent> 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<T extends DisplayItemsParent> 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<T extends DisplayItemsParent> 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<T extends DisplayItemsParent> 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<SpoilerStatusDisplayItem.Holder> 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<T extends DisplayItemsParent> exten
status.translationState=Status.TranslationState.SHOWN;
}else{
status.translationState=Status.TranslationState.LOADING;
new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage())
.setCallback(new Callback<>(){
Consumer<Translation> 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<T extends DisplayItemsParent> exten
notifyItemChanged(itemID, TextStatusDisplayItem.class);
}
if(isInstanceAkkoma())
return;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null){
spoiler.rebind();

View File

@@ -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<Boolean> 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<Status> 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());

View File

@@ -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<TimelineDefinition> implements ScrollableToTop {
private String accountID;
private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper;
private Menu optionsMenu;
private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
private final List<ListTimeline> listTimelines = new ArrayList<>();
private final List<Hashtag> hashtags = new ArrayList<>();
private MenuItem addHashtagItem;
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
private String accountID;
private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper;
private Menu optionsMenu;
private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
private final List<ListTimeline> listTimelines=new ArrayList<>();
private final List<Hashtag> hashtags=new ArrayList<>();
private MenuItem addHashtagItem;
private final List<CustomLocalTimeline> 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<ListTimeline> result) {
listTimelines.addAll(result);
updateOptionsMenu();
}
new GetLists().setCallback(new Callback<>(){
@Override
public void onSuccess(List<ListTimeline> 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<Hashtag> result) {
hashtags.addAll(result);
updateOptionsMenu();
}
new GetFollowedHashtags().setCallback(new Callback<>(){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> 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<TimelineDefi
return true;
}
private void addTimeline(TimelineDefinition tl) {
data.add(tl.copy());
adapter.notifyItemInserted(data.size());
saveTimelines();
updateOptionsMenu();
}
private void addTimeline(TimelineDefinition tl){
data.add(tl.copy());
adapter.notifyItemInserted(data.size());
saveTimelines();
updateOptionsMenu();
}
private void addNewLocalTimeline() {
private void addNewLocalTimeline() {
FrameLayout inputWrap = new FrameLayout(getContext());
EditText input = new EditText(getContext());
input.setHint(R.string.sk_example_domain);
@@ -194,92 +194,92 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
timelineByMenuItem.put(item, tl);
}
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
item.setIcon(icon);
return item;
}
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
item.setIcon(icon);
return item;
}
private void updateOptionsMenu() {
if(getActivity()==null) return;
optionsMenu.clear();
timelineByMenuItem.clear();
private void updateOptionsMenu(){
if(getActivity()==null) return;
optionsMenu.clear();
timelineByMenuItem.clear();
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
SubMenu menu=optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
MenuItem addLocalTimelines = menu.add(0, R.id.menu_add_local_timelines, NONE, R.string.local_timeline);
MenuItem addLocalTimelines = menu.add(0, R.id.menu_add_local_timelines, NONE, R.string.local_timeline);
addLocalTimelines.setIcon(R.drawable.ic_fluent_add_24_regular);
makeBackItem(timelinesMenu);
makeBackItem(listsMenu);
makeBackItem(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));
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<TimelineViewHolder> getAdapter() {
return adapter = new TimelinesAdapter();
}
@Override
protected RecyclerView.Adapter<TimelineViewHolder> 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<String> tags) {
if (tags == null || tags.isEmpty()) return false;
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> 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){
//Ill Be Back
nacho.setChipTerminators(
Map.of(
@@ -289,223 +289,228 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
';', BEHAVIOR_CHIPIFY_ALL
)
);
nacho.enableEditChipOnTouch(true, true);
nacho.setOnFocusChangeListener((v, hasFocus) -> 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<TimelineDefinition> 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<TimelineDefinition> 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<TimelineViewHolder>{
@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<TimelineViewHolder>{
@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<TimelineDefinition> 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<TimelineDefinition> 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);
}
}
}

View File

@@ -229,7 +229,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
private Rect tmpRect=new Rect();

View File

@@ -21,9 +21,12 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.text.TextWatcher;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
@@ -149,7 +152,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private SwipeRefreshLayout refreshLayout;
private View followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress, notifyProgress;
private ProgressBar actionProgress, notifyProgress, noteSaveProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
@@ -160,9 +163,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
public FrameLayout noteWrap;
public EditText noteEdit;
private String note;
private Account account, remoteAccount;
private String accountID;
private String domain;
@@ -193,6 +193,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private ListImageLoaderWrapper imgLoader;
// profile note
private FrameLayout noteWrap;
private ImageButton noteSaveBtn;
private EditText noteEdit;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -262,9 +267,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEdit=content.findViewById(R.id.bio_edit);
nameEditWrap=content.findViewById(R.id.name_edit_wrap);
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
usernameWrap=content.findViewById(R.id.username_wrap);
actionProgress=content.findViewById(R.id.action_progress);
notifyProgress=content.findViewById(R.id.notify_progress);
noteSaveProgress=content.findViewById(R.id.note_save_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
countersLayout=content.findViewById(R.id.profile_counters);
@@ -279,60 +284,50 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
noteEdit = content.findViewById(R.id.note_edit);
noteWrap = content.findViewById(R.id.note_edit_wrap);
ImageButton noteEditConfirm = content.findViewById(R.id.note_edit_confirm);
noteEdit=content.findViewById(R.id.note_edit);
noteWrap=content.findViewById(R.id.note_edit_wrap);
noteSaveBtn=content.findViewById(R.id.note_save_btn);
noteEditConfirm.setOnClickListener((v -> {
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();

View File

@@ -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<StatusEditChangeType> 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<diff_match_patch.Diff> diffs = dmp.diff_main(original, modified);
diff_match_patch dmp=new diff_match_patch();
LinkedList<diff_match_patch.Diff> 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("<edit_diff_removed>");
case DELETE->{
stringBuilder.append("<edit-diff-delete>");
stringBuilder.append(diff.text);
stringBuilder.append("</edit_diff_removed>");
stringBuilder.append("</edit-diff-delete>");
}
case INSERT -> {
stringBuilder.append("<edit_diff_added>");
case INSERT->{
stringBuilder.append("<edit-diff-insert>");
stringBuilder.append(diff.text);
stringBuilder.append("</edit_diff_added>");
stringBuilder.append("</edit-diff-insert>");
}
default -> stringBuilder.append(diff.text);
default->stringBuilder.append(diff.text);
}
}
return stringBuilder.toString();

View File

@@ -89,8 +89,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
@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>
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);
}
}
}

View File

@@ -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<String, NeighborAncestryInfo> 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();
}

View File

@@ -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<StatusDisplayItem> 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<StatusDisplayItem> 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

View File

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

View File

@@ -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<Void> implements HasAccountID{
private ListItem<Void> mediaCacheItem;
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
private static final String TAG="SettingsAboutAppFragment";
private ListItem<Void> mediaCacheItem, copyCrashLogItem;
private CheckableListItem<Void> enablePreReleasesItem;
private AccountSession session;
private boolean timelineCacheCleared=false;
private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log");
// MOSHIDON
private ListItem<Void> clearRecentEmojisItem;
@@ -38,22 +60,35 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> 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<ListItem<Void>> 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<Void> 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);
}
}
}

View File

@@ -333,13 +333,15 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
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<Void>{
(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();

View File

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

View File

@@ -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

View File

@@ -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<String, List<String>> 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;

View File

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

View File

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

View File

@@ -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<Holder> isChecked){
this.isChecked=isChecked;
}

View File

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

View File

@@ -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<? extends StatusRelatedAccountListFragment> 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);

View File

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

View File

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

View File

@@ -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){

View File

@@ -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){

View File

@@ -61,7 +61,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final List<Attachment> attachments;
private final Map<String, Pair<String, String>> translatedAttachments = new HashMap<>();
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public String sensitiveTitle;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){

View File

@@ -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<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);

View File

@@ -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<StatusDisplayItem> contentItems=new ArrayList<>();
private final CharSequence parsedTitle;
private CharSequence translatedTitle;

View File

@@ -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("<span class=\"quote-inline\">");
if (!hasQuoteInlineTag) {
String quoteUrl = statusForContent.quote.url;
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
if(statusForContent.quote!=null) {
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>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<StatusDisplayItem> items, Status status){
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> 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<T extends StatusDisplayItem> extends BindableViewHolder<T> 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;
}
}
}

View File

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

View File

@@ -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<StatusDisplayItem> filteredItems;
public LegacyFilter applyingFilter;

View File

@@ -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() {

View File

@@ -70,6 +70,10 @@ public class HtmlParser{
private HtmlParser(){}
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
return parse(source, emojis, mentions, tags, accountID, null);
}
/**
* Parse HTML and custom emoji into a spanned string for display.
* Supported tags: <ul>
@@ -82,7 +86,7 @@ public class HtmlParser{
* @param emojis Custom emojis that are present in source as <code>:code:</code>
* @return a spanned string
*/
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Context context){
class SpanInfo{
public Object span;
public int start;
@@ -107,6 +111,9 @@ public class HtmlParser{
Map<String, Hashtag> 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<SpanInfo> 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));
}
}
}

View File

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

View File

@@ -217,6 +217,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> 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()));

View File

@@ -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<String, List<String>> decode(String content, Pattern regex) {
Matcher m=regex.matcher(content);
StringBuilder decodedString=new StringBuilder();
List<String> 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);
}
}