diff --git a/mastodon/build.gradle b/mastodon/build.gradle index fb9976554..d610d8216 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -9,8 +9,8 @@ android { applicationId "org.joinmastodon.android.sk" minSdk 23 targetSdk 31 - versionCode 20 - versionName '1.1.3+fork.20' + versionCode 21 + versionName '1.1.3+fork.21' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -47,6 +47,10 @@ android { setRoot "src/appcenter" } } + lintOptions{ + checkReleaseBuilds false + abortOnError false + } } dependencies { diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java index 60e1ce439..d1e8553ff 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java @@ -27,6 +27,7 @@ public class GetAccountStatuses extends MastodonAPIRequest>{ addQueryParameter("exclude_replies", "true"); addQueryParameter("exclude_reblogs", "true"); } + case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true"); } } @@ -35,6 +36,7 @@ public class GetAccountStatuses extends MastodonAPIRequest>{ INCLUDE_REPLIES, PINNED, MEDIA, - NO_REBLOGS + NO_REBLOGS, + OWN_POSTS_AND_REPLIES } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/EditStatus.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/EditStatus.java new file mode 100644 index 000000000..ecadaf2eb --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/EditStatus.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +public class EditStatus extends MastodonAPIRequest{ + public EditStatus(CreateStatus.Request req, String id){ + super(HttpMethod.PUT, "/statuses/"+id, Status.class); + setRequestBody(req); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusEditHistory.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusEditHistory.java new file mode 100644 index 000000000..e682cd02a --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusEditHistory.java @@ -0,0 +1,33 @@ +package org.joinmastodon.android.api.requests.statuses; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.StatusPrivacy; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import okhttp3.Response; + +public class GetStatusEditHistory extends MastodonAPIRequest>{ + public GetStatusEditHistory(String id){ + super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){}); + } + + @Override + public void validateAndPostprocessResponse(List respObj, Response httpResponse) throws IOException{ + int i=0; + for(Status s:respObj){ + s.uri=""; + s.id="fakeID"+i; + s.visibility=StatusPrivacy.PUBLIC; + s.mentions=Collections.emptyList(); + s.tags=Collections.emptyList(); + i++; + } + super.validateAndPostprocessResponse(respObj, httpResponse); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusSourceText.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusSourceText.java new file mode 100644 index 000000000..f1dd895e3 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusSourceText.java @@ -0,0 +1,18 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.AllFieldsAreRequired; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.BaseModel; + +public class GetStatusSourceText extends MastodonAPIRequest{ + public GetStatusSourceText(String id){ + super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class); + } + + @AllFieldsAreRequired + public static class Response extends BaseModel{ + public String id; + public String text; + public String spoilerText; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/StatusUpdatedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/StatusUpdatedEvent.java new file mode 100644 index 000000000..f906d5095 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/events/StatusUpdatedEvent.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.events; + +import org.joinmastodon.android.model.Status; + +public class StatusUpdatedEvent{ + public Status status; + + public StatusUpdatedEvent(Status status){ + this.status=status; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 7cc820f98..575481a05 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -457,7 +457,7 @@ public abstract class BaseStatusListFragment exten status.spoilerRevealed=true; TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); if(text!=null) - adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset()); + adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset()); HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class); if(header!=null) header.rebind(); @@ -579,6 +579,10 @@ public abstract class BaseStatusListFragment exten return true; } + public ArrayList getDisplayItems(){ + return displayItems; + } + @Override public void onApplyWindowInsets(WindowInsets insets){ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index d08abf9f5..bad446c9a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -54,17 +54,20 @@ import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.ProgressListener; import org.joinmastodon.android.api.requests.statuses.CreateStatus; +import org.joinmastodon.android.api.requests.statuses.EditStatus; import org.joinmastodon.android.api.requests.statuses.UploadAttachment; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent; +import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Mention; +import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.ui.ComposeAutocompleteViewController; @@ -175,6 +178,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private Instance instance; private boolean attachmentsErrorShowing; + private Status editingStatus; + private boolean pollChanged; + private boolean creatingView; + public static DraftMediaAttachment redraftAttachment(Attachment att) { DraftMediaAttachment draft=new DraftMediaAttachment(); draft.serverAttachment=att; @@ -194,6 +201,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr instanceDomain=session.domain; customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain); instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain); + if(getArguments().containsKey("editStatus")){ + editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus")); + } if(instance==null){ Nav.finish(this); return; @@ -239,6 +249,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ + creatingView=true; emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain); emojiKeyboard.setListener(this::onCustomEmojiClick); @@ -317,6 +328,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } updatePollOptionHints(); pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr)); + }else if(savedInstanceState==null && editingStatus!=null && editingStatus.poll!=null){ + pollBtn.setSelected(true); + mediaBtn.setEnabled(false); + pollWrap.setVisibility(View.VISIBLE); + for(Poll.Option eopt:editingStatus.poll.options){ + DraftPollOption opt=createDraftPollOption(); + opt.edit.setText(eopt.title); + } + updatePollOptionHints(); + pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr)); }else{ pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=getResources().getQuantityString(R.plurals.x_days, 1, 1))); } @@ -329,6 +350,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){ spoilerEdit.setVisibility(View.VISIBLE); spoilerBtn.setSelected(true); + }else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){ + spoilerEdit.setVisibility(View.VISIBLE); + spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText)); + spoilerBtn.setSelected(true); } ArrayList serializedAttachments=(savedInstanceState!=null ? savedInstanceState : getArguments()) @@ -354,6 +379,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr autocompleteView.setVisibility(View.GONE); mainEditTextWrap.addView(autocompleteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(178), Gravity.TOP)); + creatingView=false; + return view; } @@ -455,9 +482,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void afterTextChanged(Editable s){ - updateCharCounter(s); + updateCharCounter(); } }); + spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter())); if(replyTo!=null){ replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName)); ArrayList mentions=new ArrayList<>(); @@ -475,45 +503,58 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(savedInstanceState==null){ mainEditText.setText(initialText); mainEditText.setSelection(mainEditText.length()); - // TODO: setting for preserving cw always / only when replying to own posts - // && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account) if(!TextUtils.isEmpty(replyTo.spoilerText)){ - insertSpoiler(replyTo.spoilerText); hasSpoiler=true; + spoilerEdit.setVisibility(View.VISIBLE); + spoilerEdit.setText(replyTo.spoilerText); + spoilerBtn.setSelected(true); } } }else{ replyText.setVisibility(View.GONE); } if(savedInstanceState==null){ - String prefilledText=getArguments().getString("prefilledText"); - if(!TextUtils.isEmpty(prefilledText)){ - mainEditText.setText(prefilledText); + if(editingStatus!=null){ + initialText=getArguments().getString("sourceText", ""); + mainEditText.setText(initialText); mainEditText.setSelection(mainEditText.length()); - initialText=prefilledText; - } - String spoilerText=getArguments().getString("spoilerText"); - if(!TextUtils.isEmpty(spoilerText)) insertSpoiler(spoilerText); - ArrayList mediaUris=getArguments().getParcelableArrayList("mediaAttachments"); - if(mediaUris!=null && !mediaUris.isEmpty()){ - for(Uri uri:mediaUris){ - addMediaAttachment(uri, null); + if(!editingStatus.mediaAttachments.isEmpty()){ + attachmentsView.setVisibility(View.VISIBLE); + for(Attachment att:editingStatus.mediaAttachments){ + DraftMediaAttachment da=new DraftMediaAttachment(); + da.serverAttachment=att; + da.description=att.description; + da.uri=Uri.parse(att.previewUrl); + attachmentsView.addView(createMediaAttachmentView(da)); + attachments.add(da); + } + pollBtn.setEnabled(false); + } + }else{ + String prefilledText=getArguments().getString("prefilledText"); + if(!TextUtils.isEmpty(prefilledText)){ + mainEditText.setText(prefilledText); + mainEditText.setSelection(mainEditText.length()); + initialText=prefilledText; + } + ArrayList mediaUris=getArguments().getParcelableArrayList("mediaAttachments"); + if(mediaUris!=null && !mediaUris.isEmpty()){ + for(Uri uri:mediaUris){ + addMediaAttachment(uri, null); + } } } } - } - private void insertSpoiler(String text) { - hasSpoiler=true; - if (text!=null) spoilerEdit.setText(text); - spoilerEdit.setVisibility(View.VISIBLE); - spoilerBtn.setSelected(true); + if(editingStatus!=null){ + updateCharCounter(); + } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ publishButton=new Button(getActivity()); - publishButton.setText(R.string.publish); + publishButton.setText(editingStatus==null ? R.string.publish : R.string.save); publishButton.setOnClickListener(this::onPublishClick); LinearLayout wrap=new LinearLayout(getActivity()); wrap.setOrientation(LinearLayout.HORIZONTAL); @@ -536,7 +577,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8)); wrap.setClipToPadding(false); - MenuItem item=menu.add(R.string.publish); + MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save); item.setActionView(wrap); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); updatePublishButtonState(); @@ -554,7 +595,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } @SuppressLint("NewApi") - private void updateCharCounter(CharSequence text){ + private void updateCharCounter(){ + CharSequence text=mainEditText.getText(); + String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher( MENTION_PATTERN.matcher( URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx") @@ -566,6 +609,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr charCount++; } + if(hasSpoiler){ + charCount+=spoilerEdit.length(); + } charCounter.setText(String.valueOf(charLimit-charCount)); trimmedCharCount=text.toString().trim().length(); updatePublishButtonState(); @@ -578,11 +624,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(opt.edit.length()>0) nonEmptyPollOptionsCount++; } - if(publishButton!=null){ - publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit - && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty() - && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1)); - } + publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty() + && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1)); } private void onCustomEmojiClick(Emoji emoji){ @@ -638,34 +681,54 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr sendProgress.setVisibility(View.VISIBLE); sendError.setVisibility(View.GONE); - new CreateStatus(req, uuid) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(Status result){ - wm.removeView(sendingOverlay); - sendingOverlay=null; - E.post(new StatusCreatedEvent(result)); - if(replyTo!=null){ - replyTo.repliesCount++; - E.post(new StatusCountersUpdatedEvent(replyTo)); - } - Nav.finish(ComposeFragment.this); + Callback resCallback=new Callback<>(){ + @Override + public void onSuccess(Status result){ + wm.removeView(sendingOverlay); + sendingOverlay=null; + if(editingStatus==null){ + E.post(new StatusCreatedEvent(result)); + if(replyTo!=null){ + replyTo.repliesCount++; + E.post(new StatusCountersUpdatedEvent(replyTo)); } + }else{ + E.post(new StatusUpdatedEvent(result)); + } + Nav.finish(ComposeFragment.this); + } - @Override - public void onError(ErrorResponse error){ - wm.removeView(sendingOverlay); - sendingOverlay=null; - sendProgress.setVisibility(View.GONE); - sendError.setVisibility(View.VISIBLE); - publishButton.setEnabled(true); - error.showToast(getActivity()); - } - }) - .exec(accountID); + @Override + public void onError(ErrorResponse error){ + wm.removeView(sendingOverlay); + sendingOverlay=null; + sendProgress.setVisibility(View.GONE); + sendError.setVisibility(View.VISIBLE); + publishButton.setEnabled(true); + error.showToast(getActivity()); + } + }; + + if(editingStatus!=null){ + new EditStatus(req, editingStatus.id) + .setCallback(resCallback) + .exec(accountID); + }else{ + new CreateStatus(req, uuid) + .setCallback(resCallback) + .exec(accountID); + } } private boolean hasDraft(){ + if(editingStatus!=null){ + if(!mainEditText.getText().toString().equals(initialText)) + return true; + List existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList()); + if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList()))) + return true; + return pollChanged; + } boolean pollFieldsHaveContent=false; for(DraftPollOption opt:pollOptions) pollFieldsHaveContent|=opt.edit.length()>0; @@ -716,7 +779,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void confirmDiscardDraftAndFinish(){ new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.discard_draft) + .setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes) .setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this)) .setNegativeButton(R.string.cancel, null) .show(); @@ -996,7 +1059,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr pollOptionsView.startDragging(option.view); return true; }); - option.edit.addTextChangedListener(new SimpleTextWatcher(e->updatePublishButtonState())); + option.edit.addTextChangedListener(new SimpleTextWatcher(e->{ + if(!creatingView) + pollChanged=true; + updatePublishButtonState(); + })); option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)}); pollOptionsView.addView(option.view); @@ -1016,6 +1083,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void onSwapPollOptions(int oldIndex, int newIndex){ pollOptions.add(newIndex, pollOptions.remove(oldIndex)); updatePollOptionHints(); + pollChanged=true; } private void showPollDurationMenu(){ @@ -1039,6 +1107,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr default -> throw new IllegalStateException("Unexpected value: "+item.getItemId()); }; pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString())); + pollChanged=true; return true; }); menu.show(); @@ -1055,6 +1124,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr spoilerEdit.setText(""); spoilerBtn.setSelected(false); mainEditText.requestFocus(); + updateCharCounter(); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index 105ececae..073fd66ac 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -1,10 +1,6 @@ package org.joinmastodon.android.fragments; import android.app.Activity; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; import android.os.Bundle; import android.view.View; @@ -16,15 +12,12 @@ import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; -import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; -import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.parceler.Parcels; import java.util.ArrayList; @@ -33,7 +26,6 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; @@ -160,91 +152,7 @@ public class NotificationsListFragment extends BaseStatusListFragment sdi) && sdi.getItem().inset; - if(inset){ - if(rect.isEmpty()){ - rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight()); - }else{ - rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight()); - } - }else if(!rect.isEmpty()){ - drawInsetBackground(c); - rect.setEmpty(); - } - } - if(!rect.isEmpty()){ - if(pos sdi){ - boolean inset=sdi.getItem().inset; - int pos=holder.getAbsoluteAdapterPosition(); - if(inset){ - boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; - boolean bottomSiblingInset=pos img){ - PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile; - // only inset those items that are on the edges of the layout - insetLeft=tile.startCol==0; - insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length; - // inset all items in the bottom row - if(tile.startRow+tile.rowSpan==layout.rowSizes.length) - bottomSiblingInset=false; - } - if(insetLeft) - outRect.left=pad; - if(insetRight) - outRect.right=pad; - if(!topSiblingInset) - outRect.top=pad; - if(!bottomSiblingInset) - outRect.bottom=pad; - } - } - } - }); + list.addItemDecoration(new InsetStatusItemDecoration(this)); } private Notification getNotificationByID(String id){ @@ -268,4 +176,5 @@ public class NotificationsListFragment extends BaseStatusListFragment(this){ + @Override + public void onSuccess(List result){ + Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed()); + onDataLoaded(result, false); + } + }) + .exec(accountID); + } + + @Override + protected List buildDisplayItems(Status s){ + List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false); + int idx=data.indexOf(s); + if(idx>=0){ + String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); + String action=""; + if(idx==data.size()-1){ + action=getString(R.string.edit_original_post); + }else{ + enum StatusEditChangeType{ + TEXT_CHANGED, + SPOILER_ADDED, + SPOILER_REMOVED, + SPOILER_CHANGED, + POLL_ADDED, + POLL_REMOVED, + POLL_CHANGED, + MEDIA_ADDED, + MEDIA_REMOVED, + MEDIA_REORDERED, + MARKED_SENSITIVE, + MARKED_NOT_SENSITIVE + } + EnumSet changes=EnumSet.noneOf(StatusEditChangeType.class); + Status prev=data.get(idx+1); + + if(!Objects.equals(s.content, prev.content)){ + changes.add(StatusEditChangeType.TEXT_CHANGED); + } + if(!Objects.equals(s.spoilerText, prev.spoilerText)){ + if(s.spoilerText==null){ + changes.add(StatusEditChangeType.SPOILER_REMOVED); + }else if(prev.spoilerText==null){ + changes.add(StatusEditChangeType.SPOILER_ADDED); + }else{ + changes.add(StatusEditChangeType.SPOILER_CHANGED); + } + } + if(s.poll!=null || prev.poll!=null){ + if(s.poll==null){ + changes.add(StatusEditChangeType.POLL_REMOVED); + }else if(prev.poll==null){ + changes.add(StatusEditChangeType.POLL_ADDED); + }else if(!s.poll.id.equals(prev.poll.id)){ + changes.add(StatusEditChangeType.POLL_CHANGED); + } + } + List newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList()); + List prevAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList()); + boolean addedOrRemoved=false; + if(!newAttachmentIDs.containsAll(prevAttachmentIDs)){ + changes.add(StatusEditChangeType.MEDIA_REMOVED); + addedOrRemoved=true; + } + if(!prevAttachmentIDs.containsAll(newAttachmentIDs)){ + changes.add(StatusEditChangeType.MEDIA_ADDED); + addedOrRemoved=true; + } + if(!addedOrRemoved && !newAttachmentIDs.equals(prevAttachmentIDs)){ + changes.add(StatusEditChangeType.MEDIA_REORDERED); + } + if(s.sensitive && !prev.sensitive){ + changes.add(StatusEditChangeType.MARKED_SENSITIVE); + }else if(prev.sensitive && !s.sensitive){ + changes.add(StatusEditChangeType.MARKED_NOT_SENSITIVE); + } + + if(changes.size()==1){ + action=getString(switch(changes.iterator().next()){ + case TEXT_CHANGED -> R.string.edit_text_edited; + case SPOILER_ADDED -> R.string.edit_spoiler_added; + case SPOILER_REMOVED -> R.string.edit_spoiler_removed; + case SPOILER_CHANGED -> R.string.edit_spoiler_edited; + case POLL_ADDED -> R.string.edit_poll_added; + case POLL_REMOVED -> R.string.edit_poll_removed; + case POLL_CHANGED -> R.string.edit_poll_edited; + case MEDIA_ADDED -> R.string.edit_media_added; + case MEDIA_REMOVED -> R.string.edit_media_removed; + case MEDIA_REORDERED -> R.string.edit_media_reordered; + case MARKED_SENSITIVE -> R.string.edit_marked_sensitive; + case MARKED_NOT_SENSITIVE -> R.string.edit_marked_not_sensitive; + }); + }else{ + action=getString(R.string.edit_multiple_changed); + } + } + items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" ยท "+date, Collections.emptyList(), 0)); + } + 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; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java index 47eb2b364..45edfec2b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -9,6 +9,7 @@ import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusDeletedEvent; +import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; @@ -16,6 +17,7 @@ import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.parceler.Parcels; +import java.util.ArrayList; import java.util.List; import androidx.recyclerview.widget.RecyclerView; @@ -61,8 +63,6 @@ public abstract class StatusListFragment extends BaseStatusListFragment{ protected void onStatusCreated(StatusCreatedEvent ev){} - protected void onStatusUnpinned(StatusUnpinnedEvent ev){} - protected Status getContentStatusByID(String id){ Status s=getStatusByID(id); return s==null ? null : s.getContentStatus(); @@ -138,11 +138,6 @@ public abstract class StatusListFragment extends BaseStatusListFragment{ StatusListFragment.this.onStatusCreated(ev); } - @Subscribe - public void onStatusUnpinned(StatusUnpinnedEvent ev){ - StatusListFragment.this.onStatusUnpinned(ev); - } - @Subscribe public void onPollUpdated(PollUpdatedEvent ev){ if(!ev.accountID.equals(accountID)) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java index ec49a3ab9..22d0eda16 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java @@ -43,7 +43,7 @@ public class ThreadFragment extends StatusListFragment{ @Override protected List buildDisplayItems(Status s){ List items=super.buildDisplayItems(s); - if(s==mainStatus){ + if(s.id.equals(mainStatus.id)){ for(StatusDisplayItem item:items){ if(item instanceof TextStatusDisplayItem text) text.textSelectable=true; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java index 61377f2c5..9a37ce8c1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java @@ -85,7 +85,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ @Override protected void doLoadData(int offset, int count){ - currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.NO_REBLOGS) + currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(List result){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java index f2f0542b1..bfe38e67a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -37,6 +37,7 @@ public class Status extends BaseModel implements DisplayItemsParent{ public int reblogsCount; public int favouritesCount; public int repliesCount; + public Instant editedAt; public String url; public String inReplyToId; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java index 895a0119e..c21ee992f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java @@ -1,5 +1,6 @@ package org.joinmastodon.android.ui.displayitems; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.text.SpannableStringBuilder; @@ -12,6 +13,7 @@ import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.StatusEditHistoryFragment; import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment; import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment; import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment; @@ -43,39 +45,35 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ } public static class Holder extends StatusDisplayItem.Holder{ - private final TextView reblogs, favorites, time; - private final View buttonsView; + private final TextView time, favoritesCount, reblogsCount, lastEditTime; + private final View favorites, reblogs, editHistory; public Holder(Context context, ViewGroup parent){ super(context, R.layout.display_item_extended_footer, parent); reblogs=findViewById(R.id.reblogs); favorites=findViewById(R.id.favorites); + editHistory=findViewById(R.id.edit_history); time=findViewById(R.id.timestamp); - buttonsView=findViewById(R.id.button_bar); + favoritesCount=findViewById(R.id.favorites_count); + reblogsCount=findViewById(R.id.reblogs_count); + lastEditTime=findViewById(R.id.last_edited); reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class)); favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class)); + editHistory.setOnClickListener(v->startEditHistoryFragment()); } + @SuppressLint("DefaultLocale") @Override public void onBind(ExtendedFooterStatusDisplayItem item){ Status s=item.status; - if(s.favouritesCount>0){ - favorites.setVisibility(View.VISIBLE); - favorites.setText(getFormattedPlural(R.plurals.x_favorites, s.favouritesCount)); + favoritesCount.setText(String.format("%,d", s.favouritesCount)); + reblogsCount.setText(String.format("%,d", s.reblogsCount)); + if(s.editedAt!=null){ + editHistory.setVisibility(View.VISIBLE); + lastEditTime.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt))); }else{ - favorites.setVisibility(View.GONE); - } - if(s.reblogsCount>0){ - reblogs.setVisibility(View.VISIBLE); - reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, s.reblogsCount)); - }else{ - reblogs.setVisibility(View.GONE); - } - if(s.favouritesCount==0 && s.reblogsCount==0){ - buttonsView.setVisibility(View.GONE); - }else{ - buttonsView.setVisibility(View.VISIBLE); + editHistory.setVisibility(View.GONE); } String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault())); if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){ @@ -108,5 +106,12 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ args.putParcelable("status", Parcels.wrap(item.status)); Nav.go(item.parentFragment.getActivity(), cls, args); } + + private void startEditHistoryFragment(){ + Bundle args=new Bundle(); + args.putString("account", item.parentFragment.getAccountID()); + args.putString("id", item.status.id); + Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 3e5a87a30..0939acc0e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -21,8 +21,10 @@ import android.widget.Toast; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; +import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; import org.joinmastodon.android.model.Account; @@ -135,7 +137,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ optionsMenu.setOnMenuItemClickListener(menuItem->{ Account account=item.user; int id=menuItem.getItemId(); - if(id==R.id.delete){ + if(id==R.id.edit){ + final Bundle args=new Bundle(); + args.putString("account", item.parentFragment.getAccountID()); + args.putParcelable("editStatus", Parcels.wrap(item.status)); + if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){ + Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); + }else{ + new GetStatusSourceText(item.status.id) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(GetStatusSourceText.Response result){ + args.putString("sourceText", result.text); + args.putString("sourceSpoiler", result.spoilerText); + Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(item.parentFragment.getActivity()); + } + }) + .wrapProgress(item.parentFragment.getActivity(), R.string.loading, true) + .exec(item.parentFragment.getAccountID()); + } + }else if(id==R.id.delete){ UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{}); }else if(id==R.id.delete_and_redraft) { UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{}); @@ -179,7 +205,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ public void onBind(HeaderStatusDisplayItem item){ name.setText(item.parsedName); username.setText('@'+item.user.acct); - timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt)); + if(item.status==null || item.status.editedAt==null) + timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt)); + else + timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt))); visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE); if(item.hasVisibilityToggle){ visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility); @@ -253,6 +282,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ Account account=item.user; Menu menu=optionsMenu.getMenu(); boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account); + menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 8feaa1dc8..d1e5e1fd8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -8,6 +8,7 @@ import android.view.ViewGroup; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.DisplayItemsParent; @@ -114,7 +115,7 @@ public abstract class StatusDisplayItem{ } if(addFooter){ items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID)); - if(status.hasGapAfter) + if(status.hasGapAfter && !(fragment instanceof ThreadFragment)) items.add(new GapStatusDisplayItem(parentID, fragment)); } int i=1; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java new file mode 100644 index 000000000..a811fb464 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java @@ -0,0 +1,116 @@ +package org.joinmastodon.android.ui.utils; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.View; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.NotificationsListFragment; +import org.joinmastodon.android.ui.PhotoLayoutHelper; +import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.utils.V; + +public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ + private final BaseStatusListFragment listFragment; + private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); + private int bgColor; + private int borderColor; + private RectF rect=new RectF(); + + public InsetStatusItemDecoration(BaseStatusListFragment listFragment){ + this.listFragment=listFragment; + bgColor=UiUtils.getThemeColor(listFragment.getActivity(), android.R.attr.colorBackground); + borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorPollVoted); + } + + @Override + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + List displayItems=listFragment.getDisplayItems(); + int pos=0; + for(int i=0; i sdi) && sdi.getItem().inset; + if(inset){ + if(rect.isEmpty()){ + rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight()); + }else{ + rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight()); + } + }else if(!rect.isEmpty()){ + drawInsetBackground(parent, c); + rect.setEmpty(); + } + } + if(!rect.isEmpty()){ + if(pos displayItems=listFragment.getDisplayItems(); + RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); + if(holder instanceof StatusDisplayItem.Holder sdi){ + boolean inset=sdi.getItem().inset; + int pos=holder.getAbsoluteAdapterPosition(); + if(inset){ + boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; + boolean bottomSiblingInset=pos img){ + PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout; + PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile; + // only inset those items that are on the edges of the layout + insetLeft=tile.startCol==0; + insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length; + // inset all items in the bottom row + if(tile.startRow+tile.rowSpan==layout.rowSizes.length) + bottomSiblingInset=false; + } + if(insetLeft) + outRect.left=pad; + if(insetRight) + outRect.right=pad; + if(!topSiblingInset) + outRect.top=pad; + if(!bottomSiblingInset) + outRect.bottom=pad; + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index a356c1912..dd78df513 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -69,6 +69,7 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; @@ -98,6 +99,7 @@ import okhttp3.MediaType; public class UiUtils{ private static Handler mainHandler=new Handler(Looper.getMainLooper()); private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM"); + public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); private UiUtils(){} @@ -142,6 +144,23 @@ public class UiUtils{ } } + public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){ + long t=instant.toEpochMilli(); + long now=System.currentTimeMillis(); + long diff=now-t; + if(diff<1000L){ + return context.getString(R.string.time_just_now); + }else if(diff<60_000L){ + int secs=(int)(diff/1000L); + return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs); + }else if(diff<3600_000L){ + int mins=(int)(diff/60_000L); + return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins); + }else{ + return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault())); + } + } + public static String formatTimeLeft(Context context, Instant instant){ long t=instant.toEpochMilli(); long now=System.currentTimeMillis(); diff --git a/mastodon/src/main/res/drawable/ic_fluent_edit_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_edit_24_regular.xml new file mode 100644 index 000000000..8e828fbfa --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_edit_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/display_item_extended_footer.xml b/mastodon/src/main/res/layout/display_item_extended_footer.xml index 9f292a5e4..0ffb41f21 100644 --- a/mastodon/src/main/res/layout/display_item_extended_footer.xml +++ b/mastodon/src/main/res/layout/display_item_extended_footer.xml @@ -2,49 +2,146 @@ + android:layout_height="wrap_content"> - + android:layout_height="64dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:background="?android:selectableItemBackground"> -