Compare commits

..

7 Commits

Author SHA1 Message Date
sk
8aeda56fc8 merge upstream changes 2022-09-08 14:56:43 +02:00
Grishka
f531a90b41 Better editing 2022-09-02 11:21:28 +03:00
Grishka
ff52c37868 Editing 2022-09-02 05:47:20 +03:00
Grishka
8fb2b454dd Post edit history + extended footer redesign 2022-09-02 02:00:25 +03:00
Grishka
265b2ad32c Fix #218 2022-08-29 21:21:08 +03:00
Grishka
ba3219d9fc Fix #209, fix #198 2022-08-29 00:57:09 +03:00
Grishka
b44e3424e3 Fix #249 2022-08-29 00:12:15 +03:00
23 changed files with 728 additions and 211 deletions

View File

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

View File

@@ -27,6 +27,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
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<List<Status>>{
INCLUDE_REPLIES,
PINNED,
MEDIA,
NO_REBLOGS
NO_REBLOGS,
OWN_POSTS_AND_REPLIES
}
}

View File

@@ -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<Status>{
public EditStatus(CreateStatus.Request req, String id){
super(HttpMethod.PUT, "/statuses/"+id, Status.class);
setRequestBody(req);
}
}

View File

@@ -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<List<Status>>{
public GetStatusEditHistory(String id){
super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){});
}
@Override
public void validateAndPostprocessResponse(List<Status> 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);
}
}

View File

@@ -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<GetStatusSourceText.Response>{
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;
}
}

View File

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

View File

@@ -457,7 +457,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> 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<T extends DisplayItemsParent> exten
return true;
}
public ArrayList<StatusDisplayItem> getDisplayItems(){
return displayItems;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){

View File

@@ -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<Parcelable> 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<String> 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<Uri> 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<Uri> 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<Status> 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<String> 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();
}
}

View File

@@ -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<Notificati
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private int bgColor=UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground);
private int borderColor=UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted);
private RectF rect=new RectF();
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
int pos=0;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
pos=holder.getAbsoluteAdapterPosition();
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> 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<displayItems.size()-1 && displayItems.get(pos+1).inset){
rect.bottom=parent.getHeight()+V.dp(10);
}
drawInsetBackground(c);
rect.setEmpty();
}
}
private void drawInsetBackground(Canvas c){
paint.setStyle(Paint.Style.FILL);
paint.setColor(bgColor);
rect.left=V.dp(12);
rect.right=list.getWidth()-V.dp(12);
rect.inset(V.dp(4), V.dp(4));
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
paint.setColor(borderColor);
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
c.drawRoundRect(rect, V.dp(4), V.dp(4), 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<?> sdi){
boolean inset=sdi.getItem().inset;
int pos=holder.getAbsoluteAdapterPosition();
if(inset){
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
int pad;
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
pad=V.dp(16);
else
pad=V.dp(12);
boolean insetLeft=true, insetRight=true;
if(holder instanceof ImageStatusDisplayItem.Holder<?> 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<Notificati
}
}
}
}

View File

@@ -0,0 +1,157 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEditHistoryFragment extends StatusListFragment{
private String id;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id=getArguments().getString("id");
loadData();
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.edit_history);
}
@Override
protected void doLoadData(int offset, int count){
new GetStatusEditHistory(id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
onDataLoaded(result, false);
}
})
.exec(accountID);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> 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<StatusEditChangeType> 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<String> newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
List<String> 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;
}
}

View File

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

View File

@@ -43,7 +43,7 @@ public class ThreadFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
int pos=0;
for(int i=0; i<parent.getChildCount(); i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
pos=holder.getAbsoluteAdapterPosition();
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> 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.size()-1 && displayItems.get(pos+1).inset){
rect.bottom=parent.getHeight()+V.dp(10);
}
drawInsetBackground(parent, c);
rect.setEmpty();
}
}
private void drawInsetBackground(RecyclerView list, Canvas c){
paint.setStyle(Paint.Style.FILL);
paint.setColor(bgColor);
rect.left=V.dp(12);
rect.right=list.getWidth()-V.dp(12);
rect.inset(V.dp(4), V.dp(4));
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
paint.setColor(borderColor);
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
List<StatusDisplayItem> 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<displayItems.size()-1 && displayItems.get(pos+1).inset;
int pad;
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
pad=V.dp(16);
else
pad=V.dp(12);
boolean insetLeft=true, insetRight=true;
if(holder instanceof ImageStatusDisplayItem.Holder<?> 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;
}
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M21.03 2.97c1.398 1.397 1.398 3.663 0 5.06L9.062 20c-0.277 0.277-0.621 0.477-0.999 0.58l-5.116 1.395c-0.56 0.153-1.073-0.361-0.92-0.921l1.395-5.116c0.103-0.377 0.302-0.722 0.58-0.999L15.97 2.97c1.397-1.398 3.663-1.398 5.06 0zM15 6.06L5.062 16c-0.092 0.092-0.159 0.207-0.193 0.333l-1.05 3.85 3.85-1.05C7.793 19.096 7.908 19.03 8 18.938L17.94 9 15 6.06zm2.03-2.03L16.06 5 19 7.94l0.97-0.97c0.811-0.812 0.811-2.128 0-2.94-0.812-0.811-2.128-0.811-2.94 0z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -2,49 +2,146 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLightest">
android:layout_height="wrap_content">
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
android:id="@+id/button_bar"
<RelativeLayout
android:id="@+id/reblogs"
android:layout_width="match_parent"
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">
<Button
android:id="@+id/reblogs"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_fluent_arrow_repeat_all_24_regular"
android:tint="?android:textColorSecondary"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:textSize="14sp"
android:minHeight="36dp"
android:textColor="?android:textColorSecondary"
android:background="@drawable/bg_text_button"
android:fontFamily="sans-serif"
tools:text="4 reblogs"/>
android:layout_marginStart="32dp"
android:layout_toEndOf="@id/icon"
android:minHeight="22dp"
android:singleLine="true"
android:text="@string/post_info_reblogs"
android:textAppearance="@style/m3_body_large" />
<Button
android:id="@+id/favorites"
android:layout_width="wrap_content"
<TextView
android:id="@+id/reblogs_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:textSize="14sp"
android:minHeight="36dp"
android:layout_below="@id/title"
android:layout_alignStart="@id/title"
android:singleLine="true"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:background="@drawable/bg_text_button"
android:fontFamily="sans-serif"
tools:text="12 favorites"/>
tools:text="123 456"/>
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/favorites"
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="?android:selectableItemBackground">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_fluent_star_24_regular"
android:tint="?android:textColorSecondary"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_toEndOf="@id/icon"
android:minHeight="22dp"
android:singleLine="true"
android:text="@string/post_info_favorites"
android:textAppearance="@style/m3_body_large" />
<TextView
android:id="@+id/favorites_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_alignStart="@id/title"
android:singleLine="true"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
tools:text="123 456"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/edit_history"
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="?android:selectableItemBackground">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_fluent_edit_24_regular"
android:tint="?android:textColorSecondary"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_toEndOf="@id/icon"
android:minHeight="22dp"
android:singleLine="true"
android:text="@string/edit_history"
android:textAppearance="@style/m3_body_large" />
<TextView
android:id="@+id/last_edited"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_alignStart="@id/title"
android:singleLine="true"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
tools:text="123 456"/>
</RelativeLayout>
<TextView
android:id="@+id/timestamp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_margin="16dp"
android:minHeight="20dp"
android:gravity="center_vertical"
android:textSize="14sp"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/edit" android:title="@string/edit"/>
<item android:id="@+id/delete" android:title="@string/delete"/>
<item android:id="@+id/delete_and_redraft" android:title="@string/delete_and_redraft"/>
<item android:id="@+id/pin" android:title="@string/pin_post"/>

View File

@@ -358,4 +358,34 @@
</plurals>
<string name="timestamp_via_app">%1$s via %2$s</string>
<string name="time_now">now</string>
<string name="post_info_reblogs">Reblogs</string>
<string name="post_info_favorites">Favorites</string>
<string name="edit_history">Edit history</string>
<string name="last_edit_at_x">Last edit %s</string>
<string name="time_just_now">just now</string>
<plurals name="x_seconds_ago">
<item quantity="one">%d second ago</item>
<item quantity="other">%d seconds ago</item>
</plurals>
<plurals name="x_minutes_ago">
<item quantity="one">%d minute ago</item>
<item quantity="other">%d minutes ago</item>
</plurals>
<string name="edited_timestamp">edited %s</string>
<string name="edit_original_post">Original post</string>
<string name="edit_text_edited">Text edited</string>
<string name="edit_spoiler_added">Content warning added</string>
<string name="edit_spoiler_edited">Content warning edited</string>
<string name="edit_spoiler_removed">Content warning removed</string>
<string name="edit_poll_added">Poll added</string>
<string name="edit_poll_edited">Poll edited</string>
<string name="edit_poll_removed">Poll removed</string>
<string name="edit_media_added">Media added</string>
<string name="edit_media_removed">Media removed</string>
<string name="edit_media_reordered">Media reordered</string>
<string name="edit_marked_sensitive">Marked sensitive</string>
<string name="edit_marked_not_sensitive">Marked not sensitive</string>
<string name="edit_multiple_changed">Post edited</string>
<string name="edit">Edit</string>
<string name="discard_changes">Discard changes?</string>
</resources>