package com.manuelmaly.hn; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.DataSetObserver; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcelable; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.text.Html; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.manuelmaly.hn.login.LoginActivity_; import com.manuelmaly.hn.model.HNComment; import com.manuelmaly.hn.model.HNCommentTreeNode; import com.manuelmaly.hn.model.HNPost; import com.manuelmaly.hn.model.HNPostComments; import com.manuelmaly.hn.reuse.LinkifiedTextView; import com.manuelmaly.hn.task.HNPostCommentsTask; import com.manuelmaly.hn.task.HNVoteTask; import com.manuelmaly.hn.task.ITaskFinishedHandler; import com.manuelmaly.hn.util.DisplayHelper; import com.manuelmaly.hn.util.FileUtil; import com.manuelmaly.hn.util.FontHelper; import com.manuelmaly.hn.util.SpotlightActivity; import com.manuelmaly.hn.util.ViewedUtils; import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.EActivity; import org.androidannotations.annotations.SystemService; import org.androidannotations.annotations.ViewById; import java.util.ArrayList; import java.util.HashSet; @EActivity(R.layout.comments_activity) public class CommentsActivity extends BaseListActivity implements ITaskFinishedHandler<HNPostComments> { public static final String EXTRA_HNPOST = "HNPOST"; private static final int TASKCODE_VOTE = 100; private static final int ACTIVITY_LOGIN = 136; private static final int ACTIVITY_SPOTLIGHT = 137; @ViewById(R.id.comments_list) ListView mCommentsList; @ViewById(R.id.comments_root) LinearLayout mRootView; @ViewById(R.id.comments_swiperefreshlayout) SwipeRefreshLayout mSwipeRefreshLayout; @SystemService LayoutInflater mInflater; LinearLayout mCommentHeader; TextView mCommentHeaderText; TextView mEmptyView; TextView mActionbarTitle; HNPost mPost; HNPostComments mComments; CommentsAdapter mCommentsListAdapter; boolean mHaveLoadedPosts = false; String mCurrentFontSize = null; int mFontSizeText; int mFontSizeMetadata; int mCommentLevelIndentPx; HashSet<HNComment> mUpvotedComments; private static final String LIST_STATE = "listState"; private Parcelable mListState = null; HNComment mPendingVote; HashSet<HNComment> mVotedComments; boolean mShouldShowRefreshing = false; @AfterViews public void init() { mPost = (HNPost) getIntent().getSerializableExtra(EXTRA_HNPOST); if (mPost == null || mPost.getPostID() == null) { Toast.makeText(this, "The belonging post has not been loaded", Toast.LENGTH_LONG).show(); finish(); return; } mCommentLevelIndentPx = Math.min(DisplayHelper.getScreenHeight(this), DisplayHelper.getScreenWidth(this)) / 30; initCommentsHeader(); mComments = new HNPostComments(); mVotedComments = new HashSet<HNComment>(); mCommentsListAdapter = new CommentsAdapter(); mEmptyView = getEmptyTextView(mRootView); mCommentsList.setEmptyView(mEmptyView); // Add the header for "Ask HN" text. If there is no text, this will just // be empty mCommentsList.addHeaderView(mCommentHeader, null, false); mCommentsList.setAdapter(mCommentsListAdapter); mActionbarTitle = (TextView) getSupportActionBar().getCustomView() .findViewById(R.id.actionbar_title); mActionbarTitle.setTypeface(FontHelper.getComfortaa(this, true)); mActionbarTitle.setText(getString(R.string.comments)); mActionbarTitle.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (Settings.getHtmlViewer(CommentsActivity.this).equals( getString(R.string.pref_htmlviewer_browser))) { String articleURL = ArticleReaderActivity .getArticleViewURL(mPost, Settings .getHtmlProvider(CommentsActivity.this), CommentsActivity.this); MainActivity.openURLInBrowser(articleURL, CommentsActivity.this); } else { openArticleReader(); } } }); toggleSwipeRefreshLayout(); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { startFeedLoading(); } }); loadIntermediateCommentsFromStore(); startFeedLoading(); } @Override protected void onResume() { super.onResume(); // refresh if font size changed if (refreshFontSizes()) { mCommentsListAdapter.notifyDataSetChanged(); } // restore vertical scrolling position if applicable if (mListState != null) { mCommentsList.onRestoreInstanceState(mListState); } mListState = null; // Only show the spotlight effect the first time if (!ViewedUtils.getActivityViewed(this)) { Handler handler = new Handler(Looper.getMainLooper()); // If we don't delay this there are weird race conditions handler.postDelayed(new Runnable() { @Override public void run() { showCommentsSpotlight(); ViewedUtils.setActivityViewed(CommentsActivity.this); } }, 250); } // User may have toggled pull-down refresh, so toggle the SwipeRefreshLayout. toggleSwipeRefreshLayout(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_share_refresh, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuItem refreshItem = menu.findItem(R.id.menu_refresh); if (mShouldShowRefreshing) { View refreshView = mInflater.inflate(R.layout.refresh_icon, null); MenuItemCompat.setActionView(refreshItem, refreshView); } else { MenuItemCompat.setActionView(refreshItem, null); } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: startFeedLoading(); return true; case android.R.id.home: finish(); return true; case R.id.menu_share: Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, mPost.getTitle() + " | Hacker News"); shareIntent .putExtra( Intent.EXTRA_TEXT, "https://news.ycombinator.com/item?id=" + mPost.getPostID()); startActivity(Intent.createChooser(shareIntent, getString(R.string.share_comments_url))); default: return super.onOptionsItemSelected(item); } } private void toggleSwipeRefreshLayout() { mSwipeRefreshLayout.setEnabled(Settings.isPullDownRefresh(CommentsActivity.this)); } @Override public void onTaskFinished(int taskCode, TaskResultCode code, HNPostComments result, Object tag) { if (code.equals(TaskResultCode.Success) && mCommentsListAdapter != null) { showComments(result); } else if (!code.equals(TaskResultCode.Success)) { Toast.makeText(this, getString(R.string.error_unable_to_retrieve_comments), Toast.LENGTH_SHORT).show(); } updateEmptyView(); setShowRefreshing(false); } private void showComments(HNPostComments comments) { if (comments.getHeaderHtml() != null && mCommentHeaderText.getVisibility() != View.VISIBLE) { mCommentHeaderText.setVisibility(View.VISIBLE); // We trim it here to get rid of pesky newlines that come from // closing <p> tags mCommentHeaderText.setText(Html.fromHtml(comments.getHeaderHtml()) .toString().trim()); // Linkify.ALL does some highlighting where we don't want it // (i.e if you just put certain tlds in) so we use this custom // regex. Linkify.addLinks(mCommentHeaderText, Linkify.WEB_URLS); // } mComments = comments; mCommentsListAdapter.notifyDataSetChanged(); } private void loadIntermediateCommentsFromStore() { new GetLastHNPostCommentsTask().execute(mPost.getPostID()); } class GetLastHNPostCommentsTask extends FileUtil.GetLastHNPostCommentsTask { @Override protected void onPostExecute(HNPostComments result) { boolean registeredUserChanged = result != null && result.getUserAcquiredFor() != null && (!result.getUserAcquiredFor().equals( Settings.getUserName(CommentsActivity.this))); // Only show comments if we last fetched them for the current user // and we have comments if (!registeredUserChanged && result != null) { showComments(result); } else { updateEmptyView(); } } } private void startFeedLoading() { mHaveLoadedPosts = false; setShowRefreshing(true); HNPostCommentsTask.startOrReattach(this, this, mPost.getPostID(), 0); } private boolean refreshFontSizes() { final String fontSize = Settings.getFontSize(this); if ((mCurrentFontSize == null) || (!mCurrentFontSize.equals(fontSize))) { mCurrentFontSize = fontSize; if (fontSize.equals(getString(R.string.pref_fontsize_small))) { mFontSizeText = 14; mFontSizeMetadata = 12; } else if (fontSize.equals(getString(R.string.pref_fontsize_normal))) { mFontSizeText = 16; mFontSizeMetadata = 14; } else { mFontSizeText = 20; mFontSizeMetadata = 18; } return true; } return false; } private void vote(String voteURL, HNComment comment) { HNVoteTask.start(voteURL, this, new VoteTaskFinishedHandler(), TASKCODE_VOTE, comment); } @Override protected void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); mListState = state.getParcelable(LIST_STATE); } @Override protected void onSaveInstanceState(Bundle state) { super.onSaveInstanceState(state); mListState = mCommentsList.onSaveInstanceState(); state.putParcelable(LIST_STATE, mListState); } private void updateEmptyView() { if (mHaveLoadedPosts) { mEmptyView.setText(getString(R.string.no_comments)); } mHaveLoadedPosts = true; } private void showCommentsSpotlight() { int[] posArray = new int[2]; mActionbarTitle.getLocationInWindow(posArray); Intent intent = SpotlightActivity.intentForSpotlightActivity( CommentsActivity.this, posArray[0], mActionbarTitle.getWidth(), 0, getSupportActionBar().getHeight(), getString(R.string.click_on_comments)); startActivityForResult(intent, ACTIVITY_SPOTLIGHT); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); } private void openArticleReader() { Intent intent = new Intent(this, ArticleReaderActivity_.class); intent.putExtra(CommentsActivity.EXTRA_HNPOST, mPost); if (getIntent().getStringExtra( ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE) != null) { intent.putExtra( ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE, getIntent().getStringExtra( ArticleReaderActivity.EXTRA_HTMLPROVIDER_OVERRIDE)); } startActivity(intent); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); finish(); } private void initCommentsHeader() { // Don't worry about reallocating this stuff it has already been called if (mCommentHeader == null) { mCommentHeader = new LinearLayout(this); mCommentHeader.setOrientation(LinearLayout.VERTICAL); mCommentHeaderText = new TextView(this); mCommentHeader.addView(mCommentHeaderText); // Division by 2 just gave the right feel, I'm unsure how well it // will work across platforms mCommentHeaderText.setPadding(mCommentLevelIndentPx, mCommentLevelIndentPx / 2, mCommentLevelIndentPx / 2, mCommentLevelIndentPx / 2); mCommentHeaderText.setTextColor(getResources().getColor( R.color.gray_comments_information)); // Make it look like the header is just another list item. View v = new View(this); v.setBackgroundColor(getResources().getColor( R.color.gray_comments_divider)); v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1)); mCommentHeader.addView(v); mCommentHeaderText.setVisibility(View.GONE); } } private void setShowRefreshing(boolean showRefreshing) { if (!Settings.isPullDownRefresh(CommentsActivity.this)) { mShouldShowRefreshing = showRefreshing; supportInvalidateOptionsMenu(); } if (mSwipeRefreshLayout.isEnabled() && (!mSwipeRefreshLayout.isRefreshing() || !showRefreshing)) { mSwipeRefreshLayout.setRefreshing(showRefreshing); } } private class LongPressMenuListAdapter implements ListAdapter, DialogInterface.OnClickListener { HNComment mComment; boolean mIsLoggedIn; boolean mUpVotingEnabled; boolean mDownVotingEnabled; ArrayList<CharSequence> mItems; public LongPressMenuListAdapter(HNComment comment) { mComment = comment; mIsLoggedIn = Settings.isUserLoggedIn(CommentsActivity.this); mUpVotingEnabled = !mIsLoggedIn || (mComment.getUpvoteUrl(Settings .getUserName(CommentsActivity.this)) != null && !mVotedComments .contains(mComment)); mDownVotingEnabled = mIsLoggedIn && (mComment.getDownvoteUrl(Settings .getUserName(CommentsActivity.this)) != null && !mVotedComments .contains(mComments)); mItems = new ArrayList<CharSequence>(); // Figure out why this is false if (mUpVotingEnabled) { mItems.add(getString(R.string.upvote)); } if (mDownVotingEnabled) { mItems.add(getString(R.string.downvote)); } if (!mUpVotingEnabled && !mDownVotingEnabled) { mItems.add(getString(R.string.already_voted_on)); } if (comment.getTreeNode().isExpanded()) { mItems.add(getString(R.string.collapse_comment)); } else { mItems.add(getString(R.string.expand_comment)); } if (comment.getTreeNode().getParent() != null){ mItems.add(getString(R.string.collapse_thread)); } } @Override public int getCount() { return mItems.size(); } @Override public CharSequence getItem(int position) { return mItems.get(position); } @Override public long getItemId(int position) { return 0; } @Override public int getItemViewType(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { TextView view = (TextView) mInflater.inflate( android.R.layout.simple_list_item_1, null); view.setText(getItem(position)); if (!mUpVotingEnabled && position == 0) { view.setTextColor(getResources().getColor( android.R.color.darker_gray)); } return view; } @Override public int getViewTypeCount() { return 1; } @Override public boolean hasStableIds() { return false; } @Override public boolean isEmpty() { return false; } @Override public void registerDataSetObserver(DataSetObserver observer) { } @Override public void unregisterDataSetObserver(DataSetObserver observer) { } @Override public boolean areAllItemsEnabled() { return false; } @Override public boolean isEnabled(int position) { // Top item will always be "upvote" or "already upvoted" // So, if upvoting is not enabled, this must be already upvoted // In that case we want to disable it if (!mUpVotingEnabled && position == 0) { return false; } return true; } @Override public void onClick(DialogInterface dialog, int item) { String clickedText = getItem(item).toString(); // If the clicked text is "upvote", then we want to upvote if // the user is logged in. If the user is not logged in then // we want to tell the user to login if (clickedText.equals(getApplicationContext().getString( R.string.upvote))) { if (!mIsLoggedIn) { setCommentToUpvote(mComment); startActivityForResult(new Intent(getApplicationContext(), LoginActivity_.class), ACTIVITY_LOGIN); } else { vote(mComment.getUpvoteUrl(Settings .getUserName(CommentsActivity.this)), mComment); } } else if (clickedText.equals(getApplicationContext().getString( R.string.downvote))) { // We don't need to test if the user is logged in here // because // They won't have a dowvnote url to see if they aren't // logged in vote(mComment.getDownvoteUrl(Settings .getUserName(CommentsActivity.this)), mComment); } else if(clickedText.equals(getApplicationContext().getString( R.string.collapse_thread))) { HNCommentTreeNode mRootNode = mComment.getTreeNode().getRootNode(); mComments.toggleCommentExpanded(mRootNode.getComment()); mCommentsListAdapter.notifyDataSetChanged(); }else { mComments.toggleCommentExpanded(mComment); mCommentsListAdapter.notifyDataSetChanged(); } } } class VoteTaskFinishedHandler implements ITaskFinishedHandler<Boolean> { @Override public void onTaskFinished( int taskCode, com.manuelmaly.hn.task.ITaskFinishedHandler.TaskResultCode code, Boolean result, Object tag) { if (taskCode == TASKCODE_VOTE) { if (result != null && result.booleanValue()) { Toast.makeText(CommentsActivity.this, R.string.vote_success, Toast.LENGTH_SHORT).show(); HNComment comment = (HNComment) tag; if (comment != null) { mVotedComments.add(comment); } } else { Toast.makeText(CommentsActivity.this, R.string.vote_error, Toast.LENGTH_LONG).show(); } } } } class CommentsAdapter extends BaseAdapter { @Override public int getCount() { return mComments.getComments().size(); } @Override public HNComment getItem(int position) { return mComments.getComments().get(position); } @Override public long getItemId(int position) { // Item ID not needed here: return 0; } @Override public View getView(final int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(R.layout.comments_list_item, null); CommentViewHolder holder = new CommentViewHolder(); holder.rootView = convertView; holder.textView = (LinkifiedTextView) convertView .findViewById(R.id.comments_list_item_text); holder.spacersContainer = (LinearLayout) convertView .findViewById(R.id.comments_list_item_spacerscontainer); holder.authorView = (TextView) convertView .findViewById(R.id.comments_list_item_author); holder.timeAgoView = (TextView) convertView .findViewById(R.id.comments_list_item_timeago); holder.expandView = (ImageView) convertView .findViewById(R.id.comments_list_item_expand); convertView.setTag(holder); } HNComment comment = getItem(position); CommentViewHolder holder = (CommentViewHolder) convertView.getTag(); holder.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (getItem(position).getTreeNode().hasChildren()) { mComments.toggleCommentExpanded(getItem(position)); mCommentsListAdapter.notifyDataSetChanged(); } } }); holder.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { final HNComment comment = getItem(position); AlertDialog.Builder builder = new AlertDialog.Builder(CommentsActivity.this); LongPressMenuListAdapter adapter = new LongPressMenuListAdapter(comment); builder.setAdapter(adapter, adapter); Dialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(true); dialog.show(); return true; } }); holder.setComment(comment, mCommentLevelIndentPx, CommentsActivity.this, mFontSizeText, mFontSizeMetadata); return convertView; } } static class CommentViewHolder { View rootView; LinkifiedTextView textView; TextView authorView; TextView timeAgoView; ImageView expandView; LinearLayout spacersContainer; public void setComment(HNComment comment, int commentLevelIndentPx, Context c, int commentTextSize, int metadataTextSize) { textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, commentTextSize); textView.setTextColor(comment.getColor()); textView.setText(Html.fromHtml(comment.getText())); textView.setMovementMethod(LinkMovementMethod.getInstance()); authorView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, metadataTextSize); timeAgoView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, metadataTextSize); if (!TextUtils.isEmpty(comment.getAuthor())) { authorView.setText(comment.getAuthor()); timeAgoView.setText(", " + comment.getTimeAgo()); } else { authorView.setText(c.getString(R.string.deleted)); // We set this here so that convertView doesn't reuse the old // timeAgoView value timeAgoView.setText(""); } expandView .setVisibility(comment.getTreeNode().isExpanded() ? View.INVISIBLE : View.VISIBLE); spacersContainer.removeAllViews(); for (int i = 0; i < comment.getCommentLevel(); i++) { View spacer = new View(c); spacer.setLayoutParams(new android.widget.LinearLayout.LayoutParams( commentLevelIndentPx, LayoutParams.MATCH_PARENT)); int spacerAlpha = Math.max(70 - i * 10, 10); spacer.setBackgroundColor(Color.argb(spacerAlpha, 0, 0, 0)); spacersContainer.addView(spacer, i); } } public void setOnClickListener(OnClickListener onClickListener) { rootView.setOnClickListener(onClickListener); textView.setOnClickListener(onClickListener); } public void setOnLongClickListener( OnLongClickListener onLongClickListener) { rootView.setOnLongClickListener(onLongClickListener); textView.setOnLongClickListener(onLongClickListener); } } protected void setCommentToUpvote(HNComment comment) { mPendingVote = comment; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ACTIVITY_LOGIN: if (resultCode == RESULT_OK) { if (mPendingVote != null) { mComments = new HNPostComments(); mCommentsListAdapter.notifyDataSetChanged(); startFeedLoading(); Toast.makeText(this, getString(R.string.login_success_reloading), Toast.LENGTH_SHORT).show(); } } else if (resultCode == RESULT_CANCELED) { Toast.makeText(this, getString(R.string.error_login_to_vote), Toast.LENGTH_LONG).show(); } case ACTIVITY_SPOTLIGHT: // The user tapped in the spotlight area if (resultCode == RESULT_OK) { openArticleReader(); } } } }