package org.edx.mobile.view.adapters; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.widget.TextViewCompat; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import android.widget.TextView; import com.google.inject.Inject; import com.joanzapata.iconify.Icon; import com.joanzapata.iconify.IconDrawable; import com.joanzapata.iconify.fonts.FontAwesomeIcons; import org.edx.mobile.R; import org.edx.mobile.discussion.DiscussionComment; import org.edx.mobile.discussion.DiscussionService; import org.edx.mobile.discussion.DiscussionService.FlagBody; import org.edx.mobile.discussion.DiscussionService.FollowBody; import org.edx.mobile.discussion.DiscussionService.VoteBody; import org.edx.mobile.discussion.DiscussionTextUtils; import org.edx.mobile.discussion.DiscussionThread; import org.edx.mobile.discussion.DiscussionThreadUpdatedEvent; import org.edx.mobile.http.CallTrigger; import org.edx.mobile.http.ErrorHandlingCallback; import org.edx.mobile.module.prefs.LoginPrefs; import org.edx.mobile.util.Config; import org.edx.mobile.util.ResourceUtil; import org.edx.mobile.util.UiUtil; import org.edx.mobile.view.common.TaskProgressCallback; import org.edx.mobile.view.view_holders.AuthorLayoutViewHolder; import org.edx.mobile.view.view_holders.DiscussionSocialLayoutViewHolder; import org.edx.mobile.view.view_holders.NumberResponsesViewHolder; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import de.greenrobot.event.EventBus; import roboguice.RoboGuice; public class CourseDiscussionResponsesAdapter extends RecyclerView.Adapter implements InfiniteScrollUtils.ListContentController<DiscussionComment> { public interface Listener { void onClickAuthor(@NonNull String username); void onClickAddComment(@NonNull DiscussionComment comment); void onClickViewComments(@NonNull DiscussionComment comment); } @Inject private Config config; @Inject private DiscussionService discussionService; @Inject private LoginPrefs loginPrefs; @NonNull private final Context context; @NonNull private final Listener listener; @NonNull private DiscussionThread discussionThread; private final List<DiscussionComment> discussionResponses = new ArrayList<>(); private boolean progressVisible = false; // Record the current time at initialization to keep the display of the elapsed time durations stable. private long initialTimeStampMs = System.currentTimeMillis(); static class RowType { static final int THREAD = 0; static final int RESPONSE = 1; static final int PROGRESS = 2; } public CourseDiscussionResponsesAdapter(@NonNull Context context, @NonNull Listener listener, @NonNull DiscussionThread discussionThread) { this.context = context; this.discussionThread = discussionThread; this.listener = listener; RoboGuice.getInjector(context).injectMembers(this); } @Override public void setProgressVisible(boolean visible) { if (progressVisible != visible) { progressVisible = visible; int progressRowIndex = 1 + discussionResponses.size(); if (visible) { notifyItemInserted(progressRowIndex); } else { notifyItemRemoved(progressRowIndex); } } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == RowType.THREAD) { View discussionThreadRow = LayoutInflater. from(parent.getContext()). inflate(R.layout.row_discussion_responses_thread, parent, false); return new DiscussionThreadViewHolder(discussionThreadRow); } if (viewType == RowType.PROGRESS) { return new LoadingViewHolder(parent); } View discussionResponseRow = LayoutInflater. from(parent.getContext()). inflate(R.layout.row_discussion_responses_response, parent, false); // CardView adds extra padding on pre-lollipop devices for shadows // Since, we've set cardUseCompatPadding to true in the layout file // so we need to deduct the extra padding from margins in any case to get the desired results UiUtil.adjustCardViewMargins(discussionResponseRow); return new DiscussionResponseViewHolder(discussionResponseRow); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { int rowType = getItemViewType(position); switch (rowType) { case RowType.THREAD: bindViewHolderToThreadRow((DiscussionThreadViewHolder) holder); break; case RowType.RESPONSE: bindViewHolderToResponseRow((DiscussionResponseViewHolder) holder, position); break; case RowType.PROGRESS: bindViewHolderToShowMoreRow((LoadingViewHolder) holder); break; } } private void bindViewHolderToThreadRow(DiscussionThreadViewHolder holder) { holder.authorLayoutViewHolder.populateViewHolder(config, discussionThread, discussionThread, initialTimeStampMs, new Runnable() { @Override public void run() { listener.onClickAuthor(discussionThread.getAuthor()); } }); holder.threadTitleTextView.setText(discussionThread.getTitle()); DiscussionTextUtils.renderHtml(holder.threadBodyTextView, discussionThread.getRenderedBody()); String groupName = discussionThread.getGroupName(); if (groupName == null) { holder.threadVisibilityTextView.setText(R.string.discussion_post_visibility_everyone); } else { holder.threadVisibilityTextView.setText(ResourceUtil.getFormattedString( context.getResources(), R.string.discussion_post_visibility_cohort, "cohort", groupName)); } bindNumberResponsesView(holder.numberResponsesViewHolder); if (TextUtils.equals(loginPrefs.getUsername(), discussionThread.getAuthor())) { holder.actionsBar.setVisibility(View.GONE); } else { holder.actionsBar.setVisibility(View.VISIBLE); bindSocialView(holder.socialLayoutViewHolder, discussionThread); holder.discussionReportViewHolder.reportLayout.setOnClickListener(new View.OnClickListener() { public void onClick(final View v) { discussionService.setThreadFlagged(discussionThread.getIdentifier(), new FlagBody(!discussionThread.isAbuseFlagged())) .enqueue(new ErrorHandlingCallback<DiscussionThread>( context, CallTrigger.USER_ACTION, (TaskProgressCallback) null) { @Override protected void onResponse(@NonNull final DiscussionThread topicThread) { discussionThread = discussionThread.patchObject(topicThread); notifyItemChanged(0); EventBus.getDefault().post(new DiscussionThreadUpdatedEvent(discussionThread)); } }); } }); holder.discussionReportViewHolder.setReported(discussionThread.isAbuseFlagged()); } } private void bindSocialView(DiscussionSocialLayoutViewHolder holder, DiscussionThread thread) { holder.setDiscussionThread(thread); holder.voteViewContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { discussionService.setThreadVoted(discussionThread.getIdentifier(), new VoteBody(!discussionThread.isVoted())) .enqueue(new ErrorHandlingCallback<DiscussionThread>( context, CallTrigger.USER_ACTION, (TaskProgressCallback) null) { @Override protected void onResponse(@NonNull final DiscussionThread updatedDiscussionThread) { discussionThread = discussionThread.patchObject(updatedDiscussionThread); notifyItemChanged(0); EventBus.getDefault().post(new DiscussionThreadUpdatedEvent(discussionThread)); } }); } }); holder.threadFollowContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { discussionService.setThreadFollowed(discussionThread.getIdentifier(), new FollowBody(!discussionThread.isFollowing())) .enqueue(new ErrorHandlingCallback<DiscussionThread>( context, CallTrigger.USER_ACTION, (TaskProgressCallback) null) { @Override protected void onResponse(@NonNull final DiscussionThread updatedDiscussionThread) { discussionThread = discussionThread.patchObject(updatedDiscussionThread); notifyItemChanged(0); EventBus.getDefault().post(new DiscussionThreadUpdatedEvent(discussionThread)); } }); } }); } private void bindNumberResponsesView(NumberResponsesViewHolder holder) { int responsesCount = discussionThread.getResponseCount(); if (responsesCount < 0) { // The responses count is not available yet, so hide the view. holder.numberResponsesOrCommentsLabel.setVisibility(View.GONE); } else { holder.numberResponsesOrCommentsLabel.setVisibility(View.VISIBLE); holder.numberResponsesOrCommentsLabel.setText(holder.numberResponsesOrCommentsLabel.getResources().getQuantityString( R.plurals.number_responses_or_comments_responses_label, responsesCount, responsesCount)); } } private void bindViewHolderToShowMoreRow(LoadingViewHolder holder) { } private void bindViewHolderToResponseRow(DiscussionResponseViewHolder holder, final int position) { final DiscussionComment comment = discussionResponses.get(position - 1); // Subtract 1 for the discussion thread row at position 0 holder.authorLayoutViewHolder.populateViewHolder(config, comment, comment, initialTimeStampMs, new Runnable() { @Override public void run() { listener.onClickAuthor(comment.getAuthor()); } }); if (comment.isEndorsed()) { holder.authorLayoutViewHolder.answerTextView.setVisibility(View.VISIBLE); holder.responseAnswerAuthorTextView.setVisibility(View.VISIBLE); DiscussionThread.ThreadType threadType = discussionThread.getType(); DiscussionTextUtils.AuthorAttributionLabel authorAttributionLabel; @StringRes int endorsementTypeStringRes; switch (threadType) { case QUESTION: authorAttributionLabel = DiscussionTextUtils.AuthorAttributionLabel.ANSWER; endorsementTypeStringRes = R.string.discussion_responses_answer; break; case DISCUSSION: default: authorAttributionLabel = DiscussionTextUtils.AuthorAttributionLabel.ENDORSEMENT; endorsementTypeStringRes = R.string.discussion_responses_endorsed; break; } holder.authorLayoutViewHolder.answerTextView.setText(endorsementTypeStringRes); DiscussionTextUtils.setAuthorAttributionText(holder.responseAnswerAuthorTextView, authorAttributionLabel, comment.getEndorserData(), initialTimeStampMs, new Runnable() { @Override public void run() { listener.onClickAuthor(comment.getEndorsedBy()); } }); } else { holder.authorLayoutViewHolder.answerTextView.setVisibility(View.GONE); holder.responseAnswerAuthorTextView.setVisibility(View.GONE); } DiscussionTextUtils.renderHtml(holder.responseCommentBodyTextView, comment.getRenderedBody()); if (discussionThread.isClosed() && comment.getChildCount() == 0) { holder.addCommentLayout.setEnabled(false); } else { holder.addCommentLayout.setEnabled(true); holder.addCommentLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (comment.getChildCount() > 0) { listener.onClickViewComments(comment); } else { listener.onClickAddComment(comment); } } }); } bindNumberCommentsView(holder.numberResponsesViewHolder, comment); if (TextUtils.equals(loginPrefs.getUsername(), comment.getAuthor())) { holder.actionsBar.setVisibility(View.GONE); } else { holder.actionsBar.setVisibility(View.VISIBLE); bindSocialView(holder.socialLayoutViewHolder, position, comment); holder.discussionReportViewHolder.reportLayout.setOnClickListener(new View.OnClickListener() { public void onClick(final View v) { discussionService.setCommentFlagged(comment.getIdentifier(), new FlagBody(!comment.isAbuseFlagged())) .enqueue(new ErrorHandlingCallback<DiscussionComment>( context, CallTrigger.USER_ACTION, (TaskProgressCallback) null) { @Override protected void onResponse(@NonNull final DiscussionComment comment) { discussionResponses.get(position - 1).patchObject(comment); discussionResponses.set(position - 1, comment); notifyItemChanged(position); } }); } }); holder.discussionReportViewHolder.setReported(comment.isAbuseFlagged()); holder.socialLayoutViewHolder.threadFollowContainer.setVisibility(View.INVISIBLE); } } private void bindSocialView(DiscussionSocialLayoutViewHolder holder, final int position, final DiscussionComment response) { holder.setDiscussionResponse(response); holder.voteViewContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { discussionService.setCommentVoted(response.getIdentifier(), new VoteBody(!response.isVoted())) .enqueue(new ErrorHandlingCallback<DiscussionComment>( context, CallTrigger.USER_ACTION, (TaskProgressCallback) null) { @Override protected void onResponse(@NonNull final DiscussionComment comment) { discussionResponses.get(position - 1).patchObject(comment); discussionResponses.set(position - 1, comment); notifyItemChanged(position); } }); } }); } private void bindNumberCommentsView(NumberResponsesViewHolder holder, DiscussionComment response) { String text; Icon icon; int numChildren = response == null ? 0 : response.getChildCount(); if (response.getChildCount() == 0) { if (discussionThread.isClosed()) { text = context.getString(R.string.discussion_add_comment_disabled_title); icon = FontAwesomeIcons.fa_lock; } else { text = context.getString(R.string.number_responses_or_comments_add_comment_label); icon = FontAwesomeIcons.fa_comment; } } else { text = context.getResources().getQuantityString( R.plurals.number_responses_or_comments_comments_label, numChildren, numChildren); icon = FontAwesomeIcons.fa_comment; } holder.numberResponsesOrCommentsLabel.setText(text); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds( holder.numberResponsesOrCommentsLabel, new IconDrawable(context, icon) .colorRes(context, R.color.edx_brand_gray_base) .sizeRes(context, R.dimen.edx_small), null, null, null); } @Override public int getItemCount() { int total = 1 + discussionResponses.size(); if (progressVisible) total++; return total; } @Override public int getItemViewType(int position) { if (position == 0) { return RowType.THREAD; } if (progressVisible && position == getItemCount() - 1) { return RowType.PROGRESS; } return RowType.RESPONSE; } public void updateDiscussionThread(@NonNull DiscussionThread discussionThread) { this.discussionThread = discussionThread; notifyDataSetChanged(); } @Override public void clear() { int responsesCount = discussionResponses.size(); discussionResponses.clear(); notifyItemRangeRemoved(1, responsesCount); } @Override public void addAll(List<DiscussionComment> items) { int offset = 1 + discussionResponses.size(); discussionResponses.addAll(items); notifyItemRangeInserted(offset, items.size()); } public void addNewResponse(@NonNull DiscussionComment response) { // Since, we have a added a new response we need to update timestamps of all responses initialTimeStampMs = System.currentTimeMillis(); int offset = 1 + discussionResponses.size(); discussionResponses.add(response); incrementResponseCount(); notifyItemInserted(offset); } public void incrementResponseCount() { discussionThread.incrementResponseCount(); notifyItemChanged(0); // Response count is shown in the thread details header, so it also needs to be refreshed. } public void addNewComment(@NonNull DiscussionComment parent) { // Since, we have a added a new comment we need to update timestamps of all responses as well initialTimeStampMs = System.currentTimeMillis(); discussionThread.incrementCommentCount(); String parentId = parent.getIdentifier(); for (ListIterator<DiscussionComment> responseIterator = discussionResponses.listIterator(); responseIterator.hasNext(); ) { DiscussionComment response = responseIterator.next(); if (parentId.equals(response.getIdentifier())) { response.incrementChildCount(); notifyItemChanged(1 + responseIterator.previousIndex()); break; } } } public static class DiscussionThreadViewHolder extends RecyclerView.ViewHolder { View actionsBar; TextView threadTitleTextView; TextView threadBodyTextView; TextView threadVisibilityTextView; AuthorLayoutViewHolder authorLayoutViewHolder; NumberResponsesViewHolder numberResponsesViewHolder; DiscussionSocialLayoutViewHolder socialLayoutViewHolder; DiscussionReportViewHolder discussionReportViewHolder; public DiscussionThreadViewHolder(View itemView) { super(itemView); actionsBar = itemView.findViewById(R.id.discussion_actions_bar); threadTitleTextView = (TextView) itemView. findViewById(R.id.discussion_responses_thread_row_title_text_view); threadBodyTextView = (TextView) itemView. findViewById(R.id.discussion_responses_thread_row_body_text_view); threadVisibilityTextView = (TextView) itemView. findViewById(R.id.discussion_responses_thread_row_visibility_text_view); authorLayoutViewHolder = new AuthorLayoutViewHolder(itemView.findViewById(R.id.discussion_user_profile_row)); numberResponsesViewHolder = new NumberResponsesViewHolder(itemView); socialLayoutViewHolder = new DiscussionSocialLayoutViewHolder(itemView); discussionReportViewHolder = new DiscussionReportViewHolder(itemView); } } public static class DiscussionResponseViewHolder extends RecyclerView.ViewHolder { View actionsBar; RelativeLayout addCommentLayout; TextView responseCommentBodyTextView; TextView responseAnswerAuthorTextView; AuthorLayoutViewHolder authorLayoutViewHolder; NumberResponsesViewHolder numberResponsesViewHolder; DiscussionSocialLayoutViewHolder socialLayoutViewHolder; DiscussionReportViewHolder discussionReportViewHolder; public DiscussionResponseViewHolder(View itemView) { super(itemView); actionsBar = itemView.findViewById(R.id.discussion_actions_bar); addCommentLayout = (RelativeLayout) itemView.findViewById(R.id.discussion_responses_comment_relative_layout); responseCommentBodyTextView = (TextView) itemView.findViewById(R.id.discussion_responses_comment_body_text_view); responseAnswerAuthorTextView = (TextView) itemView.findViewById(R.id.discussion_responses_answer_author_text_view); authorLayoutViewHolder = new AuthorLayoutViewHolder(itemView.findViewById(R.id.discussion_user_profile_row)); numberResponsesViewHolder = new NumberResponsesViewHolder(itemView); socialLayoutViewHolder = new DiscussionSocialLayoutViewHolder(itemView); discussionReportViewHolder = new DiscussionReportViewHolder(itemView); } } }