Refresh message timestamp every minute (#1113)
* Refresh message timestamp every minute * Refresh timeline adapter every minute via differ
This commit is contained in:
		
					parent
					
						
							
								d0f7f6f83c
							
						
					
				
			
			
				commit
				
					
						7680b1b529
					
				
			
		
					 5 changed files with 182 additions and 104 deletions
				
			
		|  | @ -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<Attachment> 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<Attachment> 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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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<String> 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() | ||||
|                     ); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue