From cd74ebb6fc823300dbe4e63f36639e14039948b9 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Thu, 3 Aug 2017 00:29:31 -0400 Subject: [PATCH] Makes the main status of a thread appear as a more detailed view. --- .../tusky/adapter/StatusViewHolder.java | 53 +-- .../tusky/adapter/ThreadAdapter.java | 128 ++++++- .../keylesspalace/tusky/entity/Status.java | 7 + .../tusky/fragment/ViewThreadFragment.java | 3 + .../tusky/util/CustomTabURLSpan.java | 4 +- .../tusky/util/ViewDataUtils.java | 3 + .../tusky/viewdata/StatusViewData.java | 49 ++- app/src/main/res/layout/item_status.xml | 6 +- .../main/res/layout/item_status_detailed.xml | 350 ++++++++++++++++++ 9 files changed, 565 insertions(+), 38 deletions(-) create mode 100644 app/src/main/res/layout/item_status_detailed.xml 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 05d70061..991c5598 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -51,7 +51,6 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { private View container; private TextView displayName; private TextView username; - private TextView sinceCreated; private TextView content; private ImageView avatar; private ImageView avatarReblog; @@ -74,12 +73,14 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { 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); - sinceCreated = (TextView) itemView.findViewById(R.id.status_since_created); + 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); @@ -147,19 +148,21 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { .into(avatar); } - if (hasReblog) { - avatarReblog.setVisibility(View.VISIBLE); - Picasso.with(context) - .load(rebloggedUrl) - .fit() - .transform(new RoundedTransformation(7, 0)) - .into(avatarReblog); - } else { - avatarReblog.setVisibility(View.GONE); + 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); + } } } - private void setCreatedAt(@Nullable Date createdAt) { + 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" @@ -168,7 +171,7 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { if (createdAt != null) { long then = createdAt.getTime(); long now = new Date().getTime(); - readout = DateUtils.getRelativeTimeSpanString(sinceCreated.getContext(), then, now); + 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); @@ -177,11 +180,14 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { readout = "?m"; readoutAloud = "? minutes"; } - sinceCreated.setText(readout); - sinceCreated.setContentDescription(readoutAloud); + timestamp.setText(readout); + timestamp.setContentDescription(readoutAloud); } 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); @@ -190,6 +196,9 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { } private void hideRebloggedByDisplayName() { + if (rebloggedBar == null) { + return; + } rebloggedBar.setVisibility(View.GONE); } @@ -529,11 +538,13 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { // 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()); - } - }); + 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 7dcc2c07..aa99c66b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -15,42 +15,83 @@ 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 { + private static final int VIEW_TYPE_STATUS = 0; + private static final int VIEW_TYPE_STATUS_DETAILED = 1; + private List statuses; private StatusActionListener statusActionListener; private boolean mediaPreviewEnabled; + private int detailedStatusPosition; public ThreadAdapter(StatusActionListener listener) { this.statusActionListener = listener; this.statuses = new ArrayList<>(); mediaPreviewEnabled = true; + detailedStatusPosition = RecyclerView.NO_POSITION; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_status, parent, false); - return new StatusViewHolder(view); + switch (viewType) { + default: + case VIEW_TYPE_STATUS: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_STATUS_DETAILED: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status_detailed, parent, false); + return new StatusDetailedViewHolder(view); + } + } } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { - StatusViewHolder holder = (StatusViewHolder) viewHolder; - StatusViewData status = statuses.get(position); - holder.setupWithStatus(status, - statusActionListener, mediaPreviewEnabled); + if (position == detailedStatusPosition) { + StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder; + StatusViewData status = statuses.get(position); + holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled); + } else { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + StatusViewData status = statuses.get(position); + holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled); + } + } + + @Override + public int getItemViewType(int position) { + if (position == detailedStatusPosition) { + return VIEW_TYPE_STATUS_DETAILED; + } else { + return VIEW_TYPE_STATUS; + } } @Override @@ -72,6 +113,7 @@ public class ThreadAdapter extends RecyclerView.Adapter { public void clearItems() { int oldSize = statuses.size(); statuses.clear(); + detailedStatusPosition = RecyclerView.NO_POSITION; notifyItemRangeRemoved(0, oldSize); } @@ -88,15 +130,85 @@ public class ThreadAdapter extends RecyclerView.Adapter { public void clear() { statuses.clear(); + detailedStatusPosition = RecyclerView.NO_POSITION; notifyDataSetChanged(); } public void setItem(int position, StatusViewData status, boolean notifyAdapter) { statuses.set(position, status); - if (notifyAdapter) notifyItemChanged(position); + if (notifyAdapter) { + notifyItemChanged(position); + } } public void setMediaPreviewEnabled(boolean enabled) { mediaPreviewEnabled = enabled; } + + public void setDetailedStatusPosition(int position) { + if (position != detailedStatusPosition + && detailedStatusPosition != RecyclerView.NO_POSITION) { + int prior = detailedStatusPosition; + detailedStatusPosition = position; + notifyItemChanged(prior); + } else { + 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()); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index 8fe10681..e9bb9ff1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -100,6 +100,8 @@ public class Status { public Mention[] mentions; + public Application application; + public static final int MAX_MEDIA_ATTACHMENTS = 4; @Override @@ -172,4 +174,9 @@ public class Status { @SerializedName("username") public String localUsername; } + + public static class Application { + public String name; + public String website; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index f41df0a7..83dc6504 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -270,6 +270,7 @@ public class ViewThreadFragment extends SFragment implements } } statusIndex = statuses.indexOf(status); + adapter.setDetailedStatusPosition(statusIndex); adapter.setStatuses(statuses.getPairedCopy()); } @@ -344,6 +345,7 @@ public class ViewThreadFragment extends SFragment implements } int i = statusIndex; statuses.add(i, status); + adapter.setDetailedStatusPosition(i); adapter.addItem(i, statuses.getPairedItem(i)); return i; } @@ -362,6 +364,7 @@ public class ViewThreadFragment extends SFragment implements // Insert newly fetched ancestors statusIndex = ancestors.size(); + adapter.setDetailedStatusPosition(statusIndex); statuses.addAll(0, ancestors); List ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex); if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java index 718fd826..0be8a978 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java @@ -14,8 +14,8 @@ import android.view.View; import com.keylesspalace.tusky.R; -class CustomTabURLSpan extends URLSpan { - CustomTabURLSpan(String url) { +public class CustomTabURLSpan extends URLSpan { + public CustomTabURLSpan(String url) { super(url); } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 7b9c0f7b..d294f3ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -26,6 +26,8 @@ public final class ViewDataUtils { .setAvatar(visibleStatus.account.avatar) .setContent(visibleStatus.content) .setCreatedAt(visibleStatus.createdAt) + .setReblogsCount(visibleStatus.reblogsCount) + .setFavouritesCount(visibleStatus.favouritesCount) .setFavourited(visibleStatus.favourited) .setReblogged(visibleStatus.reblogged) .setIsExpanded(false) @@ -40,6 +42,7 @@ public final class ViewDataUtils { .setVisibility(visibleStatus.visibility) .setSenderId(visibleStatus.account.id) .setRebloggingEnabled(visibleStatus.rebloggingAllowed()) + .setApplication(visibleStatus.application) .createStatusViewData(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index 87ff129c..767095a4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -31,19 +31,23 @@ public final class StatusViewData { private final String nickname; private final String avatar; private final Date createdAt; + private final String reblogsCount; + private final String favouritesCount; // I would rather have something else but it would be too much of a rewrite @Nullable private final Status.Mention[] mentions; private final String senderId; private final boolean rebloggingEnabled; + private final Status.Application application; public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited, String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments, String rebloggedByUsername, String rebloggedAvatar, boolean sensitive, boolean isExpanded, boolean isShowingSensitiveWarning, String userFullName, String nickname, - String avatar, Date createdAt, Status.Mention[] mentions, - String senderId, boolean rebloggingEnabled) { + String avatar, Date createdAt, String reblogsCount, + String favouritesCount, Status.Mention[] mentions, String senderId, + boolean rebloggingEnabled, Status.Application application) { this.id = id; this.content = content; this.reblogged = reblogged; @@ -60,9 +64,12 @@ public final class StatusViewData { this.nickname = nickname; this.avatar = avatar; this.createdAt = createdAt; + this.reblogsCount = reblogsCount; + this.favouritesCount = favouritesCount; this.mentions = mentions; this.senderId = senderId; this.rebloggingEnabled = rebloggingEnabled; + this.application = application; } public String getId() { @@ -132,6 +139,14 @@ public final class StatusViewData { return createdAt; } + public String getReblogsCount() { + return reblogsCount; + } + + public String getFavouritesCount() { + return favouritesCount; + } + public String getSenderId() { return senderId; } @@ -145,6 +160,10 @@ public final class StatusViewData { return mentions; } + public Status.Application getApplication() { + return application; + } + public static class Builder { private String id; private Spanned content; @@ -162,9 +181,12 @@ public final class StatusViewData { private String nickname; private String avatar; private Date createdAt; + private String reblogsCount; + private String favouritesCount; private Status.Mention[] mentions; private String senderId; private boolean rebloggingEnabled; + private Status.Application application; public Builder() { } @@ -186,9 +208,12 @@ public final class StatusViewData { nickname = viewData.nickname; avatar = viewData.avatar; createdAt = new Date(viewData.createdAt.getTime()); + reblogsCount = viewData.reblogsCount; + favouritesCount = viewData.favouritesCount; mentions = viewData.mentions == null ? null : viewData.mentions.clone(); senderId = viewData.senderId; rebloggingEnabled = viewData.rebloggingEnabled; + application = viewData.application; } public Builder setId(String id) { @@ -271,6 +296,16 @@ public final class StatusViewData { return this; } + public Builder setReblogsCount(String reblogsCount) { + this.reblogsCount = reblogsCount; + return this; + } + + public Builder setFavouritesCount(String favouritesCount) { + this.favouritesCount = favouritesCount; + return this; + } + public Builder setMentions(Status.Mention[] mentions) { this.mentions = mentions; return this; @@ -286,11 +321,17 @@ public final class StatusViewData { return this; } + public Builder setApplication(Status.Application application) { + this.application = application; + return this; + } + public StatusViewData createStatusViewData() { return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, - isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, mentions, - senderId, rebloggingEnabled); + isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, + reblogsCount, favouritesCount, mentions, senderId, rebloggingEnabled, + application); } } } diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 92105895..6d9d52e2 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -77,7 +77,7 @@ android:paddingTop="@dimen/status_avatar_padding"> + android:layout_toStartOf="@id/status_timestamp" + android:layout_toLeftOf="@id/status_timestamp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file