From ffd1d6e204fc7032a13216fe0c6dbe78205b2a98 Mon Sep 17 00:00:00 2001 From: Alex Ning Date: Sun, 18 Aug 2019 16:03:58 +0800 Subject: View messages in ViewMessageActivity. Use LinkResolverActivity to handle link clicking in CustomMarkwonView. Fixed cannot load user profile image after the post is deleted. --- .../infinityforreddit/AppComponent.java | 1 + .../CommentAndPostRecyclerViewAdapter.java | 22 +- .../infinityforreddit/CommentDataSource.java | 12 +- .../CommentsListingRecyclerViewAdapter.java | 8 +- .../infinityforreddit/FetchMessages.java | 25 ++- .../infinityforreddit/LinkResolverActivity.java | 9 +- .../infinityforreddit/MainActivity.java | 13 ++ .../docilealligator/infinityforreddit/Message.java | 2 +- .../infinityforreddit/MessageDataSource.java | 123 +++++++++++ .../MessageDataSourceFactory.java | 47 +++++ .../MessageRecyclerViewAdapter.java | 195 +++++++++++++++++ .../infinityforreddit/MessageViewModel.java | 100 +++++++++ .../infinityforreddit/ParseSubredditData.java | 33 +-- .../infinityforreddit/PostRecyclerViewAdapter.java | 3 +- .../infinityforreddit/PullNotificationWorker.java | 4 +- .../infinityforreddit/RedditAPI.java | 2 +- .../infinityforreddit/ViewMessageActivity.java | 230 +++++++++++++++++++++ .../infinityforreddit/ViewPostDetailActivity.java | 8 +- 18 files changed, 756 insertions(+), 81 deletions(-) create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSource.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSourceFactory.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/MessageRecyclerViewAdapter.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/MessageViewModel.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/ViewMessageActivity.java (limited to 'app/src/main/java/ml') diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java b/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java index 95e8f658..5a28dc40 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/AppComponent.java @@ -35,4 +35,5 @@ interface AppComponent { void inject(EditCommentActivity editCommentActivity); void inject(AccountPostsActivity accountPostsActivity); void inject(PullNotificationWorker pullNotificationWorker); + void inject(ViewMessageActivity viewMessageActivity); } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/CommentAndPostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/CommentAndPostRecyclerViewAdapter.java index 89b2817b..347634aa 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/CommentAndPostRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/CommentAndPostRecyclerViewAdapter.java @@ -72,7 +72,6 @@ class CommentAndPostRecyclerViewAdapter extends RecyclerView.Adapter { + String authorName = mPost.getAuthor().equals("[deleted]") ? mPost.getSubredditNamePrefixed().substring(2) : mPost.getAuthor(); + new LoadUserDataAsyncTask(mRedditDataRoomDatabase.userDao(), authorName, mOauthRetrofit, iconImageUrl -> { if(mActivity != null && getItemCount() > 0) { if(iconImageUrl == null || iconImageUrl.equals("")) { mGlide.load(R.drawable.subreddit_default_icon) @@ -229,10 +226,7 @@ class CommentAndPostRecyclerViewAdapter extends RecyclerView.Adapter { if(iconImageUrl == null || iconImageUrl.equals("")) { @@ -248,9 +242,7 @@ class CommentAndPostRecyclerViewAdapter extends RecyclerView.Adapter hasPostLiveData.postValue(true); } - callback.onResult(comments, null, after); + if(after == null || after.equals("") || after.equals("null")) { + callback.onResult(comments, null, null); + } else { + callback.onResult(comments, null, after); + } initialLoadStateLiveData.postValue(NetworkState.LOADED); } @@ -147,13 +151,15 @@ public class CommentDataSource extends PageKeyedDataSource } }).execute(); } else { - paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error parsing data")); + Log.i("Comments fetch error", "Error fetching data"); + paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetching data")); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error parsing data")); + Log.i("Comments fetch error", "Error fetchin data"); + paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetching data")); } }); } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/CommentsListingRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/CommentsListingRecyclerViewAdapter.java index c20920a9..06c4f232 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/CommentsListingRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/CommentsListingRecyclerViewAdapter.java @@ -57,13 +57,13 @@ class CommentsListingRecyclerViewAdapter extends PagedListAdapter DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override - public boolean areItemsTheSame(@NonNull CommentData CommentData, @NonNull CommentData t1) { - return CommentData.getId().equals(t1.getId()); + public boolean areItemsTheSame(@NonNull CommentData commentData, @NonNull CommentData t1) { + return commentData.getId().equals(t1.getId()); } @Override - public boolean areContentsTheSame(@NonNull CommentData CommentData, @NonNull CommentData t1) { - return CommentData.getCommentContent().equals(t1.getCommentContent()); + public boolean areContentsTheSame(@NonNull CommentData commentData, @NonNull CommentData t1) { + return commentData.getCommentContent().equals(t1.getCommentContent()); } }; diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/FetchMessages.java b/app/src/main/java/ml/docilealligator/infinityforreddit/FetchMessages.java index 04d6f8de..424c1c83 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/FetchMessages.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/FetchMessages.java @@ -22,8 +22,8 @@ import retrofit2.Retrofit; class FetchMessages { interface FetchMessagesListener { - void fetchSuccess(@Nullable ArrayList messages); - void fetchFailed(boolean shouldRetry); + void fetchSuccess(ArrayList messages, @Nullable String after); + void fetchFailed(); } static final String WHERE_INBOX = "inbox"; @@ -32,22 +32,21 @@ class FetchMessages { static final String WHERE_COMMENTS = "comments"; static void fetchMessagesAsync(Retrofit oauthRetrofit, Locale locale, String accessToken, String where, - FetchMessagesListener fetchMessagesListener) { - oauthRetrofit.create(RedditAPI.class).getMessages(RedditUtils.getOAuthHeader(accessToken), where) + String after, FetchMessagesListener fetchMessagesListener) { + oauthRetrofit.create(RedditAPI.class).getMessages(RedditUtils.getOAuthHeader(accessToken), where, after) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if(response.isSuccessful()) { - new ParseMessageAsnycTask(response.body(), locale, - fetchMessagesListener::fetchSuccess).execute(); + new ParseMessageAsnycTask(response.body(), locale, fetchMessagesListener::fetchSuccess).execute(); } else { - fetchMessagesListener.fetchFailed(true); + fetchMessagesListener.fetchFailed(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fetchMessagesListener.fetchFailed(true); + fetchMessagesListener.fetchFailed(); } }); } @@ -104,12 +103,13 @@ class FetchMessages { private static class ParseMessageAsnycTask extends AsyncTask { interface ParseMessageAsyncTaskListener { - void parseSuccess(ArrayList messages); + void parseSuccess(ArrayList messages, @Nullable String after); } private String response; private Locale locale; private ArrayList messages; + private String after; private ParseMessageAsyncTaskListener parseMessageAsyncTaskListener; ParseMessageAsnycTask(String response, Locale locale, ParseMessageAsyncTaskListener parseMessageAsnycTaskListener) { @@ -122,13 +122,18 @@ class FetchMessages { @Override protected Void doInBackground(Void... voids) { messages = parseMessage(response, locale); + try { + after = new JSONObject(response).getJSONObject(JSONUtils.DATA_KEY).getString(JSONUtils.AFTER_KEY); + } catch (JSONException e) { + e.printStackTrace(); + } return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); - parseMessageAsyncTaskListener.parseSuccess(messages); + parseMessageAsyncTaskListener.parseSuccess(messages, after); } } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/LinkResolverActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/LinkResolverActivity.java index d95a210e..1c6ee055 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/LinkResolverActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/LinkResolverActivity.java @@ -85,11 +85,10 @@ public class LinkResolverActivity extends AppCompatActivity { builder.setToolbarColor(getResources().getColor(R.color.colorPrimary)); CustomTabsIntent customTabsIntent = builder.build(); customTabsIntent.intent.setPackage(resolveInfos.get(0).activityInfo.packageName); - String uriString = uri.toString(); - if(!uriString.startsWith("http://") || (!uriString.startsWith("https://"))) { - uriString = "http://" + uriString; + if(uri.getScheme() == null) { + uri = Uri.parse("http://" + uri.toString()); } - customTabsIntent.launchUrl(this, Uri.parse(uriString)); + customTabsIntent.launchUrl(this, uri); } else { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); @@ -133,7 +132,7 @@ public class LinkResolverActivity extends AppCompatActivity { return packagesSupportingCustomTabs; } - static Uri getRedditUriByPath(String path) { + public static Uri getRedditUriByPath(String path) { return Uri.parse("https://www.reddit.com" + path); } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/MainActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/MainActivity.java index b9d79b94..f2c58214 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/MainActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/MainActivity.java @@ -83,6 +83,7 @@ public class MainActivity extends AppCompatActivity implements SortTypeBottomShe @BindView(R.id.all_drawer_items_linear_layout_main_activity) LinearLayout allDrawerItemsLinearLayout; @BindView(R.id.profile_linear_layout_main_activity) LinearLayout profileLinearLayout; @BindView(R.id.subscriptions_linear_layout_main_activity) LinearLayout subscriptionLinearLayout; + @BindView(R.id.inbox_linear_layout_main_activity) LinearLayout inboxLinearLayout; @BindView(R.id.upvoted_linear_layout_main_activity) LinearLayout upvotedLinearLayout; @BindView(R.id.downvoted_linear_layout_main_activity) LinearLayout downvotedLinearLayout; @BindView(R.id.hidden_linear_layout_main_activity) LinearLayout hiddenLinearLayout; @@ -385,6 +386,12 @@ public class MainActivity extends AppCompatActivity implements SortTypeBottomShe mAccountNameTextView.setText(R.string.anonymous_account); profileLinearLayout.setVisibility(View.GONE); subscriptionLinearLayout.setVisibility(View.GONE); + inboxLinearLayout.setVisibility(View.GONE); + upvotedLinearLayout.setVisibility(View.GONE); + downvotedLinearLayout.setVisibility(View.GONE); + hiddenLinearLayout.setVisibility(View.GONE); + savedLinearLayout.setVisibility(View.GONE); + gildedLinearLayout.setVisibility(View.GONE); divider.setVisibility(View.GONE); } @@ -417,6 +424,12 @@ public class MainActivity extends AppCompatActivity implements SortTypeBottomShe drawer.closeDrawers(); }); + inboxLinearLayout.setOnClickListener(view -> { + Intent intent = new Intent(this, ViewMessageActivity.class); + startActivity(intent); + drawer.closeDrawers(); + }); + upvotedLinearLayout.setOnClickListener(view -> { Intent intent = new Intent(MainActivity.this, AccountPostsActivity.class); intent.putExtra(AccountPostsActivity.EXTRA_USER_WHERE, PostDataSource.USER_WHERE_UPVOTED); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/Message.java b/app/src/main/java/ml/docilealligator/infinityforreddit/Message.java index 1846a8ef..1df3b233 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/Message.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/Message.java @@ -104,7 +104,7 @@ class Message { return formattedTime; } - public boolean isWasComment() { + public boolean wasComment() { return wasComment; } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSource.java b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSource.java new file mode 100644 index 00000000..3b2af4fb --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSource.java @@ -0,0 +1,123 @@ +package ml.docilealligator.infinityforreddit; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.MutableLiveData; +import androidx.paging.PageKeyedDataSource; + +import java.util.ArrayList; +import java.util.Locale; + +import retrofit2.Retrofit; + +class MessageDataSource extends PageKeyedDataSource { + private Retrofit oauthRetrofit; + private Locale locale; + private String accessToken; + private String where; + + private MutableLiveData paginationNetworkStateLiveData; + private MutableLiveData initialLoadStateLiveData; + private MutableLiveData hasPostLiveData; + + private LoadInitialParams initialParams; + private LoadInitialCallback initialCallback; + private LoadParams params; + private LoadCallback callback; + + MessageDataSource(Retrofit oauthRetrofit, Locale locale, String accessToken, String where) { + this.oauthRetrofit = oauthRetrofit; + this.locale = locale; + this.accessToken = accessToken; + this.where = where; + paginationNetworkStateLiveData = new MutableLiveData<>(); + initialLoadStateLiveData = new MutableLiveData<>(); + hasPostLiveData = new MutableLiveData<>(); + } + + MutableLiveData getPaginationNetworkStateLiveData() { + return paginationNetworkStateLiveData; + } + + MutableLiveData getInitialLoadStateLiveData() { + return initialLoadStateLiveData; + } + + MutableLiveData hasPostLiveData() { + return hasPostLiveData; + } + + void retry() { + loadInitial(initialParams, initialCallback); + } + + void retryLoadingMore() { + loadAfter(params, callback); + } + + @Override + public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) { + initialParams = params; + initialCallback = callback; + + initialLoadStateLiveData.postValue(NetworkState.LOADING); + + FetchMessages.fetchMessagesAsync(oauthRetrofit, locale, accessToken, where, null, new FetchMessages.FetchMessagesListener() { + @Override + public void fetchSuccess(ArrayList messages, @Nullable String after) { + if(messages.size() == 0) { + hasPostLiveData.postValue(false); + } else { + hasPostLiveData.postValue(true); + } + + if(after == null || after.equals("") || after.equals("null")) { + callback.onResult(messages, null, null); + } else { + callback.onResult(messages, null, after); + } + initialLoadStateLiveData.postValue(NetworkState.LOADED); + } + + @Override + public void fetchFailed() { + Log.i("Messages fetch error", "Error fetch messages"); + initialLoadStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetch messages")); + } + }); + } + + @Override + public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) { + + } + + @Override + public void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) { + this.params = params; + this.callback = callback; + + paginationNetworkStateLiveData.postValue(NetworkState.LOADING); + + FetchMessages.fetchMessagesAsync(oauthRetrofit, locale, accessToken, where, params.key, new FetchMessages.FetchMessagesListener() { + @Override + public void fetchSuccess(ArrayList messages, @Nullable String after) { + if(after == null || after.equals("") || after.equals("null")) { + callback.onResult(messages, null); + } else { + callback.onResult(messages, after); + } + + paginationNetworkStateLiveData.postValue(NetworkState.LOADED); + } + + @Override + public void fetchFailed() { + Log.i("Comments fetch error", "Error parsing data"); + paginationNetworkStateLiveData.postValue(new NetworkState(NetworkState.Status.FAILED, "Error fetching data")); + } + }); + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSourceFactory.java b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSourceFactory.java new file mode 100644 index 00000000..b1470bf7 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageDataSourceFactory.java @@ -0,0 +1,47 @@ +package ml.docilealligator.infinityforreddit; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; +import androidx.paging.DataSource; + +import java.util.Locale; + +import retrofit2.Retrofit; + +class MessageDataSourceFactory extends DataSource.Factory { + private Retrofit oauthRetrofit; + private Locale locale; + private String accessToken; + private String where; + + private MessageDataSource messageDataSource; + private MutableLiveData messageDataSourceLiveData; + + MessageDataSourceFactory(Retrofit oauthRetrofit, Locale locale, String accessToken, String where) { + this.oauthRetrofit = oauthRetrofit; + this.locale = locale; + this.accessToken = accessToken; + this.where = where; + messageDataSourceLiveData = new MutableLiveData<>(); + } + + @NonNull + @Override + public DataSource create() { + messageDataSource = new MessageDataSource(oauthRetrofit, locale, accessToken, where); + messageDataSourceLiveData.postValue(messageDataSource); + return messageDataSource; + } + + public MutableLiveData getMessageDataSourceLiveData() { + return messageDataSourceLiveData; + } + + MessageDataSource getMessageDataSource() { + return messageDataSource; + } + + void changeWhere(String where) { + this.where = where; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/MessageRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageRecyclerViewAdapter.java new file mode 100644 index 00000000..3865123b --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageRecyclerViewAdapter.java @@ -0,0 +1,195 @@ +package ml.docilealligator.infinityforreddit; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.paging.PagedListAdapter; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import CustomView.CustomMarkwonView; +import butterknife.BindView; +import butterknife.ButterKnife; + +class MessageRecyclerViewAdapter extends PagedListAdapter { + private static final int VIEW_TYPE_DATA = 0; + private static final int VIEW_TYPE_ERROR = 1; + private static final int VIEW_TYPE_LOADING = 2; + + private Context mContext; + private Resources resources; + + private NetworkState networkState; + private CommentsListingRecyclerViewAdapter.RetryLoadingMoreCallback mRetryLoadingMoreCallback; + + interface RetryLoadingMoreCallback { + void retryLoadingMore(); + } + + MessageRecyclerViewAdapter(Context context) { + super(DIFF_CALLBACK); + mContext = context; + resources = context.getResources(); + } + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull Message message, @NonNull Message t1) { + return message.getId().equals(t1.getId()); + } + + @Override + public boolean areContentsTheSame(@NonNull Message message, @NonNull Message t1) { + return message.getBody().equals(t1.getBody()); + } + }; + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_DATA) { + return new DataViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message, parent, false)); + } else if(viewType == VIEW_TYPE_ERROR) { + return new ErrorViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_footer_error, parent, false)); + } else { + return new LoadingViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_footer_loading, parent, false)); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if(holder instanceof DataViewHolder) { + Message message = getItem(holder.getAdapterPosition()); + if(message != null) { + if(message.isNew()) { + ((DataViewHolder) holder).itemView.setBackgroundColor( + resources.getColor(R.color.unreadMessageBackgroundColor)); + } + + if(message.wasComment()) { + ((DataViewHolder) holder).authorTextView.setTextColor(resources.getColor(R.color.colorPrimaryDarkDayNightTheme)); + ((DataViewHolder) holder).titleTextView.setText(message.getTitle()); + } else { + ((DataViewHolder) holder).titleTextView.setVisibility(View.GONE); + } + + ((DataViewHolder) holder).authorTextView.setText(message.getAuthor()); + String subject = message.getSubject().substring(0, 1).toUpperCase() + message.getSubject().substring(1); + ((DataViewHolder) holder).subjectTextView.setText(subject); + ((DataViewHolder) holder).contentCustomMarkwonView.setMarkdown(message.getBody(), mContext); + + ((DataViewHolder) holder).itemView.setOnClickListener(view -> { + if(message.getContext() != null && !message.getContext().equals("")) { + Uri uri = LinkResolverActivity.getRedditUriByPath(message.getContext()); + Intent intent = new Intent(mContext, LinkResolverActivity.class); + intent.setData(uri); + mContext.startActivity(intent); + } + }); + + ((DataViewHolder) holder).authorTextView.setOnClickListener(view -> { + if(message.wasComment()) { + Intent intent = new Intent(mContext, ViewUserDetailActivity.class); + intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, message.getAuthor()); + mContext.startActivity(intent); + } + }); + + ((DataViewHolder) holder).contentCustomMarkwonView.setOnClickListener(view -> ((DataViewHolder) holder).itemView.performClick()); + } + } + } + + @Override + public int getItemViewType(int position) { + // Reached at the end + if (hasExtraRow() && position == getItemCount() - 1) { + if (networkState.getStatus() == NetworkState.Status.LOADING) { + return VIEW_TYPE_LOADING; + } else { + return VIEW_TYPE_ERROR; + } + } else { + return VIEW_TYPE_DATA; + } + } + + @Override + public int getItemCount() { + if(hasExtraRow()) { + return super.getItemCount() + 1; + } + return super.getItemCount(); + } + + @Override + public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { + super.onViewRecycled(holder); + if(holder instanceof DataViewHolder) { + ((DataViewHolder) holder).itemView.setBackgroundColor(resources.getColor(android.R.color.white)); + ((DataViewHolder) holder).titleTextView.setVisibility(View.VISIBLE); + ((DataViewHolder) holder).authorTextView.setTextColor(resources.getColor(R.color.primaryTextColor)); + } + } + + private boolean hasExtraRow() { + return networkState != null && networkState.getStatus() != NetworkState.Status.SUCCESS; + } + + void setNetworkState(NetworkState newNetworkState) { + NetworkState previousState = this.networkState; + boolean previousExtraRow = hasExtraRow(); + this.networkState = newNetworkState; + boolean newExtraRow = hasExtraRow(); + if (previousExtraRow != newExtraRow) { + if (previousExtraRow) { + notifyItemRemoved(super.getItemCount()); + } else { + notifyItemInserted(super.getItemCount()); + } + } else if (newExtraRow && !previousState.equals(newNetworkState)) { + notifyItemChanged(getItemCount() - 1); + } + } + + class DataViewHolder extends RecyclerView.ViewHolder { + View itemView; + @BindView(R.id.author_text_view_item_message) TextView authorTextView; + @BindView(R.id.subject_text_view_item_message) TextView subjectTextView; + @BindView(R.id.title_text_view_item_message) TextView titleTextView; + @BindView(R.id.content_custom_markwon_view_item_message) CustomMarkwonView contentCustomMarkwonView; + + DataViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + this.itemView = itemView; + } + } + + class ErrorViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.error_text_view_item_footer_error) TextView errorTextView; + @BindView(R.id.retry_button_item_footer_error) Button retryButton; + + ErrorViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + errorTextView.setText(R.string.load_comments_failed); + retryButton.setOnClickListener(view -> mRetryLoadingMoreCallback.retryLoadingMore()); + } + } + + class LoadingViewHolder extends RecyclerView.ViewHolder { + LoadingViewHolder(@NonNull View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/MessageViewModel.java b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageViewModel.java new file mode 100644 index 00000000..4ad155d6 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/MessageViewModel.java @@ -0,0 +1,100 @@ +package ml.docilealligator.infinityforreddit; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import androidx.paging.LivePagedListBuilder; +import androidx.paging.PagedList; + +import java.util.Locale; + +import retrofit2.Retrofit; + +public class MessageViewModel extends ViewModel { + private MessageDataSourceFactory messageDataSourceFactory; + private LiveData paginationNetworkState; + private LiveData initialLoadingState; + private LiveData hasMessageLiveData; + private LiveData> messages; + private MutableLiveData whereLiveData; + + public MessageViewModel(Retrofit retrofit, Locale locale, String accessToken, String where) { + messageDataSourceFactory = new MessageDataSourceFactory(retrofit, locale, accessToken, where); + + initialLoadingState = Transformations.switchMap(messageDataSourceFactory.getMessageDataSourceLiveData(), + MessageDataSource::getInitialLoadStateLiveData); + paginationNetworkState = Transformations.switchMap(messageDataSourceFactory.getMessageDataSourceLiveData(), + MessageDataSource::getPaginationNetworkStateLiveData); + hasMessageLiveData = Transformations.switchMap(messageDataSourceFactory.getMessageDataSourceLiveData(), + MessageDataSource::hasPostLiveData); + + whereLiveData = new MutableLiveData<>(); + whereLiveData.postValue(where); + + PagedList.Config pagedListConfig = + (new PagedList.Config.Builder()) + .setEnablePlaceholders(false) + .setPageSize(25) + .build(); + + messages = Transformations.switchMap(whereLiveData, newWhere -> { + messageDataSourceFactory.changeWhere(whereLiveData.getValue()); + return (new LivePagedListBuilder(messageDataSourceFactory, pagedListConfig)).build(); + }); + } + + LiveData> getMessages() { + return messages; + } + + LiveData getPaginationNetworkState() { + return paginationNetworkState; + } + + LiveData getInitialLoadingState() { + return initialLoadingState; + } + + LiveData hasMessage() { + return hasMessageLiveData; + } + + void refresh() { + messageDataSourceFactory.getMessageDataSource().invalidate(); + } + + void retry() { + messageDataSourceFactory.getMessageDataSource().retry(); + } + + void retryLoadingMore() { + messageDataSourceFactory.getMessageDataSource().retryLoadingMore(); + } + + void changeWhere(String where) { + whereLiveData.postValue(where); + } + + public static class Factory extends ViewModelProvider.NewInstanceFactory { + private Retrofit retrofit; + private Locale locale; + private String accessToken; + private String where; + + public Factory(Retrofit retrofit, Locale locale, String accessToken, String where) { + this.retrofit = retrofit; + this.locale = locale; + this.accessToken = accessToken; + this.where = where; + } + + @NonNull + @Override + public T create(@NonNull Class modelClass) { + return (T) new MessageViewModel(retrofit, locale, accessToken, where); + } + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/ParseSubredditData.java b/app/src/main/java/ml/docilealligator/infinityforreddit/ParseSubredditData.java index 22e3787c..56d35248 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/ParseSubredditData.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/ParseSubredditData.java @@ -43,7 +43,7 @@ class ParseSubredditData { jsonResponse = new JSONObject(response); parseFailed = false; } catch (JSONException e) { - Log.i("subreddit json error", e.getMessage()); + Log.i("subreddit json error", "message: " + e.getMessage()); parseSubredditDataListener.onParseSubredditDataFail(); } } @@ -54,38 +54,9 @@ class ParseSubredditData { JSONObject data = jsonResponse.getJSONObject(JSONUtils.DATA_KEY); mNCurrentOnlineSubscribers = data.getInt(JSONUtils.ACTIVE_USER_COUNT_KEY); subredditData = parseSubredditData(data); - /*String id = data.getString(JSONUtils.EXTRA_NAME); - String subredditFullName = data.getString(JSONUtils.DISPLAY_NAME); - String description = data.getString(JSONUtils.PUBLIC_DESCRIPTION_KEY).trim(); - - String bannerImageUrl; - if(data.isNull(JSONUtils.BANNER_BACKGROUND_IMAGE_KEY)) { - bannerImageUrl = ""; - } else { - bannerImageUrl = data.getString(JSONUtils.BANNER_BACKGROUND_IMAGE_KEY); - } - if(bannerImageUrl.equals("") && !data.isNull(JSONUtils.BANNER_IMG_KEY)) { - bannerImageUrl= data.getString(JSONUtils.BANNER_IMG_KEY); - } - - String iconUrl; - if(data.isNull(JSONUtils.COMMUNITY_ICON_KEY)) { - iconUrl = ""; - } else { - iconUrl = data.getString(JSONUtils.COMMUNITY_ICON_KEY); - } - if(iconUrl.equals("") && !data.isNull(JSONUtils.ICON_IMG_KEY)) { - iconUrl = data.getString(JSONUtils.ICON_IMG_KEY); - } - - int nSubscribers = data.getInt(JSONUtils.SUBSCRIBERS_KEY); - int nCurrentOnlineSubscribers = data.getInt(JSONUtils.ACTIVE_USER_COUNT_KEY); - subredditData = new SubredditData(id, subredditFullName, iconUrl, bannerImageUrl, description, nSubscribers); - mNCurrentOnlineSubscribers = nCurrentOnlineSubscribers;*/ } catch (JSONException e) { parseFailed = true; Log.i("parse", "SubredditData error"); - parseSubredditDataListener.onParseSubredditDataFail(); } return null; } @@ -114,7 +85,7 @@ class ParseSubredditData { parseFailed = false; subredditListingData = new ArrayList<>(); } catch (JSONException e) { - Log.i("subreddit json error", e.getMessage()); + Log.i("subreddit json error", "message: " + e.getMessage()); parseFailed = true; } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/PostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/PostRecyclerViewAdapter.java index 239bf8c0..8d00a010 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/PostRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/PostRecyclerViewAdapter.java @@ -248,7 +248,8 @@ class PostRecyclerViewAdapter extends PagedListAdapter { + String authorName = post.getAuthor().equals("[deleted]") ? post.getSubredditNamePrefixed().substring(2) : post.getAuthor(); + new LoadUserDataAsyncTask(mUserDao, authorName, mRetrofit, iconImageUrl -> { if(mContext != null && getItemCount() > 0) { if(iconImageUrl == null || iconImageUrl.equals("")) { mGlide.load(R.drawable.subreddit_default_icon) diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/PullNotificationWorker.java b/app/src/main/java/ml/docilealligator/infinityforreddit/PullNotificationWorker.java index 512096c7..57c13212 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/PullNotificationWorker.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/PullNotificationWorker.java @@ -49,13 +49,13 @@ public class PullNotificationWorker extends Worker { Account currentAccount = redditDataRoomDatabase.accountDao().getCurrentAccount(); Response response = mOauthRetrofit.create(RedditAPI.class).getMessages( RedditUtils.getOAuthHeader(currentAccount.getAccessToken()), - FetchMessages.WHERE_COMMENTS).execute(); + FetchMessages.WHERE_UNREAD, null).execute(); Log.i("workmanager", "has response"); if(response.isSuccessful()) { String responseBody = response.body(); ArrayList messages = FetchMessages.parseMessage(responseBody, context.getResources().getConfiguration().locale); - if(messages != null) { + if(messages != null && !messages.isEmpty()) { NotificationManagerCompat notificationManager = NotificationUtils.getNotificationManager(context); NotificationCompat.Builder summaryBuilder = NotificationUtils.buildSummaryNotification(context, diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/RedditAPI.java b/app/src/main/java/ml/docilealligator/infinityforreddit/RedditAPI.java index ddd53c47..a187b521 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/RedditAPI.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/RedditAPI.java @@ -158,5 +158,5 @@ public interface RedditAPI { Call selectFlair(@Path("subredditNamePrefixed") String subredditName, @HeaderMap Map headers, @FieldMap Map params); @GET("/message/{where}.json?raw_json=1") - Call getMessages(@HeaderMap Map headers, @Path("where") String where); + Call getMessages(@HeaderMap Map headers, @Path("where") String where, @Query("after") String after); } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/ViewMessageActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/ViewMessageActivity.java new file mode 100644 index 00000000..b61438f4 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/ViewMessageActivity.java @@ -0,0 +1,230 @@ +package ml.docilealligator.infinityforreddit; + +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestManager; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.lsjwzh.widget.materialloadingprogressbar.CircleProgressBar; + +import javax.inject.Inject; +import javax.inject.Named; + +import Account.Account; +import butterknife.BindView; +import butterknife.ButterKnife; +import retrofit2.Retrofit; + +public class ViewMessageActivity extends AppCompatActivity { + + private static final String NULL_ACCESS_TOKEN_STATE = "NATS"; + private static final String ACCESS_TOKEN_STATE = "ATS"; + + @BindView(R.id.collapsing_toolbar_layout_view_message_activity) CollapsingToolbarLayout collapsingToolbarLayout; + @BindView(R.id.appbar_layout_view_message_activity) AppBarLayout appBarLayout; + @BindView(R.id.toolbar_view_message_activity) Toolbar toolbar; + @BindView(R.id.progress_bar_view_message_activity) CircleProgressBar mProgressBar; + @BindView(R.id.recycler_view_view_message_activity) RecyclerView recyclerView; + @BindView(R.id.fetch_messages_info_linear_layout_view_message_activity) LinearLayout mFetchMessageInfoLinearLayout; + @BindView(R.id.fetch_messages_info_image_view_view_message_activity) ImageView mFetchMessageInfoImageView; + @BindView(R.id.fetch_messages_info_text_view_view_message_activity) TextView mFetchMessageInfoTextView; + + private boolean mNullAccessToken = false; + private String mAccessToken; + + private MessageRecyclerViewAdapter mAdapter; + + private RequestManager mGlide; + + MessageViewModel mMessageViewModel; + + @Inject + @Named("oauth") + Retrofit mOauthRetrofit; + + @Inject + RedditDataRoomDatabase mRedditDataRoomDatabase; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_message); + + ButterKnife.bind(this); + + ((Infinity) getApplication()).getAppComponent().inject(this); + + mGlide = Glide.with(this); + + Resources resources = getResources(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 + && (resources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT + || resources.getBoolean(R.bool.isTablet))) { + Window window = getWindow(); + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + + boolean lightNavBar = false; + if((resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES) { + lightNavBar = true; + } + boolean finalLightNavBar = lightNavBar; + + View decorView = window.getDecorView(); + if(finalLightNavBar) { + decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + appBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() { + @Override + void onStateChanged(AppBarLayout appBarLayout, State state) { + if (state == State.COLLAPSED) { + if(finalLightNavBar) { + decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + } else if (state == State.EXPANDED) { + if(finalLightNavBar) { + decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + } + } + }); + + int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (statusBarResourceId > 0) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) toolbar.getLayoutParams(); + params.topMargin = getResources().getDimensionPixelSize(statusBarResourceId); + toolbar.setLayoutParams(params); + } + + int navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + if (navBarResourceId > 0) { + recyclerView.setPadding(0, 0, 0, resources.getDimensionPixelSize(navBarResourceId)); + } + } + + toolbar.setTitle(R.string.inbox); + setSupportActionBar(toolbar); + + if(savedInstanceState != null) { + mNullAccessToken = savedInstanceState.getBoolean(NULL_ACCESS_TOKEN_STATE); + mAccessToken = savedInstanceState.getString(ACCESS_TOKEN_STATE); + + if(!mNullAccessToken && mAccessToken == null) { + getCurrentAccountAndFetchMessage(); + } else { + bindView(); + } + } else { + getCurrentAccountAndFetchMessage(); + } + } + + private void getCurrentAccountAndFetchMessage() { + new GetCurrentAccountAsyncTask(mRedditDataRoomDatabase.accountDao(), new GetCurrentAccountAsyncTask.GetCurrentAccountAsyncTaskListener() { + @Override + public void success(Account account) { + if(account == null) { + mNullAccessToken = true; + } else { + mAccessToken = account.getAccessToken(); + + bindView(); + } + } + }).execute(); + } + + private void bindView() { + mAdapter = new MessageRecyclerViewAdapter(this); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(mAdapter); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + MessageViewModel.Factory factory = new MessageViewModel.Factory(mOauthRetrofit, + getResources().getConfiguration().locale, mAccessToken, FetchMessages.WHERE_INBOX); + mMessageViewModel = new ViewModelProvider(this, factory).get(MessageViewModel.class); + mMessageViewModel.getMessages().observe(this, messages -> mAdapter.submitList(messages)); + + mMessageViewModel.getInitialLoadingState().observe(this, networkState -> { + if(networkState.getStatus().equals(NetworkState.Status.SUCCESS)) { + mProgressBar.setVisibility(View.GONE); + } else if(networkState.getStatus().equals(NetworkState.Status.FAILED)) { + mFetchMessageInfoLinearLayout.setOnClickListener(view -> mMessageViewModel.retry()); + showErrorView(R.string.load_messages_failed); + } else { + mFetchMessageInfoLinearLayout.setVisibility(View.GONE); + mProgressBar.setVisibility(View.VISIBLE); + } + }); + + mMessageViewModel.hasMessage().observe(this, hasMessage -> { + if(hasMessage) { + mFetchMessageInfoLinearLayout.setVisibility(View.GONE); + } else { + mFetchMessageInfoLinearLayout.setOnClickListener(view -> { + //Do nothing + }); + showErrorView(R.string.no_messages); + } + }); + + mMessageViewModel.getPaginationNetworkState().observe(this, networkState -> { + mAdapter.setNetworkState(networkState); + }); + } + + private void showErrorView(int stringResId) { + mProgressBar.setVisibility(View.GONE); + mFetchMessageInfoLinearLayout.setVisibility(View.VISIBLE); + mFetchMessageInfoTextView.setText(stringResId); + mGlide.load(R.drawable.load_post_error_indicator).into(mFetchMessageInfoImageView); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.view_message_activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if(item.getItemId() == R.id.action_refresh_view_message_activity) { + mMessageViewModel.refresh(); + return true; + } else if(item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return false; + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(NULL_ACCESS_TOKEN_STATE, mNullAccessToken); + outState.putString(ACCESS_TOKEN_STATE, mAccessToken); + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/ViewPostDetailActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/ViewPostDetailActivity.java index 1039fc96..9e7bdb9b 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/ViewPostDetailActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/ViewPostDetailActivity.java @@ -101,7 +101,6 @@ public class ViewPostDetailActivity extends AppCompatActivity implements FlairBo private LinearLayoutManager mLinearLayoutManager; private CommentAndPostRecyclerViewAdapter mAdapter; - private LoadSubredditIconAsyncTask mLoadSubredditIconAsyncTask; @BindView(R.id.coordinator_layout_view_post_detail) CoordinatorLayout mCoordinatorLayout; @BindView(R.id.appbar_layout_view_post_detail_activity) AppBarLayout appBarLayout; @@ -255,7 +254,7 @@ public class ViewPostDetailActivity extends AppCompatActivity implements FlairBo } mAdapter = new CommentAndPostRecyclerViewAdapter(ViewPostDetailActivity.this, mRetrofit, mOauthRetrofit, mRedditDataRoomDatabase, mGlide, mAccessToken, mAccountName, mPost, - mLocale, mSingleCommentId, isSingleCommentThreadMode, mLoadSubredditIconAsyncTask, + mLocale, mSingleCommentId, isSingleCommentThreadMode, new CommentAndPostRecyclerViewAdapter.CommentRecyclerViewAdapterCallback() { @Override public void updatePost(Post post) { @@ -331,7 +330,7 @@ public class ViewPostDetailActivity extends AppCompatActivity implements FlairBo mAdapter = new CommentAndPostRecyclerViewAdapter(ViewPostDetailActivity.this, mRetrofit, mOauthRetrofit, mRedditDataRoomDatabase, mGlide, mAccessToken, mAccountName, mPost, - mLocale, mSingleCommentId, isSingleCommentThreadMode, mLoadSubredditIconAsyncTask, + mLocale, mSingleCommentId, isSingleCommentThreadMode, new CommentAndPostRecyclerViewAdapter.CommentRecyclerViewAdapterCallback() { @Override public void updatePost(Post post) { @@ -817,9 +816,6 @@ public class ViewPostDetailActivity extends AppCompatActivity implements FlairBo EventBus.getDefault().unregister(this); super.onDestroy(); Bridge.clear(this); - if(mLoadSubredditIconAsyncTask != null) { - mLoadSubredditIconAsyncTask.cancel(true); - } } @Override -- cgit v1.2.3