diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 0fd5f618..1f025e6b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -44,7 +44,9 @@ import at.connyduck.sparkbutton.SparkEventListener; import kotlin.collections.CollectionsKt; public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { - + public static class Key{ + public static final String KEY_CREATED = "created"; + } private TextView displayName; private TextView username; private ImageButton replyButton; @@ -538,45 +540,60 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, boolean mediaPreviewEnabled) { - setDisplayName(status.getUserFullName(), status.getAccountEmojis()); - setUsername(status.getNickname()); - setCreatedAt(status.getCreatedAt()); - setIsReply(status.getInReplyToId() != null); - setAvatar(status.getAvatar(), status.getRebloggedAvatar()); - setReblogged(status.isReblogged()); - setFavourited(status.isFavourited()); - List attachments = status.getAttachments(); - boolean sensitive = status.isSensitive(); - if (mediaPreviewEnabled) { - setMediaPreviews(attachments, sensitive, listener, status.isShowingContent()); + this.setupWithStatus(status,listener,mediaPreviewEnabled,null); + } + protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, + boolean mediaPreviewEnabled, @Nullable Object payloads) { + if (payloads == null) { + setDisplayName(status.getUserFullName(), status.getAccountEmojis()); + setUsername(status.getNickname()); + setCreatedAt(status.getCreatedAt()); + setIsReply(status.getInReplyToId() != null); + setAvatar(status.getAvatar(), status.getRebloggedAvatar()); + setReblogged(status.isReblogged()); + setFavourited(status.isFavourited()); + List attachments = status.getAttachments(); + boolean sensitive = status.isSensitive(); + if (mediaPreviewEnabled) { + setMediaPreviews(attachments, sensitive, listener, status.isShowingContent()); - if (attachments.size() == 0) { + if (attachments.size() == 0) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + mediaLabel.setVisibility(View.GONE); + } else { + setMediaLabel(attachments, sensitive, listener); + // Hide all unused views. + mediaPreviews[0].setVisibility(View.GONE); + mediaPreviews[1].setVisibility(View.GONE); + mediaPreviews[2].setVisibility(View.GONE); + mediaPreviews[3].setVisibility(View.GONE); hideSensitiveMediaWarning(); } - // Hide the unused label. - mediaLabel.setVisibility(View.GONE); - } else { - setMediaLabel(attachments, sensitive, listener); - // Hide all unused views. - mediaPreviews[0].setVisibility(View.GONE); - mediaPreviews[1].setVisibility(View.GONE); - mediaPreviews[2].setVisibility(View.GONE); - mediaPreviews[3].setVisibility(View.GONE); - hideSensitiveMediaWarning(); + + setupButtons(listener, status.getSenderId()); + setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); + + setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener); + + setContentDescription(status); + // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 + // RecyclerView tries to set AccessibilityDelegateCompat to null + // but ViewCompat code replaces is with the default one. RecyclerView never + // fetches another one from its delegate because it checks that it's set so we remove it + // and let RecyclerView ask for a new delegate. + itemView.setAccessibilityDelegate(null); } + else{ + if (payloads instanceof List) + for (Object item:(List)payloads) { + if (Key.KEY_CREATED.equals(item)){ + setCreatedAt(status.getCreatedAt()); + } + } - setupButtons(listener, status.getSenderId()); - setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); - - setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener); - - setContentDescription(status); - // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 - // RecyclerView tries to set AccessibilityDelegateCompat to null - // but ViewCompat code replaces is with the default one. RecyclerView never - // fetches another one from its delegate because it checks that it's set so we remove it - // and let RecyclerView ask for a new delegate. - itemView.setAccessibilityDelegate(null); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 55c8c117..adf94d57 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -125,74 +125,75 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { @Override protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener, - boolean mediaPreviewEnabled) { - super.setupWithStatus(status, listener, mediaPreviewEnabled); + boolean mediaPreviewEnabled, @Nullable Object payloads) { + super.setupWithStatus(status, listener, mediaPreviewEnabled,payloads); + if (payloads==null) { + setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); - setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); - - setApplication(status.getApplication()); + setApplication(status.getApplication()); - View.OnLongClickListener longClickListener = view -> { - TextView textView = (TextView) view; - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("toot", textView.getText()); - clipboard.setPrimaryClip(clip); + View.OnLongClickListener longClickListener = view -> { + TextView textView = (TextView) view; + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("toot", textView.getText()); + clipboard.setPrimaryClip(clip); - Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show(); + Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show(); - return true; - }; + return true; + }; - content.setOnLongClickListener(longClickListener); - contentWarningDescription.setOnLongClickListener(longClickListener); + content.setOnLongClickListener(longClickListener); + contentWarningDescription.setOnLongClickListener(longClickListener); - if (status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) { - final Card card = status.getCard(); - cardView.setVisibility(View.VISIBLE); - cardTitle.setText(card.getTitle()); - cardDescription.setText(card.getDescription()); + if (status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) { + final Card card = status.getCard(); + cardView.setVisibility(View.VISIBLE); + cardTitle.setText(card.getTitle()); + cardDescription.setText(card.getDescription()); - cardUrl.setText(card.getUrl()); + cardUrl.setText(card.getUrl()); - if (card.getWidth() > 0 && card.getHeight() > 0 && !TextUtils.isEmpty(card.getImage())) { - cardImage.setVisibility(View.VISIBLE); + if (card.getWidth() > 0 && card.getHeight() > 0 && !TextUtils.isEmpty(card.getImage())) { + cardImage.setVisibility(View.VISIBLE); + + if (card.getWidth() > card.getHeight()) { + cardView.setOrientation(LinearLayout.VERTICAL); + cardImage.getLayoutParams().height = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_vertical_height); + cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + } + + cardView.setClipToOutline(true); + + Picasso.with(cardImage.getContext()) + .load(card.getImage()) + .fit() + .centerCrop() + .into(cardImage); - if (card.getWidth() > card.getHeight()) { - cardView.setOrientation(LinearLayout.VERTICAL); - cardImage.getLayoutParams().height = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_vertical_height); - cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; - cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; } else { - cardView.setOrientation(LinearLayout.HORIZONTAL); - cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardImage.getLayoutParams().width = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_horizontal_width); - cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; - cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.setVisibility(View.GONE); } - cardView.setClipToOutline(true); - - Picasso.with(cardImage.getContext()) - .load(card.getImage()) - .fit() - .centerCrop() - .into(cardImage); + cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext())); } else { - cardImage.setVisibility(View.GONE); + cardView.setVisibility(View.GONE); } - cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext())); - - } else { - cardView.setVisibility(View.GONE); + setStatusVisibility(status.getVisibility()); } - - setStatusVisibility(status.getVisibility()); } private void setStatusVisibility(Status.Visibility visibility) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index df708d94..f13a359f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -75,22 +75,26 @@ public class StatusViewHolder extends StatusBaseViewHolder { @Override protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, - boolean mediaPreviewEnabled) { - if(status == null) { - showContent(false); - } else { - showContent(true); - setupCollapsedState(status, listener); - super.setupWithStatus(status, listener, mediaPreviewEnabled); - - String rebloggedByDisplayName = status.getRebloggedByUsername(); - if (rebloggedByDisplayName == null) { - hideRebloggedByDisplayName(); + boolean mediaPreviewEnabled, @Nullable Object payloads) { + if (status == null || payloads==null) { + if (status == null) { + showContent(false); } else { - setRebloggedByDisplayName(rebloggedByDisplayName); - } + showContent(true); + setupCollapsedState(status, listener); + super.setupWithStatus(status, listener, mediaPreviewEnabled, null); - rebloggedBar.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); + String rebloggedByDisplayName = status.getRebloggedByUsername(); + if (rebloggedByDisplayName == null) { + hideRebloggedByDisplayName(); + } else { + setRebloggedByDisplayName(rebloggedByDisplayName); + } + + rebloggedBar.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition()));} + } + else{ + super.setupWithStatus(status, listener, mediaPreviewEnabled, payloads); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 98f65e1e..c7d95d17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -16,11 +16,14 @@ package com.keylesspalace.tusky.adapter; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import java.util.List; + import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.viewdata.StatusViewData; @@ -70,17 +73,25 @@ public final class TimelineAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + bindViewHolder(viewHolder,position,null); + } + + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { + bindViewHolder(viewHolder,position,payloads); + } + + private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads){ StatusViewData status = dataSource.getItemAt(position); if (status instanceof StatusViewData.Placeholder) { PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; holder.setup(statusListener, ((StatusViewData.Placeholder) status).isLoading()); - } else { + } else if (status instanceof StatusViewData.Concrete) { StatusViewHolder holder = (StatusViewHolder) viewHolder; - holder.setupWithStatus((StatusViewData.Concrete) status, - statusListener, mediaPreviewEnabled); + holder.setupWithStatus((StatusViewData.Concrete)status,statusListener, mediaPreviewEnabled,payloads!=null&&!payloads.isEmpty()?payloads.get(0):null); } } - @Override public int getItemCount() { return dataSource.getItemCount(); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 89171f91..0a3807cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -28,9 +28,11 @@ import android.widget.ProgressBar; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; +import io.reactivex.Observable; import com.keylesspalace.tusky.AccountListActivity; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.adapter.TimelineAdapter; import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.EventHub; @@ -63,10 +65,12 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.StatusViewData; import java.io.IOException; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1266,7 +1270,48 @@ public class TimelineFragment extends SFragment implements @Override public boolean areContentsTheSame(StatusViewData oldItem, @NonNull StatusViewData newItem) { - return oldItem.deepEquals(newItem); + return false; //Items are different always. It allows to refresh timestamp on every view holder update + } + + @Nullable + @Override + public Object getChangePayload(@NonNull StatusViewData oldItem, @NonNull StatusViewData newItem) { + if (oldItem.deepEquals(newItem)){ + //If items are equal - update timestamp only + List payload = new ArrayList<>(); + payload.add(StatusBaseViewHolder.Key.KEY_CREATED); + return payload; + } + else + // If items are different - update a whole view holder + return null; } }; + + @Override + public void onResume() { + super.onResume(); + startUpdateTimestamp(); + } + + /** + * Start to update adapter every minute to refresh timestamp + * If setting absoluteTimeView is false + * Auto dispose observable on pause + */ + private void startUpdateTimestamp() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) + .subscribe( + interval -> updateAdapter() + ); + } + + } + + }