Post edit history + extended footer redesign

This commit is contained in:
Grishka 2022-09-02 02:00:25 +03:00
parent 265b2ad32c
commit 8fb2b454dd
12 changed files with 517 additions and 142 deletions

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

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

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

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

@ -175,7 +175,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void onBind(HeaderStatusDisplayItem item){
name.setText(item.parsedName);
username.setText('@'+item.user.acct);
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);

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

@ -62,6 +62,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.util.Arrays;
import java.util.List;
import java.util.Map;
@ -85,6 +86,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(){}
@ -129,6 +131,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">
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
<RelativeLayout
android:id="@+id/reblogs"
android:layout_width="wrap_content"
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_width="match_parent"
android:layout_height="64dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="?android:selectableItemBackground">
<Button
<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: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" />
<TextView
android:id="@+id/reblogs_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/favorites"
android:layout_width="wrap_content"
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="12 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">
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
<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

@ -340,4 +340,32 @@
</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>
</resources>