diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java new file mode 100644 index 000000000..279773a40 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -0,0 +1,469 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.content.res.AppCompatResources; +import android.support.v7.widget.RecyclerView; +import android.text.Spanned; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ToggleButton; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.DateUtils; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.ThemeUtils; +import com.keylesspalace.tusky.view.RoundedTransformation; +import com.keylesspalace.tusky.viewdata.StatusViewData; +import com.squareup.picasso.Picasso; +import com.varunest.sparkbutton.SparkButton; +import com.varunest.sparkbutton.SparkEventListener; + +import java.util.Date; + +class StatusBaseViewHolder extends RecyclerView.ViewHolder { + private View container; + private TextView displayName; + private TextView username; + private TextView content; + private ImageButton replyButton; + private SparkButton reblogButton; + private SparkButton favouriteButton; + private ImageButton moreButton; + private boolean favourited; + private boolean reblogged; + private ImageView mediaPreview0; + private ImageView mediaPreview1; + private ImageView mediaPreview2; + private ImageView mediaPreview3; + private View sensitiveMediaWarning; + private View videoIndicator; + private TextView mediaLabel; + private View contentWarningBar; + private TextView contentWarningDescription; + private ToggleButton contentWarningButton; + + ImageView avatar; + TextView timestamp; + + StatusBaseViewHolder(View itemView) { + super(itemView); + container = itemView.findViewById(R.id.status_container); + displayName = (TextView) itemView.findViewById(R.id.status_display_name); + username = (TextView) itemView.findViewById(R.id.status_username); + timestamp = (TextView) itemView.findViewById(R.id.status_timestamp); + content = (TextView) itemView.findViewById(R.id.status_content); + avatar = (ImageView) itemView.findViewById(R.id.status_avatar); + replyButton = (ImageButton) itemView.findViewById(R.id.status_reply); + reblogButton = (SparkButton) itemView.findViewById(R.id.status_reblog); + favouriteButton = (SparkButton) itemView.findViewById(R.id.status_favourite); + moreButton = (ImageButton) itemView.findViewById(R.id.status_more); + reblogged = false; + favourited = false; + mediaPreview0 = (ImageView) itemView.findViewById(R.id.status_media_preview_0); + mediaPreview1 = (ImageView) itemView.findViewById(R.id.status_media_preview_1); + mediaPreview2 = (ImageView) itemView.findViewById(R.id.status_media_preview_2); + mediaPreview3 = (ImageView) itemView.findViewById(R.id.status_media_preview_3); + sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); + videoIndicator = itemView.findViewById(R.id.status_video_indicator); + mediaLabel = (TextView) itemView.findViewById(R.id.status_media_label); + contentWarningBar = itemView.findViewById(R.id.status_content_warning_bar); + contentWarningDescription = + (TextView) itemView.findViewById(R.id.status_content_warning_description); + contentWarningButton = + (ToggleButton) itemView.findViewById(R.id.status_content_warning_button); + } + + private void setDisplayName(String name) { + displayName.setText(name); + } + + private void setUsername(String name) { + Context context = username.getContext(); + String format = context.getString(R.string.status_username_format); + String usernameText = String.format(format, name); + username.setText(usernameText); + } + + private void setContent(Spanned content, Status.Mention[] mentions, + StatusActionListener listener) { + /* Redirect URLSpan's in the status content to the listener for viewing tag pages and + * account pages. */ + Context context = this.content.getContext(); + boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("customTabs", true); + LinkHelper.setClickableText(this.content, content, mentions, useCustomTabs, listener); + } + + void setAvatar(String url, @Nullable String rebloggedUrl) { + if (url.isEmpty()) { + avatar.setImageResource(R.drawable.avatar_default); + } else { + Picasso.with(avatar.getContext()) + .load(url) + .placeholder(R.drawable.avatar_default) + .error(R.drawable.avatar_error) + .transform(new RoundedTransformation(7, 0)) + .into(avatar); + } + } + + protected void setCreatedAt(@Nullable Date createdAt) { + // This is the visible timestamp. + String readout; + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + CharSequence readoutAloud; + if (createdAt != null) { + long then = createdAt.getTime(); + long now = new Date().getTime(); + readout = DateUtils.getRelativeTimeSpanString(timestamp.getContext(), then, now); + readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now, + android.text.format.DateUtils.SECOND_IN_MILLIS, + android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); + } else { + // unknown minutes~ + readout = "?m"; + readoutAloud = "? minutes"; + } + timestamp.setText(readout); + timestamp.setContentDescription(readoutAloud); + } + + private void setReblogged(boolean reblogged) { + this.reblogged = reblogged; + reblogButton.setChecked(reblogged); + } + + // This should only be called after setReblogged, in order to override the tint correctly. + private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) { + reblogButton.setEnabled(enabled); + + if (enabled) { + int inactiveId = ThemeUtils.getDrawableId(reblogButton.getContext(), + R.attr.status_reblog_inactive_drawable, R.drawable.reblog_inactive_dark); + reblogButton.setInactiveImage(inactiveId); + reblogButton.setActiveImage(R.drawable.reblog_active); + } else { + int disabledId; + if (visibility == Status.Visibility.DIRECT) { + disabledId = ThemeUtils.getDrawableId(reblogButton.getContext(), + R.attr.status_reblog_direct_drawable, R.drawable.reblog_direct_dark); + } else { + disabledId = ThemeUtils.getDrawableId(reblogButton.getContext(), + R.attr.status_reblog_disabled_drawable, R.drawable.reblog_disabled_dark); + } + reblogButton.setInactiveImage(disabledId); + reblogButton.setActiveImage(disabledId); + } + } + + private void setFavourited(boolean favourited) { + this.favourited = favourited; + favouriteButton.setChecked(favourited); + } + + private void setMediaPreviews(final Status.MediaAttachment[] attachments, boolean sensitive, + final StatusActionListener listener, boolean showingSensitive) { + final ImageView[] previews = { + mediaPreview0, + mediaPreview1, + mediaPreview2, + mediaPreview3 + }; + Context context = mediaPreview0.getContext(); + + int mediaPreviewUnloadedId = ThemeUtils.getDrawableId(itemView.getContext(), + R.attr.media_preview_unloaded_drawable, android.R.color.black); + + final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); + + final String[] urls = new String[n]; + for (int i = 0; i < n; i++) { + urls[i] = attachments[i].url; + } + + for (int i = 0; i < n; i++) { + String previewUrl = attachments[i].previewUrl; + + previews[i].setVisibility(View.VISIBLE); + + if (previewUrl == null || previewUrl.isEmpty()) { + Picasso.with(context) + .load(mediaPreviewUnloadedId) + .into(previews[i]); + } else { + Picasso.with(context) + .load(previewUrl) + .placeholder(mediaPreviewUnloadedId) + .into(previews[i]); + } + + final Status.MediaAttachment.Type type = attachments[i].type; + if (type == Status.MediaAttachment.Type.VIDEO + | type == Status.MediaAttachment.Type.GIFV) { + videoIndicator.setVisibility(View.VISIBLE); + } + + if (urls[i] == null || urls[i].isEmpty()) { + previews[i].setOnClickListener(null); + } else { + final int urlIndex = i; + previews[i].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewMedia(urls, urlIndex, type, v); + } + }); + } + } + + if (sensitive) { + sensitiveMediaWarning.setVisibility(showingSensitive ? View.GONE : View.VISIBLE); + sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getAdapterPosition()); + } + v.setVisibility(View.GONE); + v.setOnClickListener(null); + } + }); + } + + // Hide any of the placeholder previews beyond the ones set. + for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { + previews[i].setVisibility(View.GONE); + } + } + + @NonNull + private static String getLabelTypeText(Context context, Status.MediaAttachment.Type type) { + switch (type) { + default: + case IMAGE: + return context.getString(R.string.status_media_images); + case GIFV: + case VIDEO: + return context.getString(R.string.status_media_video); + } + } + + @DrawableRes + private static int getLabelIcon(Status.MediaAttachment.Type type) { + switch (type) { + default: + case IMAGE: + return R.drawable.ic_photo_24dp; + case GIFV: + case VIDEO: + return R.drawable.ic_videocam_24dp; + } + } + + private void setMediaLabel(Status.MediaAttachment[] attachments, boolean sensitive, + final StatusActionListener listener) { + if (attachments.length == 0) { + mediaLabel.setVisibility(View.GONE); + return; + } + mediaLabel.setVisibility(View.VISIBLE); + + // Set the label's text. + Context context = itemView.getContext(); + String labelText = getLabelTypeText(context, attachments[0].type); + if (sensitive) { + String sensitiveText = context.getString(R.string.status_sensitive_media_title); + labelText += String.format(" (%s)", sensitiveText); + } + mediaLabel.setText(labelText); + + // Set the icon next to the label. + int drawableId = getLabelIcon(attachments[0].type); + Drawable drawable = AppCompatResources.getDrawable(context, drawableId); + ThemeUtils.setDrawableTint(context, drawable, android.R.attr.textColorTertiary); + mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + + // Set the listener for the media view action. + int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); + final String[] urls = new String[n]; + for (int i = 0; i < n; i++) { + urls[i] = attachments[i].url; + } + final Status.MediaAttachment.Type type = attachments[0].type; + mediaLabel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewMedia(urls, 0, type, null); + } + }); + } + + private void hideSensitiveMediaWarning() { + sensitiveMediaWarning.setVisibility(View.GONE); + } + + private void setSpoilerText(String spoilerText, final boolean expanded, + final StatusActionListener listener) { + contentWarningDescription.setText(spoilerText); + contentWarningBar.setVisibility(View.VISIBLE); + contentWarningButton.setChecked(expanded); + contentWarningButton.setOnCheckedChangeListener( + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onExpandedChange(isChecked, getAdapterPosition()); + } + if (isChecked) { + content.setVisibility(View.VISIBLE); + } else { + content.setVisibility(View.GONE); + } + } + }); + if (expanded) { + content.setVisibility(View.VISIBLE); + } else { + content.setVisibility(View.GONE); + } + } + + private void hideSpoilerText() { + contentWarningBar.setVisibility(View.GONE); + content.setVisibility(View.VISIBLE); + } + + private void setupButtons(final StatusActionListener listener, final String accountId) { + /* Originally position was passed through to all these listeners, but it caused several + * bugs where other statuses in the list would be removed or added and cause the position + * here to become outdated. So, getting the adapter position at the time the listener is + * actually called is the appropriate solution. */ + avatar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewAccount(accountId); + } + }); + replyButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onReply(position); + } + } + }); + reblogButton.setEventListener(new SparkEventListener() { + @Override + public void onEvent(ImageView button, boolean buttonState) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onReblog(!reblogged, position); + } + } + + @Override + public void onEventAnimationEnd(ImageView button, boolean buttonState) { + } + + @Override + public void onEventAnimationStart(ImageView button, boolean buttonState) { + } + }); + favouriteButton.setEventListener(new SparkEventListener() { + @Override + public void onEvent(ImageView button, boolean buttonState) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onFavourite(!favourited, position); + } + } + + @Override + public void onEventAnimationEnd(ImageView button, boolean buttonState) { + } + + @Override + public void onEventAnimationStart(ImageView button, boolean buttonState) { + } + }); + moreButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMore(v, position); + } + } + }); + /* Even though the content TextView is a child of the container, it won't respond to clicks + * if it contains URLSpans without also setting its listener. The surrounding spans will + * just eat the clicks instead of deferring to the parent listener, but WILL respond to a + * listener directly on the TextView, for whatever reason. */ + View.OnClickListener viewThreadListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + } + }; + content.setOnClickListener(viewThreadListener); + container.setOnClickListener(viewThreadListener); + } + + void setupWithStatus(StatusViewData status, final StatusActionListener listener, + boolean mediaPreviewEnabled) { + setDisplayName(status.getUserFullName()); + setUsername(status.getNickname()); + setCreatedAt(status.getCreatedAt()); + setContent(status.getContent(), status.getMentions(), listener); + setAvatar(status.getAvatar(), status.getRebloggedAvatar()); + setReblogged(status.isReblogged()); + setFavourited(status.isFavourited()); + Status.MediaAttachment[] attachments = status.getAttachments(); + boolean sensitive = status.isSensitive(); + if (mediaPreviewEnabled) { + setMediaPreviews(attachments, sensitive, listener, status.isShowingSensitiveContent()); + /* A status without attachments is sometimes still marked sensitive, so it's necessary + * to check both whether there are any attachments and if it's marked sensitive. */ + if (!sensitive || attachments.length == 0) { + hideSensitiveMediaWarning(); + } + if (attachments.length == 0) { + videoIndicator.setVisibility(View.GONE); + } + // Hide the unused label. + mediaLabel.setVisibility(View.GONE); + } else { + setMediaLabel(attachments, sensitive, listener); + // Hide all unused views. + mediaPreview0.setVisibility(View.GONE); + mediaPreview1.setVisibility(View.GONE); + mediaPreview2.setVisibility(View.GONE); + mediaPreview3.setVisibility(View.GONE); + hideSensitiveMediaWarning(); + videoIndicator.setVisibility(View.GONE); + } + + setupButtons(listener, status.getSenderId()); + setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); + if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) { + hideSpoilerText(); + } else { + setSpoilerText(status.getSpoilerText(), status.isExpanded(), listener); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java new file mode 100644 index 000000000..457cc1a21 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -0,0 +1,75 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.URLSpan; +import android.view.View; +import android.widget.TextView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomTabURLSpan; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.DateFormat; +import java.util.Date; + +class StatusDetailedViewHolder extends StatusBaseViewHolder { + private TextView reblogs; + private TextView favourites; + private TextView application; + + StatusDetailedViewHolder(View view) { + super(view); + reblogs = (TextView) view.findViewById(R.id.status_reblogs); + favourites = (TextView) view.findViewById(R.id.status_favourites); + application = (TextView) view.findViewById(R.id.status_application); + } + + @Override + protected void setCreatedAt(@Nullable Date createdAt) { + if (createdAt != null) { + DateFormat dateFormat = android.text.format.DateFormat.getMediumDateFormat( + timestamp.getContext()); + timestamp.setText(dateFormat.format(createdAt)); + } else { + timestamp.setText(""); + } + } + + private void setApplication(@Nullable Status.Application app) { + if (app == null) { + application.setText(""); + } else if (app.website != null) { + URLSpan span; + Context context = application.getContext(); + boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("customTabs", true); + if (useCustomTabs) { + span = new CustomTabURLSpan(app.website); + } else { + span = new URLSpan(app.website); + } + SpannableStringBuilder text = new SpannableStringBuilder(app.name); + text.setSpan(span, 0, app.name.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + application.setText(text); + application.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + application.setText(app.name); + } + } + + @Override + void setupWithStatus(StatusViewData status, final StatusActionListener listener, + boolean mediaPreviewEnabled) { + super.setupWithStatus(status, listener, mediaPreviewEnabled); + reblogs.setText(status.getReblogsCount()); + favourites.setText(status.getFavouritesCount()); + setApplication(status.getApplication()); + } +} 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 991c5598f..4a2ca4c5c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -16,118 +16,35 @@ package com.keylesspalace.tusky.adapter; import android.content.Context; -import android.graphics.drawable.Drawable; import android.os.Build; -import android.preference.PreferenceManager; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v7.content.res.AppCompatResources; -import android.support.v7.widget.RecyclerView; -import android.text.Spanned; import android.view.View; -import android.widget.CompoundButton; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; -import android.widget.ToggleButton; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.util.DateUtils; -import com.keylesspalace.tusky.util.LinkHelper; -import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.view.RoundedTransformation; import com.keylesspalace.tusky.viewdata.StatusViewData; import com.squareup.picasso.Picasso; -import com.varunest.sparkbutton.SparkButton; -import com.varunest.sparkbutton.SparkEventListener; import com.varunest.sparkbutton.helpers.Utils; -import java.util.Date; - -public class StatusViewHolder extends RecyclerView.ViewHolder { - private View container; - private TextView displayName; - private TextView username; - private TextView content; - private ImageView avatar; +public class StatusViewHolder extends StatusBaseViewHolder { private ImageView avatarReblog; private View rebloggedBar; private TextView rebloggedByDisplayName; - private ImageButton replyButton; - private SparkButton reblogButton; - private SparkButton favouriteButton; - private ImageButton moreButton; - private boolean favourited; - private boolean reblogged; - private ImageView mediaPreview0; - private ImageView mediaPreview1; - private ImageView mediaPreview2; - private ImageView mediaPreview3; - private View sensitiveMediaWarning; - private View videoIndicator; - private TextView mediaLabel; - private View contentWarningBar; - private TextView contentWarningDescription; - private ToggleButton contentWarningButton; - - TextView timestamp; StatusViewHolder(View itemView) { super(itemView); - container = itemView.findViewById(R.id.status_container); - displayName = (TextView) itemView.findViewById(R.id.status_display_name); - username = (TextView) itemView.findViewById(R.id.status_username); - timestamp = (TextView) itemView.findViewById(R.id.status_timestamp); - content = (TextView) itemView.findViewById(R.id.status_content); - avatar = (ImageView) itemView.findViewById(R.id.status_avatar); avatarReblog = (ImageView) itemView.findViewById(R.id.status_avatar_reblog); rebloggedBar = itemView.findViewById(R.id.status_reblogged_bar); rebloggedByDisplayName = (TextView) itemView.findViewById(R.id.status_reblogged); - replyButton = (ImageButton) itemView.findViewById(R.id.status_reply); - reblogButton = (SparkButton) itemView.findViewById(R.id.status_reblog); - favouriteButton = (SparkButton) itemView.findViewById(R.id.status_favourite); - moreButton = (ImageButton) itemView.findViewById(R.id.status_more); - reblogged = false; - favourited = false; - mediaPreview0 = (ImageView) itemView.findViewById(R.id.status_media_preview_0); - mediaPreview1 = (ImageView) itemView.findViewById(R.id.status_media_preview_1); - mediaPreview2 = (ImageView) itemView.findViewById(R.id.status_media_preview_2); - mediaPreview3 = (ImageView) itemView.findViewById(R.id.status_media_preview_3); - sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); - videoIndicator = itemView.findViewById(R.id.status_video_indicator); - mediaLabel = (TextView) itemView.findViewById(R.id.status_media_label); - contentWarningBar = itemView.findViewById(R.id.status_content_warning_bar); - contentWarningDescription = - (TextView) itemView.findViewById(R.id.status_content_warning_description); - contentWarningButton = - (ToggleButton) itemView.findViewById(R.id.status_content_warning_button); } - private void setDisplayName(String name) { - displayName.setText(name); - } + @Override + void setAvatar(String url, @Nullable String rebloggedUrl) { + super.setAvatar(url, rebloggedUrl); - private void setUsername(String name) { - Context context = username.getContext(); - String format = context.getString(R.string.status_username_format); - String usernameText = String.format(format, name); - username.setText(usernameText); - } - - private void setContent(Spanned content, Status.Mention[] mentions, - StatusActionListener listener) { - /* Redirect URLSpan's in the status content to the listener for viewing tag pages and - * account pages. */ - Context context = this.content.getContext(); - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean("customTabs", true); - LinkHelper.setClickableText(this.content, content, mentions, useCustomTabs, listener); - } - - private void setAvatar(String url, @Nullable String rebloggedUrl) { Context context = avatar.getContext(); boolean hasReblog = rebloggedUrl != null && !rebloggedUrl.isEmpty(); int padding = hasReblog ? Utils.dpToPx(context, 12) : 0; @@ -137,57 +54,42 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { avatar.setPadding(0, 0, padding, padding); } - if (url.isEmpty()) { - avatar.setImageResource(R.drawable.avatar_default); - } else { + if (hasReblog) { + avatarReblog.setVisibility(View.VISIBLE); Picasso.with(context) - .load(url) - .placeholder(R.drawable.avatar_default) - .error(R.drawable.avatar_error) + .load(rebloggedUrl) + .fit() .transform(new RoundedTransformation(7, 0)) - .into(avatar); - } - - if (avatarReblog != null) { - if (hasReblog) { - avatarReblog.setVisibility(View.VISIBLE); - Picasso.with(context) - .load(rebloggedUrl) - .fit() - .transform(new RoundedTransformation(7, 0)) - .into(avatarReblog); - } else { - avatarReblog.setVisibility(View.GONE); - } + .into(avatarReblog); + } else { + avatarReblog.setVisibility(View.GONE); } } - protected void setCreatedAt(@Nullable Date createdAt) { - // This is the visible timestamp. - String readout; - /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" - * as 17 meters instead of minutes. */ - CharSequence readoutAloud; - if (createdAt != null) { - long then = createdAt.getTime(); - long now = new Date().getTime(); - readout = DateUtils.getRelativeTimeSpanString(timestamp.getContext(), then, now); - readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now, - android.text.format.DateUtils.SECOND_IN_MILLIS, - android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); + @Override + void setupWithStatus(StatusViewData status, final StatusActionListener listener, + boolean mediaPreviewEnabled) { + super.setupWithStatus(status, listener, mediaPreviewEnabled); + + String rebloggedByDisplayName = status.getRebloggedByUsername(); + if (rebloggedByDisplayName == null) { + hideRebloggedByDisplayName(); } else { - // unknown minutes~ - readout = "?m"; - readoutAloud = "? minutes"; + setRebloggedByDisplayName(rebloggedByDisplayName); } - timestamp.setText(readout); - timestamp.setContentDescription(readoutAloud); + + // I think it's not efficient to create new object every time we bind a holder. + // More efficient approach would be creating View.OnClickListener during holder creation + // and storing StatusActionListener in a variable after binding. + rebloggedBar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onOpenReblog(getAdapterPosition()); + } + }); } private void setRebloggedByDisplayName(String name) { - if (rebloggedByDisplayName == null || rebloggedBar == null) { - return; - } Context context = rebloggedByDisplayName.getContext(); String format = context.getString(R.string.status_boosted_format); String boostedText = String.format(format, name); @@ -201,350 +103,4 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { } rebloggedBar.setVisibility(View.GONE); } - - private void setReblogged(boolean reblogged) { - this.reblogged = reblogged; - reblogButton.setChecked(reblogged); - } - - // This should only be called after setReblogged, in order to override the tint correctly. - private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) { - reblogButton.setEnabled(enabled); - - if (enabled) { - int inactiveId = ThemeUtils.getDrawableId(reblogButton.getContext(), - R.attr.status_reblog_inactive_drawable, R.drawable.reblog_inactive_dark); - reblogButton.setInactiveImage(inactiveId); - reblogButton.setActiveImage(R.drawable.reblog_active); - } else { - int disabledId; - if (visibility == Status.Visibility.DIRECT) { - disabledId = ThemeUtils.getDrawableId(reblogButton.getContext(), - R.attr.status_reblog_direct_drawable, R.drawable.reblog_direct_dark); - } else { - disabledId = ThemeUtils.getDrawableId(reblogButton.getContext(), - R.attr.status_reblog_disabled_drawable, R.drawable.reblog_disabled_dark); - } - reblogButton.setInactiveImage(disabledId); - reblogButton.setActiveImage(disabledId); - } - } - - private void setFavourited(boolean favourited) { - this.favourited = favourited; - favouriteButton.setChecked(favourited); - } - - private void setMediaPreviews(final Status.MediaAttachment[] attachments, boolean sensitive, - final StatusActionListener listener, boolean showingSensitive) { - final ImageView[] previews = { - mediaPreview0, - mediaPreview1, - mediaPreview2, - mediaPreview3 - }; - Context context = mediaPreview0.getContext(); - - int mediaPreviewUnloadedId = ThemeUtils.getDrawableId(itemView.getContext(), - R.attr.media_preview_unloaded_drawable, android.R.color.black); - - final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); - - final String[] urls = new String[n]; - for (int i = 0; i < n; i++) { - urls[i] = attachments[i].url; - } - - for (int i = 0; i < n; i++) { - String previewUrl = attachments[i].previewUrl; - - previews[i].setVisibility(View.VISIBLE); - - if (previewUrl == null || previewUrl.isEmpty()) { - Picasso.with(context) - .load(mediaPreviewUnloadedId) - .into(previews[i]); - } else { - Picasso.with(context) - .load(previewUrl) - .placeholder(mediaPreviewUnloadedId) - .into(previews[i]); - } - - final Status.MediaAttachment.Type type = attachments[i].type; - if (type == Status.MediaAttachment.Type.VIDEO - | type == Status.MediaAttachment.Type.GIFV) { - videoIndicator.setVisibility(View.VISIBLE); - } - - if (urls[i] == null || urls[i].isEmpty()) { - previews[i].setOnClickListener(null); - } else { - final int urlIndex = i; - previews[i].setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onViewMedia(urls, urlIndex, type, v); - } - }); - } - } - - if (sensitive) { - sensitiveMediaWarning.setVisibility(showingSensitive ? View.GONE : View.VISIBLE); - sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(true, getAdapterPosition()); - } - v.setVisibility(View.GONE); - v.setOnClickListener(null); - } - }); - } - - // Hide any of the placeholder previews beyond the ones set. - for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { - previews[i].setVisibility(View.GONE); - } - } - - @NonNull - private static String getLabelTypeText(Context context, Status.MediaAttachment.Type type) { - switch (type) { - default: - case IMAGE: - return context.getString(R.string.status_media_images); - case GIFV: - case VIDEO: - return context.getString(R.string.status_media_video); - } - } - - @DrawableRes - private static int getLabelIcon(Status.MediaAttachment.Type type) { - switch (type) { - default: - case IMAGE: - return R.drawable.ic_photo_24dp; - case GIFV: - case VIDEO: - return R.drawable.ic_videocam_24dp; - } - } - - private void setMediaLabel(Status.MediaAttachment[] attachments, boolean sensitive, - final StatusActionListener listener) { - if (attachments.length == 0) { - mediaLabel.setVisibility(View.GONE); - return; - } - mediaLabel.setVisibility(View.VISIBLE); - - // Set the label's text. - Context context = itemView.getContext(); - String labelText = getLabelTypeText(context, attachments[0].type); - if (sensitive) { - String sensitiveText = context.getString(R.string.status_sensitive_media_title); - labelText += String.format(" (%s)", sensitiveText); - } - mediaLabel.setText(labelText); - - // Set the icon next to the label. - int drawableId = getLabelIcon(attachments[0].type); - Drawable drawable = AppCompatResources.getDrawable(context, drawableId); - ThemeUtils.setDrawableTint(context, drawable, android.R.attr.textColorTertiary); - mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - - // Set the listener for the media view action. - int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); - final String[] urls = new String[n]; - for (int i = 0; i < n; i++) { - urls[i] = attachments[i].url; - } - final Status.MediaAttachment.Type type = attachments[0].type; - mediaLabel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onViewMedia(urls, 0, type, null); - } - }); - } - - private void hideSensitiveMediaWarning() { - sensitiveMediaWarning.setVisibility(View.GONE); - } - - private void setSpoilerText(String spoilerText, final boolean expanded, - final StatusActionListener listener) { - contentWarningDescription.setText(spoilerText); - contentWarningBar.setVisibility(View.VISIBLE); - contentWarningButton.setChecked(expanded); - contentWarningButton.setOnCheckedChangeListener( - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onExpandedChange(isChecked, getAdapterPosition()); - } - if (isChecked) { - content.setVisibility(View.VISIBLE); - } else { - content.setVisibility(View.GONE); - } - } - }); - if (expanded) { - content.setVisibility(View.VISIBLE); - } else { - content.setVisibility(View.GONE); - } - } - - private void hideSpoilerText() { - contentWarningBar.setVisibility(View.GONE); - content.setVisibility(View.VISIBLE); - } - - private void setupButtons(final StatusActionListener listener, final String accountId) { - /* Originally position was passed through to all these listeners, but it caused several - * bugs where other statuses in the list would be removed or added and cause the position - * here to become outdated. So, getting the adapter position at the time the listener is - * actually called is the appropriate solution. */ - avatar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onViewAccount(accountId); - } - }); - replyButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onReply(position); - } - } - }); - reblogButton.setEventListener(new SparkEventListener() { - @Override - public void onEvent(ImageView button, boolean buttonState) { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onReblog(!reblogged, position); - } - } - - @Override - public void onEventAnimationEnd(ImageView button, boolean buttonState) { - } - - @Override - public void onEventAnimationStart(ImageView button, boolean buttonState) { - } - }); - favouriteButton.setEventListener(new SparkEventListener() { - @Override - public void onEvent(ImageView button, boolean buttonState) { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onFavourite(!favourited, position); - } - } - - @Override - public void onEventAnimationEnd(ImageView button, boolean buttonState) { - } - - @Override - public void onEventAnimationStart(ImageView button, boolean buttonState) { - } - }); - moreButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onMore(v, position); - } - } - }); - /* Even though the content TextView is a child of the container, it won't respond to clicks - * if it contains URLSpans without also setting its listener. The surrounding spans will - * just eat the clicks instead of deferring to the parent listener, but WILL respond to a - * listener directly on the TextView, for whatever reason. */ - View.OnClickListener viewThreadListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onViewThread(position); - } - } - }; - content.setOnClickListener(viewThreadListener); - container.setOnClickListener(viewThreadListener); - } - - void setupWithStatus(StatusViewData status, final StatusActionListener listener, - boolean mediaPreviewEnabled) { - setDisplayName(status.getUserFullName()); - setUsername(status.getNickname()); - setCreatedAt(status.getCreatedAt()); - setContent(status.getContent(), status.getMentions(), listener); - setAvatar(status.getAvatar(), status.getRebloggedAvatar()); - setReblogged(status.isReblogged()); - setFavourited(status.isFavourited()); - String rebloggedByDisplayName = status.getRebloggedByUsername(); - if (rebloggedByDisplayName == null) { - hideRebloggedByDisplayName(); - } else { - setRebloggedByDisplayName(rebloggedByDisplayName); - } - Status.MediaAttachment[] attachments = status.getAttachments(); - boolean sensitive = status.isSensitive(); - if (mediaPreviewEnabled) { - setMediaPreviews(attachments, sensitive, listener, status.isShowingSensitiveContent()); - /* A status without attachments is sometimes still marked sensitive, so it's necessary - * to check both whether there are any attachments and if it's marked sensitive. */ - if (!sensitive || attachments.length == 0) { - hideSensitiveMediaWarning(); - } - if (attachments.length == 0) { - videoIndicator.setVisibility(View.GONE); - } - // Hide the unused label. - mediaLabel.setVisibility(View.GONE); - } else { - setMediaLabel(attachments, sensitive, listener); - // Hide all unused views. - mediaPreview0.setVisibility(View.GONE); - mediaPreview1.setVisibility(View.GONE); - mediaPreview2.setVisibility(View.GONE); - mediaPreview3.setVisibility(View.GONE); - hideSensitiveMediaWarning(); - videoIndicator.setVisibility(View.GONE); - } - - setupButtons(listener, status.getSenderId()); - setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); - if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) { - hideSpoilerText(); - } else { - setSpoilerText(status.getSpoilerText(), status.isExpanded(), listener); - } - - // I think it's not efficient to create new object every time we bind a holder. - // More efficient approach would be creating View.OnClickListener during holder creation - // and storing StatusActionListener in a variable after binding. - if (rebloggedBar != null) { - rebloggedBar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onOpenReblog(getAdapterPosition()); - } - }); - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java index aa99c66b0..ac59a6e83 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -15,28 +15,16 @@ package com.keylesspalace.tusky.adapter; -import android.content.Context; -import android.preference.PreferenceManager; -import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.text.style.URLSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.util.CustomTabURLSpan; import com.keylesspalace.tusky.viewdata.StatusViewData; -import java.text.DateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; public class ThreadAdapter extends RecyclerView.Adapter { @@ -155,60 +143,4 @@ public class ThreadAdapter extends RecyclerView.Adapter { detailedStatusPosition = position; } } - - private static class StatusDetailedViewHolder extends StatusViewHolder { - private TextView reblogs; - private TextView favourites; - private TextView application; - - StatusDetailedViewHolder(View view) { - super(view); - reblogs = (TextView) view.findViewById(R.id.status_reblogs); - favourites = (TextView) view.findViewById(R.id.status_favourites); - application = (TextView) view.findViewById(R.id.status_application); - } - - @Override - protected void setCreatedAt(@Nullable Date createdAt) { - if (createdAt != null) { - DateFormat dateFormat = android.text.format.DateFormat.getMediumDateFormat( - timestamp.getContext()); - timestamp.setText(dateFormat.format(createdAt)); - } else { - timestamp.setText(""); - } - } - - private void setApplication(@Nullable Status.Application app) { - if (app == null) { - return; - } - if (app.website != null) { - URLSpan span; - Context context = application.getContext(); - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean("customTabs", true); - if (useCustomTabs) { - span = new CustomTabURLSpan(app.website); - } else { - span = new URLSpan(app.website); - } - SpannableStringBuilder text = new SpannableStringBuilder(app.name); - text.setSpan(span, 0, app.name.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - application.setText(text); - application.setMovementMethod(LinkMovementMethod.getInstance()); - } else { - application.setText(app.name); - } - } - - @Override - void setupWithStatus(StatusViewData status, final StatusActionListener listener, - boolean mediaPreviewEnabled) { - super.setupWithStatus(status, listener, mediaPreviewEnabled); - reblogs.setText(status.getReblogsCount()); - favourites.setText(status.getFavouritesCount()); - setApplication(status.getApplication()); - } - } }