package org.edx.mobile.view;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.TextViewCompat;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.google.inject.Inject;
import com.joanzapata.iconify.IconDrawable;
import com.joanzapata.iconify.fonts.FontAwesomeIcons;
import org.edx.mobile.R;
import org.edx.mobile.discussion.CourseTopics;
import org.edx.mobile.discussion.DiscussionCommentPostedEvent;
import org.edx.mobile.discussion.DiscussionPostsFilter;
import org.edx.mobile.discussion.DiscussionPostsSort;
import org.edx.mobile.discussion.DiscussionRequestFields;
import org.edx.mobile.discussion.DiscussionService;
import org.edx.mobile.discussion.DiscussionThread;
import org.edx.mobile.discussion.DiscussionThreadPostedEvent;
import org.edx.mobile.discussion.DiscussionThreadUpdatedEvent;
import org.edx.mobile.discussion.DiscussionTopic;
import org.edx.mobile.http.CallTrigger;
import org.edx.mobile.http.ErrorHandlingCallback;
import org.edx.mobile.model.Page;
import org.edx.mobile.view.adapters.DiscussionPostsSpinnerAdapter;
import org.edx.mobile.view.adapters.InfiniteScrollUtils;
import org.edx.mobile.view.common.TaskProgressCallback.ProgressViewController;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import de.greenrobot.event.EventBus;
import retrofit2.Call;
import roboguice.inject.InjectExtra;
import roboguice.inject.InjectView;
public class CourseDiscussionPostsThreadFragment extends CourseDiscussionPostsBaseFragment {
public static final String ARG_DISCUSSION_HAS_TOPIC_NAME = "discussion_has_topic_name";
@InjectView(R.id.spinners_container)
private ViewGroup spinnersContainerLayout;
@InjectView(R.id.discussion_posts_filter_spinner)
private Spinner discussionPostsFilterSpinner;
@InjectView(R.id.discussion_posts_sort_spinner)
private Spinner discussionPostsSortSpinner;
@InjectView(R.id.create_new_item_text_view)
private TextView createNewPostTextView;
@InjectView(R.id.create_new_item_layout)
private ViewGroup createNewPostLayout;
@InjectView(R.id.center_message_box)
private TextView centerMessageBox;
@InjectView(R.id.loading_indicator)
private ProgressBar loadingIndicator;
@InjectExtra(value = Router.EXTRA_DISCUSSION_TOPIC, optional = true)
private DiscussionTopic discussionTopic;
@Inject
private DiscussionService discussionService;
private DiscussionPostsFilter postsFilter = DiscussionPostsFilter.ALL;
private DiscussionPostsSort postsSort = DiscussionPostsSort.LAST_ACTIVITY_AT;
private Call<Page<DiscussionThread>> getThreadListCall;
/**
* Runnable for deferring the fetching of threads for a topic, until we
* have fetched the {@link #discussionTopic} object.
*/
@Nullable
private Runnable populatePostListRunnable;
private enum EmptyQueryResultsFor {
FOLLOWING,
CATEGORY,
COURSE
}
public static CourseDiscussionPostsThreadFragment newInstance(@NonNull String topicId,
@NonNull Serializable courseData,
boolean hasTopicName) {
CourseDiscussionPostsThreadFragment f = new CourseDiscussionPostsThreadFragment();
Bundle args = new Bundle();
args.putString(Router.EXTRA_DISCUSSION_TOPIC_ID, topicId);
args.putSerializable(Router.EXTRA_COURSE_DATA, courseData);
args.putBoolean(ARG_DISCUSSION_HAS_TOPIC_NAME, hasTopicName);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_discussion_thread_posts, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (discussionTopic == null) {
// Hide the button for adding new posts until the topic is loaded.
createNewPostLayout.setVisibility(View.GONE);
// Either we are coming from a deep link or courseware's inline discussion
fetchDiscussionTopic();
} else {
getActivity().setTitle(discussionTopic.getName());
}
createNewPostTextView.setText(R.string.discussion_post_create_new_post);
Context context = getActivity();
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(createNewPostTextView,
new IconDrawable(context, FontAwesomeIcons.fa_plus_circle)
.sizeRes(context, R.dimen.small_icon_size)
.colorRes(context, R.color.white),
null, null, null
);
createNewPostLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
router.showCourseDiscussionAddPost(getActivity(), discussionTopic, courseData);
}
});
discussionPostsFilterSpinner.setAdapter(new DiscussionPostsSpinnerAdapter(
discussionPostsFilterSpinner, DiscussionPostsFilter.values(),
FontAwesomeIcons.fa_filter));
discussionPostsSortSpinner.setAdapter(new DiscussionPostsSpinnerAdapter(
discussionPostsSortSpinner, DiscussionPostsSort.values(),
// Since we can't define IconDrawable in XML resources, we'll have to define
// this constructed dynamically in code. This is far more efficient than the
// alternative option of defining multiple IconView items in the layout.
new DiscussionPostsSpinnerAdapter.IconDrawableFactory() {
@Override
@NonNull
public Drawable createIcon() {
Context context = getActivity();
LayerDrawable layeredIcon = new LayerDrawable(new Drawable[]{
new IconDrawable(context, FontAwesomeIcons.fa_long_arrow_up)
.colorRes(context, R.color.edx_brand_primary_base),
new IconDrawable(context, FontAwesomeIcons.fa_long_arrow_down)
.colorRes(context, R.color.edx_brand_primary_base)
});
Resources resources = context.getResources();
final int width = resources.getDimensionPixelSize(
R.dimen.small_icon_size);
final int verticalPadding = resources.getDimensionPixelSize(
R.dimen.discussion_posts_filter_popup_icon_margin);
final int height = width + verticalPadding;
final float halfWidth = width / 2f;
final int leftIconWidth = (int) Math.ceil(halfWidth);
final int rightIconWidth = (int) halfWidth;
layeredIcon.setLayerInset(0, 0, 0, rightIconWidth, verticalPadding);
layeredIcon.setLayerInset(1, leftIconWidth, verticalPadding, 0, 0);
layeredIcon.setBounds(0, 0, width, height);
return layeredIcon;
}
}));
discussionPostsFilterSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(@NonNull AdapterView<?> parent, @NonNull View view, int position, long id) {
DiscussionPostsFilter selectedPostsFilter =
(DiscussionPostsFilter) parent.getItemAtPosition(position);
if (postsFilter != selectedPostsFilter) {
postsFilter = selectedPostsFilter;
clearListAndLoadFirstPage();
}
}
@Override
public void onNothingSelected(@NonNull AdapterView<?> parent) {
}
});
discussionPostsSortSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(@NonNull AdapterView<?> parent, @NonNull View view, int position, long id) {
DiscussionPostsSort selectedPostsSort =
(DiscussionPostsSort) parent.getItemAtPosition(position);
if (postsSort != selectedPostsSort) {
postsSort = selectedPostsSort;
clearListAndLoadFirstPage();
}
}
@Override
public void onNothingSelected(@NonNull AdapterView<?> parent) {
}
});
}
private void fetchDiscussionTopic() {
String topicId = getArguments().getString(Router.EXTRA_DISCUSSION_TOPIC_ID);
discussionService.getSpecificCourseTopics(courseData.getCourse().getId(),
Collections.singletonList(topicId))
.enqueue(new ErrorHandlingCallback<CourseTopics>(getContext(),
CallTrigger.LOADING_UNCACHED,
new ProgressViewController(loadingIndicator)) {
@Override
protected void onResponse(@NonNull final CourseTopics courseTopics) {
discussionTopic = courseTopics.getCoursewareTopics().get(0).getChildren().get(0);
Activity activity = getActivity();
if (activity != null &&
!getArguments().getBoolean(ARG_DISCUSSION_HAS_TOPIC_NAME)) {
// We only need to set the title here when coming from a deep link
activity.setTitle(discussionTopic.getName());
}
if (getView() != null) {
if (populatePostListRunnable != null) {
populatePostListRunnable.run();
}
// Now that we have the topic data, we can allow the user to add new posts.
createNewPostLayout.setVisibility(View.VISIBLE);
}
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
@SuppressWarnings("unused")
public void onEventMainThread(DiscussionThreadUpdatedEvent event) {
// If a listed thread's following status has changed, we need to replace it to show/hide the "following" label
for (int i = 0; i < discussionPostsAdapter.getCount(); ++i) {
if (discussionPostsAdapter.getItem(i).hasSameId(event.getDiscussionThread())) {
discussionPostsAdapter.replace(event.getDiscussionThread(), i);
break;
}
}
}
@SuppressWarnings("unused")
public void onEventMainThread(DiscussionCommentPostedEvent event) {
// If a new response/comment was posted in a listed thread, we need to update the list
for (int i = 0; i < discussionPostsAdapter.getCount(); ++i) {
final DiscussionThread discussionThread = discussionPostsAdapter.getItem(i);
if (discussionThread.containsComment(event.getComment())) {
// No need to update the discussionThread object because its already updated on
// the responses screen and is shared on both screens, because it's queried via
// a PATCH call in the responses screen to mark it as read, and the response is
// broadcasted on the event bus as a DiscussionThreadUpdatedEvent, which is then
// used to replace the existing model. A better approach may be to not allow
// sharing of the objects in an unpredictable manner by always cloning or copying
// from them, or on the other extreme, having a central memory cache with
// registered observers so that the objects are always shared.
discussionPostsAdapter.notifyDataSetChanged();
break;
}
}
}
@SuppressWarnings("unused")
public void onEventMainThread(DiscussionThreadPostedEvent event) {
DiscussionThread newThread = event.getDiscussionThread();
// If a new post is created in this topic, insert it at the top of the list, after any pinned posts
if (discussionTopic.containsThread(newThread)) {
if (postsFilter == DiscussionPostsFilter.UNANSWERED &&
newThread.getType() != DiscussionThread.ThreadType.QUESTION) {
return;
}
int i = 0;
for (; i < discussionPostsAdapter.getCount(); ++i) {
if (!discussionPostsAdapter.getItem(i).isPinned()) {
break;
}
}
discussionPostsAdapter.insert(newThread, i);
// move the ListView's scroll to that newly added post's position
discussionPostsListView.setSelection(i);
// In case this is the first addition, we need to hide the no-item-view
setScreenStateUponResult();
}
}
@Override
public void loadNextPage(@NonNull final InfiniteScrollUtils.PageLoadCallback<DiscussionThread> callback) {
if (discussionTopic == null) {
populatePostListRunnable = new Runnable() {
@Override
public void run() {
populatePostList(callback);
}
};
} else {
populatePostList(callback);
}
}
private void clearListAndLoadFirstPage() {
nextPage = 1;
discussionPostsListView.setVisibility(View.INVISIBLE);
centerMessageBox.setVisibility(View.GONE);
controller.reset();
}
private void populatePostList(@NonNull final InfiniteScrollUtils.PageLoadCallback<DiscussionThread> callback) {
if (getThreadListCall != null) {
getThreadListCall.cancel();
}
final List<String> requestedFields = Collections.singletonList(
DiscussionRequestFields.PROFILE_IMAGE.getQueryParamValue());
if (!discussionTopic.isFollowingType()) {
getThreadListCall = discussionService.getThreadList(courseData.getCourse().getId(),
getAllTopicIds(), postsFilter.getQueryParamValue(),
postsSort.getQueryParamValue(), nextPage, requestedFields);
} else {
getThreadListCall = discussionService.getFollowingThreadList(
courseData.getCourse().getId(), postsFilter.getQueryParamValue(),
postsSort.getQueryParamValue(), nextPage, requestedFields);
}
getThreadListCall.enqueue(new ErrorHandlingCallback<Page<DiscussionThread>>(getActivity(),
CallTrigger.LOADING_UNCACHED,
// Initially we need to show the spinner at the center of the screen. After that,
// the ListView will start showing a footer-based loading indicator.
nextPage > 1 || callback.isRefreshingSilently() ? null :
new ProgressViewController(loadingIndicator)) {
@Override
protected void onResponse(@NonNull final Page<DiscussionThread> threadsPage) {
if (getView() == null) return;
++nextPage;
callback.onPageLoaded(threadsPage);
if (discussionPostsAdapter.getCount() == 0) {
if (discussionTopic.isAllType()) {
setScreenStateUponError(EmptyQueryResultsFor.COURSE);
} else if (discussionTopic.isFollowingType()) {
setScreenStateUponError(EmptyQueryResultsFor.FOLLOWING);
} else {
setScreenStateUponError(EmptyQueryResultsFor.CATEGORY);
}
} else {
setScreenStateUponResult();
}
}
@Override
public void onFailure(@NonNull final Call<Page<DiscussionThread>> call,
@NonNull final Throwable error) {
if (getView() == null) return;
// Don't display any error message if we're doing a silent
// refresh, as that would be confusing to the user.
if (!callback.isRefreshingSilently()) {
super.onFailure(call, error);
}
callback.onError();
nextPage = 1;
}
});
}
private void setScreenStateUponError(@NonNull EmptyQueryResultsFor query) {
String resultsText = "";
boolean isAllPostsFilter = (postsFilter == DiscussionPostsFilter.ALL);
switch (query) {
case FOLLOWING:
if (!isAllPostsFilter) {
resultsText = getString(R.string.forum_no_results_for_filtered_following);
} else {
resultsText = getString(R.string.forum_no_results_for_following);
}
break;
case CATEGORY:
resultsText = getString(R.string.forum_no_results_in_category);
break;
case COURSE:
resultsText = getString(R.string.forum_no_results_for_all_posts);
break;
}
if (!isAllPostsFilter) {
resultsText += " " + getString(R.string.forum_no_results_with_filter);
spinnersContainerLayout.setVisibility(View.VISIBLE);
} else {
spinnersContainerLayout.setVisibility(View.GONE);
}
centerMessageBox.setText(resultsText);
centerMessageBox.setVisibility(View.VISIBLE);
discussionPostsListView.setVisibility(View.INVISIBLE);
}
private void setScreenStateUponResult() {
centerMessageBox.setVisibility(View.GONE);
spinnersContainerLayout.setVisibility(View.VISIBLE);
discussionPostsListView.setVisibility(View.VISIBLE);
}
@NonNull
public List<String> getAllTopicIds() {
if (discussionTopic.isAllType()) {
return Collections.EMPTY_LIST;
} else {
final List<String> ids = new ArrayList<>();
appendTopicIds(discussionTopic, ids);
return ids;
}
}
private void appendTopicIds(@NonNull DiscussionTopic dTopic, @NonNull List<String> ids) {
String id = dTopic.getIdentifier();
if (!TextUtils.isEmpty(id)) {
ids.add(id);
}
for (DiscussionTopic child : dTopic.getChildren()) {
appendTopicIds(child, ids);
}
}
}