diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/AppComponent.java b/app/src/main/java/eu/toldi/infinityforlemmy/AppComponent.java index 59fc2a8a..ee1e8632 100644 --- a/app/src/main/java/eu/toldi/infinityforlemmy/AppComponent.java +++ b/app/src/main/java/eu/toldi/infinityforlemmy/AppComponent.java @@ -77,6 +77,7 @@ import eu.toldi.infinityforlemmy.fragments.InboxFragment; import eu.toldi.infinityforlemmy.fragments.MorePostsInfoFragment; import eu.toldi.infinityforlemmy.fragments.MultiRedditListingFragment; import eu.toldi.infinityforlemmy.fragments.PostFragment; +import eu.toldi.infinityforlemmy.fragments.PrivateMessageFragment; import eu.toldi.infinityforlemmy.fragments.SidebarFragment; import eu.toldi.infinityforlemmy.fragments.SubredditListingFragment; import eu.toldi.infinityforlemmy.fragments.SubscribedSubredditsListingFragment; @@ -319,6 +320,7 @@ public interface AppComponent { void inject(CommentMoreBottomSheetFragment commentMoreBottomSheetFragment); + void inject(PrivateMessageFragment privateMessageFragment); @Component.Factory interface Factory { diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/NetworkModule.java b/app/src/main/java/eu/toldi/infinityforlemmy/NetworkModule.java index 85909e06..c197f718 100644 --- a/app/src/main/java/eu/toldi/infinityforlemmy/NetworkModule.java +++ b/app/src/main/java/eu/toldi/infinityforlemmy/NetworkModule.java @@ -10,6 +10,7 @@ import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import eu.toldi.infinityforlemmy.apis.StreamableAPI; +import eu.toldi.infinityforlemmy.privatemessage.LemmyPrivateMessageAPI; import eu.toldi.infinityforlemmy.comment.LemmyCommentAPI; import eu.toldi.infinityforlemmy.post.LemmyPostAPI; import eu.toldi.infinityforlemmy.utils.APIUtils; @@ -226,4 +227,10 @@ abstract class NetworkModule { static LemmyCommentAPI provideCommentAPI(@Named("no_oauth") RetrofitHolder retrofitHolder) { return new LemmyCommentAPI(retrofitHolder); } + + @Provides + @Singleton + static LemmyPrivateMessageAPI provideLemmyPrivateMessageAPI(@Named("base") RetrofitHolder retrofit) { + return new LemmyPrivateMessageAPI(retrofit); + } } diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/activities/InboxActivity.java b/app/src/main/java/eu/toldi/infinityforlemmy/activities/InboxActivity.java index 5d63f290..e5006498 100644 --- a/app/src/main/java/eu/toldi/infinityforlemmy/activities/InboxActivity.java +++ b/app/src/main/java/eu/toldi/infinityforlemmy/activities/InboxActivity.java @@ -55,6 +55,7 @@ import eu.toldi.infinityforlemmy.events.PassPrivateMessageEvent; import eu.toldi.infinityforlemmy.events.PassPrivateMessageIndexEvent; import eu.toldi.infinityforlemmy.events.SwitchAccountEvent; import eu.toldi.infinityforlemmy.fragments.InboxFragment; +import eu.toldi.infinityforlemmy.fragments.PrivateMessageFragment; import eu.toldi.infinityforlemmy.message.CommentInteraction; import eu.toldi.infinityforlemmy.message.FetchMessage; import eu.toldi.infinityforlemmy.message.ReadMessage; @@ -441,7 +442,7 @@ public class InboxActivity extends BaseActivity implements ActivityToolbarInterf @NonNull @Override public Fragment createFragment(int position) { - InboxFragment fragment = new InboxFragment(); + Fragment fragment = new InboxFragment(); Bundle bundle = new Bundle(); bundle.putString(InboxFragment.EXTRA_ACCESS_TOKEN, mAccessToken); switch (position) { @@ -450,11 +451,9 @@ public class InboxActivity extends BaseActivity implements ActivityToolbarInterf break; case 1: bundle.putString(InboxFragment.EXTRA_MESSAGE_WHERE, FetchMessage.WHERE_MENTIONS); - fragment.setArguments(bundle); break; case 2: - bundle.putString(InboxFragment.EXTRA_MESSAGE_WHERE, FetchMessage.WHERE_MESSAGES); - fragment.setArguments(bundle); + fragment = new PrivateMessageFragment(); break; } fragment.setArguments(bundle); diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/adapters/PrivateMessageRecycleViewAdapter.java b/app/src/main/java/eu/toldi/infinityforlemmy/adapters/PrivateMessageRecycleViewAdapter.java new file mode 100644 index 00000000..3ec24f58 --- /dev/null +++ b/app/src/main/java/eu/toldi/infinityforlemmy/adapters/PrivateMessageRecycleViewAdapter.java @@ -0,0 +1,344 @@ +package eu.toldi.infinityforlemmy.adapters; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.paging.PagedListAdapter; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import eu.toldi.infinityforlemmy.NetworkState; +import eu.toldi.infinityforlemmy.R; +import eu.toldi.infinityforlemmy.activities.BaseActivity; +import eu.toldi.infinityforlemmy.activities.LinkResolverActivity; +import eu.toldi.infinityforlemmy.activities.ViewPrivateMessagesActivity; +import eu.toldi.infinityforlemmy.activities.ViewUserDetailActivity; +import eu.toldi.infinityforlemmy.customtheme.CustomThemeWrapper; +import eu.toldi.infinityforlemmy.markdown.RedditHeadingPlugin; +import eu.toldi.infinityforlemmy.markdown.SpoilerAwareMovementMethod; +import eu.toldi.infinityforlemmy.markdown.SpoilerParserPlugin; +import eu.toldi.infinityforlemmy.markdown.SuperscriptPlugin; +import eu.toldi.infinityforlemmy.privatemessage.LemmyPrivateMessageAPI; +import eu.toldi.infinityforlemmy.privatemessage.PrivateMessage; +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.Markwon; +import io.noties.markwon.MarkwonConfiguration; +import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; +import io.noties.markwon.image.glide.GlideImagesPlugin; +import io.noties.markwon.inlineparser.HtmlInlineProcessor; +import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; +import io.noties.markwon.linkify.LinkifyPlugin; +import io.noties.markwon.movement.MovementMethodPlugin; +import retrofit2.Retrofit; + +public class PrivateMessageRecycleViewAdapter 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 static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback<>() { + @Override + public boolean areItemsTheSame(@NonNull PrivateMessage message, @NonNull PrivateMessage t1) { + return message.getId() == t1.getId(); + } + + @Override + public boolean areContentsTheSame(@NonNull PrivateMessage message, @NonNull PrivateMessage t1) { + return message.getContent().equals(t1.getContent()); + } + }; + private BaseActivity mActivity; + private Retrofit retrofit; + private Markwon mMarkwon; + private String mAccessToken; + + private final LemmyPrivateMessageAPI lemmyPrivateMessageAPI; + private int mMessageType; + private NetworkState networkState; + private RetryLoadingMoreCallback mRetryLoadingMoreCallback; + private int mColorAccent; + private int mMessageBackgroundColor; + private int mUsernameColor; + private int mPrimaryTextColor; + private int mSecondaryTextColor; + private int mUnreadMessageBackgroundColor; + private int mColorPrimaryLightTheme; + private int mButtonTextColor; + private boolean markAllMessagesAsRead = false; + + public PrivateMessageRecycleViewAdapter(BaseActivity activity, Retrofit oauthRetrofit, + CustomThemeWrapper customThemeWrapper, + String accessToken, + LemmyPrivateMessageAPI lemmyPrivateMessageAPI, RetryLoadingMoreCallback retryLoadingMoreCallback) { + super(DIFF_CALLBACK); + mActivity = activity; + retrofit = oauthRetrofit; + this.lemmyPrivateMessageAPI = lemmyPrivateMessageAPI; + mRetryLoadingMoreCallback = retryLoadingMoreCallback; + + mColorAccent = customThemeWrapper.getColorAccent(); + mMessageBackgroundColor = customThemeWrapper.getCardViewBackgroundColor(); + mUsernameColor = customThemeWrapper.getUsername(); + mPrimaryTextColor = customThemeWrapper.getPrimaryTextColor(); + mSecondaryTextColor = customThemeWrapper.getSecondaryTextColor(); + int spoilerBackgroundColor = mSecondaryTextColor | 0xFF000000; + mUnreadMessageBackgroundColor = customThemeWrapper.getUnreadMessageBackgroundColor(); + mColorPrimaryLightTheme = customThemeWrapper.getColorPrimaryLightTheme(); + mButtonTextColor = customThemeWrapper.getButtonTextColor(); + + // todo:https://github.com/Docile-Alligator/Infinity-For-Reddit/issues/1027 + // add tables support and replace with MarkdownUtils#commonPostMarkwonBuilder + mMarkwon = Markwon.builder(mActivity) + .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { + plugin.excludeInlineProcessor(HtmlInlineProcessor.class); + })) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.linkResolver((view, link) -> { + Intent intent = new Intent(mActivity, LinkResolverActivity.class); + Uri uri = Uri.parse(link); + intent.setData(uri); + mActivity.startActivity(intent); + }); + } + + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder.linkColor(customThemeWrapper.getLinkColor()); + } + }) + .usePlugin(SuperscriptPlugin.create()) + .usePlugin(SpoilerParserPlugin.create(mSecondaryTextColor, spoilerBackgroundColor)) + .usePlugin(RedditHeadingPlugin.create()) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod())) + .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) + .usePlugin(GlideImagesPlugin.create(mActivity)) + .build(); + mAccessToken = accessToken; + + } + + @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) { + PrivateMessage message = getItem(holder.getBindingAdapterPosition()); + if (message != null) { + + if (message.getRead()) { + if (markAllMessagesAsRead) { + message.setRead(true); + } else { + holder.itemView.setBackgroundColor( + mUnreadMessageBackgroundColor); + } + } + + + ((DataViewHolder) holder).titleTextView.setVisibility(View.GONE); + + + ((DataViewHolder) holder).authorTextView.setText(message.getCreatorQualifiedName()); + String subject = message.getRecipientQualifiedName(); + ((DataViewHolder) holder).subjectTextView.setText(subject); + mMarkwon.setMarkdown(((DataViewHolder) holder).contentCustomMarkwonView, message.getContent()); + + holder.itemView.setOnClickListener(view -> { + + Intent intent = new Intent(mActivity, ViewPrivateMessagesActivity.class); + intent.putExtra(ViewPrivateMessagesActivity.EXTRA_PRIVATE_MESSAGE_INDEX, holder.getBindingAdapterPosition()); + intent.putExtra(ViewPrivateMessagesActivity.EXTRA_MESSAGE_POSITION, holder.getBindingAdapterPosition()); + mActivity.startActivity(intent); + + + if (message.getRead()) { + holder.itemView.setBackgroundColor(mMessageBackgroundColor); + + + lemmyPrivateMessageAPI.markPrivateMessageAsRead(mAccessToken, message.getId(), new LemmyPrivateMessageAPI.PrivateMessageMarkedAsReadListener() { + + @Override + public void onPrivateMessageMarkedAsReadError() { + message.setRead(false); + } + + @Override + public void onPrivateMessageMarkedAsReadSuccess() { + message.setRead(true); + } + }); + } + }); + + ((DataViewHolder) holder).authorTextView.setOnClickListener(view -> { + + Intent intent = new Intent(mActivity, ViewUserDetailActivity.class); + intent.putExtra(ViewUserDetailActivity.EXTRA_USER_NAME_KEY, message.getCreatorName()); + intent.putExtra(ViewUserDetailActivity.EXTRA_QUALIFIED_USER_NAME_KEY, message.getCreatorQualifiedName()); + mActivity.startActivity(intent); + }); + } + } + } + + @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(mMessageBackgroundColor); + ((DataViewHolder) holder).titleTextView.setVisibility(View.VISIBLE); + } + } + + private boolean hasExtraRow() { + return networkState != null && networkState.getStatus() != NetworkState.Status.SUCCESS; + } + + public 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); + } + } + + public void updateMessageReply(PrivateMessage newReply, int position) { + if (position >= 0 && position < super.getItemCount()) { + PrivateMessage message = getItem(position); + if (message != null) { + notifyItemChanged(position); + } + } + } + + public void setMarkAllMessagesAsRead(boolean markAllMessagesAsRead) { + this.markAllMessagesAsRead = markAllMessagesAsRead; + } + + public interface RetryLoadingMoreCallback { + void retryLoadingMore(); + } + + class DataViewHolder extends RecyclerView.ViewHolder { + @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) + TextView contentCustomMarkwonView; + + DataViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + if (mActivity.typeface != null) { + authorTextView.setTypeface(mActivity.typeface); + subjectTextView.setTypeface(mActivity.typeface); + titleTextView.setTypeface(mActivity.titleTypeface); + contentCustomMarkwonView.setTypeface(mActivity.contentTypeface); + } + itemView.setBackgroundColor(mMessageBackgroundColor); + authorTextView.setTextColor(mUsernameColor); + subjectTextView.setTextColor(mPrimaryTextColor); + titleTextView.setTextColor(mPrimaryTextColor); + contentCustomMarkwonView.setTextColor(mSecondaryTextColor); + + contentCustomMarkwonView.setMovementMethod(LinkMovementMethod.getInstance()); + + contentCustomMarkwonView.setOnClickListener(view -> { + if (contentCustomMarkwonView.getSelectionStart() == -1 && contentCustomMarkwonView.getSelectionEnd() == -1) { + itemView.performClick(); + } + }); + } + } + + 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); + if (mActivity.typeface != null) { + errorTextView.setTypeface(mActivity.typeface); + retryButton.setTypeface(mActivity.typeface); + } + errorTextView.setText(R.string.load_comments_failed); + errorTextView.setTextColor(mSecondaryTextColor); + retryButton.setOnClickListener(view -> mRetryLoadingMoreCallback.retryLoadingMore()); + retryButton.setBackgroundTintList(ColorStateList.valueOf(mColorPrimaryLightTheme)); + retryButton.setTextColor(mButtonTextColor); + } + } + + class LoadingViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.progress_bar_item_footer_loading) + ProgressBar progressBar; + + LoadingViewHolder(@NonNull View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + progressBar.setIndeterminateTintList(ColorStateList.valueOf(mColorAccent)); + } + } +} + diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/apis/LemmyAPI.java b/app/src/main/java/eu/toldi/infinityforlemmy/apis/LemmyAPI.java index 4ef15b11..57e9d767 100644 --- a/app/src/main/java/eu/toldi/infinityforlemmy/apis/LemmyAPI.java +++ b/app/src/main/java/eu/toldi/infinityforlemmy/apis/LemmyAPI.java @@ -15,6 +15,10 @@ import eu.toldi.infinityforlemmy.dto.EditCommentDTO; import eu.toldi.infinityforlemmy.dto.EditPostDTO; import eu.toldi.infinityforlemmy.dto.FollowCommunityDTO; import eu.toldi.infinityforlemmy.dto.PostVoteDTO; +import eu.toldi.infinityforlemmy.dto.PrivateMessageDTO; +import eu.toldi.infinityforlemmy.dto.PrivateMessageReadDTO; +import eu.toldi.infinityforlemmy.dto.PrivateMessageReportDTO; +import eu.toldi.infinityforlemmy.dto.PrivateMessageUpdateDTO; import eu.toldi.infinityforlemmy.dto.ReadCommentDTO; import eu.toldi.infinityforlemmy.dto.ReadMessageDTO; import eu.toldi.infinityforlemmy.dto.ReadPostDTO; @@ -246,4 +250,37 @@ public interface LemmyAPI { Call getSiteInfo( @Query("auth") String auth ); + + @GET("api/v3/private_message/list") + Call privateMessagesList( + @Query("page") Integer page, + @Query("limit") Integer limit, + @Query("unread_only") Boolean unread_only, + @NonNull @Query("auth") String auth + ); + + @POST("api/v3/private_message") + Call privateMessageSend( + @Body PrivateMessageDTO params + ); + + @PUT("api/v3/private_message") + Call privateMessageEdit( + @Body PrivateMessageUpdateDTO params + ); + + @POST("api/v3/private_message/delete") + Call privateMessageDelete( + @Body PrivateMessageUpdateDTO params + ); + + @POST("api/v3/private_message/mark_as_read") + Call privateMessageMarkAsRead( + @Body PrivateMessageReadDTO params + ); + + @POST("api/v3/private_message/report") + Call privateMessageReport( + @Body PrivateMessageReportDTO params + ); } diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/fragments/PrivateMessageFragment.java b/app/src/main/java/eu/toldi/infinityforlemmy/fragments/PrivateMessageFragment.java new file mode 100644 index 00000000..2c9b0923 --- /dev/null +++ b/app/src/main/java/eu/toldi/infinityforlemmy/fragments/PrivateMessageFragment.java @@ -0,0 +1,255 @@ +package eu.toldi.infinityforlemmy.fragments; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.paging.PagedList; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestManager; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import eu.toldi.infinityforlemmy.FragmentCommunicator; +import eu.toldi.infinityforlemmy.Infinity; +import eu.toldi.infinityforlemmy.NetworkState; +import eu.toldi.infinityforlemmy.R; +import eu.toldi.infinityforlemmy.RecyclerViewContentScrollingInterface; +import eu.toldi.infinityforlemmy.RedditDataRoomDatabase; +import eu.toldi.infinityforlemmy.RetrofitHolder; +import eu.toldi.infinityforlemmy.activities.BaseActivity; +import eu.toldi.infinityforlemmy.adapters.PrivateMessageRecycleViewAdapter; +import eu.toldi.infinityforlemmy.customtheme.CustomThemeWrapper; +import eu.toldi.infinityforlemmy.customviews.LinearLayoutManagerBugFixed; +import eu.toldi.infinityforlemmy.events.RepliedToPrivateMessageEvent; +import eu.toldi.infinityforlemmy.privatemessage.LemmyPrivateMessageAPI; +import eu.toldi.infinityforlemmy.privatemessage.PrivateMessage; +import eu.toldi.infinityforlemmy.privatemessage.PrivateMessageViewModel; + +public class PrivateMessageFragment extends Fragment implements FragmentCommunicator { + + public static final String EXTRA_ACCESS_TOKEN = "EAT"; + public static final String EXTRA_MESSAGE_WHERE = "EMT"; + @BindView(R.id.swipe_refresh_layout_inbox_fragment) + SwipeRefreshLayout mSwipeRefreshLayout; + @BindView(R.id.recycler_view_inbox_fragment) + RecyclerView mRecyclerView; + @BindView(R.id.fetch_messages_info_linear_layout_inbox_fragment) + LinearLayout mFetchMessageInfoLinearLayout; + @BindView(R.id.fetch_messages_info_image_view_inbox_fragment) + ImageView mFetchMessageInfoImageView; + @BindView(R.id.fetch_messages_info_text_view_inbox_fragment) + TextView mFetchMessageInfoTextView; + PrivateMessageViewModel mMessageViewModel; + @Inject + @Named("no_oauth") + RetrofitHolder mRetrofit; + @Inject + RedditDataRoomDatabase mRedditDataRoomDatabase; + @Inject + @Named("default") + SharedPreferences mSharedPreferences; + @Inject + CustomThemeWrapper mCustomThemeWrapper; + + @Inject + LemmyPrivateMessageAPI mLemmyPrivateMessageAPI; + private String mAccessToken; + private PrivateMessageRecycleViewAdapter mAdapter; + private RequestManager mGlide; + private LinearLayoutManagerBugFixed mLinearLayoutManager; + private BaseActivity mActivity; + + public PrivateMessageFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Log.i("PrivateMessageFragment", "onCreateView"); + View rootView = inflater.inflate(R.layout.fragment_inbox, container, false); + + ((Infinity) mActivity.getApplication()).getAppComponent().inject(this); + + ButterKnife.bind(this, rootView); + + EventBus.getDefault().register(this); + + applyTheme(); + + Bundle arguments = getArguments(); + if (arguments == null) { + return rootView; + } + mAccessToken = getArguments().getString(EXTRA_ACCESS_TOKEN); + mGlide = Glide.with(this); + + if (mActivity.isImmersiveInterface()) { + mRecyclerView.setPadding(0, 0, 0, mActivity.getNavBarHeight()); + } + + + mAdapter = new PrivateMessageRecycleViewAdapter(mActivity, mRetrofit.getRetrofit(), mCustomThemeWrapper, mAccessToken, mLemmyPrivateMessageAPI, () -> mMessageViewModel.retryLoadingMore()); + mLinearLayoutManager = new LinearLayoutManagerBugFixed(mActivity); + mRecyclerView.setLayoutManager(mLinearLayoutManager); + mRecyclerView.setAdapter(mAdapter); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mActivity, mLinearLayoutManager.getOrientation()); + mRecyclerView.addItemDecoration(dividerItemDecoration); + + if (mActivity instanceof RecyclerViewContentScrollingInterface) { + mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0) { + ((RecyclerViewContentScrollingInterface) mActivity).contentScrollDown(); + } else if (dy < 0) { + ((RecyclerViewContentScrollingInterface) mActivity).contentScrollUp(); + } + } + }); + } + + PrivateMessageViewModel.Factory factory = new PrivateMessageViewModel.Factory(mRetrofit.getRetrofit(), + getResources().getConfiguration().locale, mAccessToken, mLemmyPrivateMessageAPI); + mMessageViewModel = new ViewModelProvider(this, factory).get(PrivateMessageViewModel.class); + mMessageViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> mAdapter.submitList(messages)); + + mMessageViewModel.hasMessage().observe(getViewLifecycleOwner(), hasMessage -> { + mSwipeRefreshLayout.setRefreshing(false); + if (hasMessage) { + mFetchMessageInfoLinearLayout.setVisibility(View.GONE); + } else { + mFetchMessageInfoLinearLayout.setOnClickListener(null); + showErrorView(R.string.no_messages); + } + }); + + mMessageViewModel.getInitialLoadingState().observe(getViewLifecycleOwner(), networkState -> { + if (networkState.getStatus().equals(NetworkState.Status.SUCCESS)) { + mSwipeRefreshLayout.setRefreshing(false); + } else if (networkState.getStatus().equals(NetworkState.Status.FAILED)) { + mSwipeRefreshLayout.setRefreshing(false); + mFetchMessageInfoLinearLayout.setOnClickListener(view -> { + mFetchMessageInfoLinearLayout.setVisibility(View.GONE); + mMessageViewModel.refresh(); + mAdapter.setNetworkState(null); + }); + showErrorView(R.string.load_messages_failed); + } else { + mSwipeRefreshLayout.setRefreshing(true); + } + }); + + mMessageViewModel.getPaginationNetworkState().observe(getViewLifecycleOwner(), networkState -> { + mAdapter.setNetworkState(networkState); + }); + + mSwipeRefreshLayout.setOnRefreshListener(this::onRefresh); + + return rootView; + } + + private void showErrorView(int stringResId) { + mSwipeRefreshLayout.setRefreshing(false); + mFetchMessageInfoLinearLayout.setVisibility(View.VISIBLE); + mFetchMessageInfoTextView.setText(stringResId); + mGlide.load(R.drawable.error_image).into(mFetchMessageInfoImageView); + } + + @Override + public void applyTheme() { + mSwipeRefreshLayout.setProgressBackgroundColorSchemeColor(mCustomThemeWrapper.getCircularProgressBarBackground()); + mSwipeRefreshLayout.setColorSchemeColors(mCustomThemeWrapper.getColorAccent()); + mFetchMessageInfoTextView.setTextColor(mCustomThemeWrapper.getSecondaryTextColor()); + if (mActivity.typeface != null) { + mFetchMessageInfoTextView.setTypeface(mActivity.typeface); + } + } + + public void goBackToTop() { + if (mLinearLayoutManager != null) { + mLinearLayoutManager.scrollToPositionWithOffset(0, 0); + } + } + + public void markAllMessagesRead() { + if (mAdapter != null) { + mAdapter.setMarkAllMessagesAsRead(true); + + int previousPosition = -1; + if (mLinearLayoutManager != null) { + previousPosition = mLinearLayoutManager.findFirstVisibleItemPosition(); + } + + RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + mRecyclerView.setAdapter(null); + mRecyclerView.setLayoutManager(null); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setLayoutManager(layoutManager); + + if (previousPosition > 0) { + mRecyclerView.scrollToPosition(previousPosition); + } + } + } + + private void onRefresh() { + mMessageViewModel.refresh(); + mAdapter.setNetworkState(null); + } + + public PrivateMessage getMessageByIndex(int index) { + if (mMessageViewModel == null || index < 0) { + return null; + } + PagedList messages = mMessageViewModel.getMessages().getValue(); + if (messages == null) { + return null; + } + if (index >= messages.size()) { + return null; + } + + return messages.get(index); + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mActivity = (BaseActivity) context; + } + + @Subscribe + public void onRepliedToPrivateMessageEvent(RepliedToPrivateMessageEvent repliedToPrivateMessageEvent) { + /* if (mAdapter != null && mWhere.equals(FetchMessage.WHERE_MESSAGES)) { + mAdapter.updateMessageReply(repliedToPrivateMessageEvent.newReply, repliedToPrivateMessageEvent.messagePosition); + }*/ + } +} diff --git a/app/src/main/java/eu/toldi/infinityforlemmy/post/ParsePost.java b/app/src/main/java/eu/toldi/infinityforlemmy/post/ParsePost.java index 4ff63591..7c46ec93 100644 --- a/app/src/main/java/eu/toldi/infinityforlemmy/post/ParsePost.java +++ b/app/src/main/java/eu/toldi/infinityforlemmy/post/ParsePost.java @@ -220,9 +220,6 @@ public class ParsePost { String authorAvatar = (!data.getJSONObject("creator").isNull("avatar")) ? data.getJSONObject("creator").getString("avatar") : null; Uri uri = Uri.parse(url); - if (uri.getAuthority() == null) { - Log.e("ParsePost", "parseData:" + uri.toString()); - } String path = uri.getPath(); boolean isVideo = path.endsWith(".mp4") || path.endsWith(".webm") || path.endsWith(".gifv"); diff --git a/app/src/main/kotlin/eu/toldi/infinityforlemmy/dto/PrivateMessageDTO.kt b/app/src/main/kotlin/eu/toldi/infinityforlemmy/dto/PrivateMessageDTO.kt new file mode 100644 index 00000000..934c4dd0 --- /dev/null +++ b/app/src/main/kotlin/eu/toldi/infinityforlemmy/dto/PrivateMessageDTO.kt @@ -0,0 +1,23 @@ +package eu.toldi.infinityforlemmy.dto + +data class PrivateMessageDTO(val recipient_id: Int, val content: String, val auth: String) + +data class PrivateMessageUpdateDTO( + val private_message_id: Int, + val auth: String, + val content: String +) + +data class PrivateMessageDeleteDTO( + val private_message_id: Int, + val auth: String, + val deleted: Boolean +) + +data class PrivateMessageReadDTO(val private_message_id: Int, val auth: String, val read: Boolean) + +data class PrivateMessageReportDTO( + val private_message_id: Int, + val auth: String, + val reason: String +) \ No newline at end of file diff --git a/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/LemmyPrivateMessageAPI.kt b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/LemmyPrivateMessageAPI.kt new file mode 100644 index 00000000..da182a8c --- /dev/null +++ b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/LemmyPrivateMessageAPI.kt @@ -0,0 +1,107 @@ +package eu.toldi.infinityforlemmy.privatemessage + +import eu.toldi.infinityforlemmy.RetrofitHolder +import eu.toldi.infinityforlemmy.apis.LemmyAPI +import eu.toldi.infinityforlemmy.dto.PrivateMessageReadDTO +import org.json.JSONObject + +class LemmyPrivateMessageAPI(val retrofitHolder: RetrofitHolder) { + + + fun fetchPrivateMessages( + auth: String, + page: Int, + listener: PrivateMessageFetchedListener, + limit: Int = 25, + unreadOnly: Boolean = false + ) { + val api = retrofitHolder.retrofit.create(LemmyAPI::class.java) + api.privateMessagesList(page, limit, unreadOnly, auth).enqueue( + object : retrofit2.Callback { + override fun onResponse( + call: retrofit2.Call, + response: retrofit2.Response + ) { + if (response.isSuccessful) { + val jresponse = JSONObject(response.body()!!); + val privateMessages = jresponse.getJSONArray("private_messages") + val privateMessageList = mutableListOf() + for (i in 0 until privateMessages.length()) { + val privateMessage = + parsePrivateMessage(privateMessages.getJSONObject(i)) + privateMessageList.add(privateMessage) + } + listener.onPrivateMessageFetchedSuccess(privateMessageList) + } else { + listener.onPrivateMessageFetchedError() + } + } + + override fun onFailure(call: retrofit2.Call, t: Throwable) { + listener.onPrivateMessageFetchedError() + } + } + ) + } + + fun markPrivateMessageAsRead( + auth: String, + privateMessageId: Int, + listener: PrivateMessageMarkedAsReadListener + ) { + val api = retrofitHolder.retrofit.create(LemmyAPI::class.java) + api.privateMessageMarkAsRead(PrivateMessageReadDTO(privateMessageId, auth, true)).enqueue( + object : retrofit2.Callback { + override fun onResponse( + call: retrofit2.Call, + response: retrofit2.Response + ) { + if (response.isSuccessful) { + listener.onPrivateMessageMarkedAsReadSuccess() + } else { + listener.onPrivateMessageMarkedAsReadError() + } + } + + override fun onFailure(call: retrofit2.Call, t: Throwable) { + listener.onPrivateMessageMarkedAsReadError() + } + } + ) + } + + interface PrivateMessageMarkedAsReadListener { + fun onPrivateMessageMarkedAsReadSuccess() + fun onPrivateMessageMarkedAsReadError() + } + + + interface PrivateMessageFetchedListener { + fun onPrivateMessageFetchedSuccess(privateMessage: List) + fun onPrivateMessageFetchedError() + } + + private fun parsePrivateMessage(jsonObject: JSONObject): PrivateMessage { + + val privateMessage = jsonObject.getJSONObject("private_message") + val creator = jsonObject.getJSONObject("creator") + val recipient = jsonObject.getJSONObject("recipient") + + return PrivateMessage( + id = privateMessage.getInt("id"), + creatorId = privateMessage.getInt("creator_id"), + recipientId = privateMessage.getInt("recipient_id"), + content = privateMessage.getString("content"), + deleted = privateMessage.getBoolean("deleted"), + read = privateMessage.getBoolean("read"), + published = privateMessage.getString("published"), + updated = privateMessage.optString("updated", ""), + creatorName = creator.getString("name"), + creatorAvatar = creator.optString("avatar", ""), + creatorQualifiedName = creator.getString("actor_id"), + recipientName = recipient.getString("name"), + recipientAvatar = recipient.optString("avatar", ""), + recipientQualifiedName = recipient.getString("actor_id") + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessage.kt b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessage.kt new file mode 100644 index 00000000..6ade6733 --- /dev/null +++ b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessage.kt @@ -0,0 +1,18 @@ +package eu.toldi.infinityforlemmy.privatemessage + +data class PrivateMessage( + val id: Int, + val creatorId: Int, + val recipientId: Int, + val content: String, + val deleted: Boolean, + var read: Boolean, + val published: String, + val updated: String, + val creatorName: String, + val creatorAvatar: String, + val creatorQualifiedName: String, + val recipientName: String, + val recipientAvatar: String, + val recipientQualifiedName: String +) diff --git a/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageDataSource.kt b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageDataSource.kt new file mode 100644 index 00000000..d151bdd4 --- /dev/null +++ b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageDataSource.kt @@ -0,0 +1,103 @@ +package eu.toldi.infinityforlemmy.privatemessage + +import androidx.lifecycle.MutableLiveData +import androidx.paging.PageKeyedDataSource +import eu.toldi.infinityforlemmy.NetworkState +import retrofit2.Retrofit +import java.util.Locale + + +class PrivateMessageDataSource( + private val retrofit: Retrofit, + private val locale: Locale, + private val accessToken: String, + private val mLemmyPrivateMessageAPI: LemmyPrivateMessageAPI +) : + PageKeyedDataSource() { + + + val paginationNetworkStateLiveData: MutableLiveData + val initialLoadStateLiveData: MutableLiveData + private val hasPostLiveData: MutableLiveData + private var params: LoadParams? = null + private var callback: LoadCallback? = null + private val page = 1 + + init { + paginationNetworkStateLiveData = MutableLiveData() + initialLoadStateLiveData = MutableLiveData() + hasPostLiveData = MutableLiveData() + } + + fun hasPostLiveData(): MutableLiveData { + return hasPostLiveData + } + + fun retryLoadingMore() { + loadAfter(params!!, callback!!) + } + + override fun loadInitial( + params: LoadInitialParams, + callback: LoadInitialCallback + ) { + initialLoadStateLiveData.postValue(NetworkState.LOADING) + mLemmyPrivateMessageAPI.fetchPrivateMessages(accessToken, page, object : + LemmyPrivateMessageAPI.PrivateMessageFetchedListener { + override fun onPrivateMessageFetchedSuccess(privateMessages: List) { + hasPostLiveData.postValue(true) + if (privateMessages.isEmpty()) { + callback.onResult(ArrayList(), null, null) + } else { + callback.onResult(privateMessages, null, page + 1) + } + initialLoadStateLiveData.postValue(NetworkState.LOADED) + } + + override fun onPrivateMessageFetchedError() { + initialLoadStateLiveData.postValue( + NetworkState( + NetworkState.Status.FAILED, + "Error fetch messages" + ) + ) + } + }) + } + + override fun loadBefore( + params: LoadParams, + callback: LoadCallback + ) { + } + + override fun loadAfter( + params: LoadParams, + callback: LoadCallback + ) { + this.params = params + this.callback = callback + paginationNetworkStateLiveData.postValue(NetworkState.LOADING) + mLemmyPrivateMessageAPI.fetchPrivateMessages(accessToken, params.key, object : + LemmyPrivateMessageAPI.PrivateMessageFetchedListener { + override fun onPrivateMessageFetchedSuccess(privateMessages: List) { + hasPostLiveData.postValue(true) + if (privateMessages.isEmpty()) { + callback.onResult(ArrayList(), null) + } else { + callback.onResult(privateMessages, params.key + 1) + } + paginationNetworkStateLiveData.postValue(NetworkState.LOADED) + } + + override fun onPrivateMessageFetchedError() { + paginationNetworkStateLiveData.postValue( + NetworkState( + NetworkState.Status.FAILED, + "Error fetch messages" + ) + ) + } + }) + } +} diff --git a/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageDataSourceFactory.kt b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageDataSourceFactory.kt new file mode 100644 index 00000000..baf48cbc --- /dev/null +++ b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageDataSourceFactory.kt @@ -0,0 +1,37 @@ +package eu.toldi.infinityforlemmy.privatemessage + +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import retrofit2.Retrofit +import java.util.Locale + + +internal class PrivateMessageDataSourceFactory( + retrofit: Retrofit, + locale: Locale, + accessToken: String, + private var mLemmyPrivateMessageAPI: LemmyPrivateMessageAPI +) : DataSource.Factory() { + private val mPrivateMessageDataSource: PrivateMessageDataSource = + PrivateMessageDataSource(retrofit, locale, accessToken, mLemmyPrivateMessageAPI) + private val messageDataSourceLiveData: MutableLiveData = + MutableLiveData() + + override fun create(): DataSource { + messageDataSourceLiveData.postValue(mPrivateMessageDataSource) + return mPrivateMessageDataSource + } + + fun getMessageDataSourceLiveData(): MutableLiveData { + return messageDataSourceLiveData + } + + fun getMessageDataSource(): PrivateMessageDataSource { + return mPrivateMessageDataSource + } + + fun changeLemmPrivateMessageAPI(lemmyPrivateMessageAPI: LemmyPrivateMessageAPI) { + this.mLemmyPrivateMessageAPI = lemmyPrivateMessageAPI + } + +} diff --git a/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageViewModel.kt b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageViewModel.kt new file mode 100644 index 00000000..835178ae --- /dev/null +++ b/app/src/main/kotlin/eu/toldi/infinityforlemmy/privatemessage/PrivateMessageViewModel.kt @@ -0,0 +1,92 @@ +package eu.toldi.infinityforlemmy.privatemessage + +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 eu.toldi.infinityforlemmy.NetworkState +import retrofit2.Retrofit +import java.util.Locale + + +class PrivateMessageViewModel( + retrofit: Retrofit, + locale: Locale, + accessToken: String, + lemmyPrivateMessageAPI: LemmyPrivateMessageAPI +) : + ViewModel() { + private val messageDataSourceFactory: PrivateMessageDataSourceFactory + val paginationNetworkState: LiveData + val initialLoadingState: LiveData + private val hasMessageLiveData: LiveData + val messages: LiveData> + private val whereLiveData: MutableLiveData = MutableLiveData() + + init { + messageDataSourceFactory = + PrivateMessageDataSourceFactory(retrofit, locale, accessToken, lemmyPrivateMessageAPI) + initialLoadingState = Transformations.switchMap( + messageDataSourceFactory.getMessageDataSourceLiveData(), + PrivateMessageDataSource::initialLoadStateLiveData + ) + paginationNetworkState = Transformations.switchMap( + messageDataSourceFactory.getMessageDataSourceLiveData(), + PrivateMessageDataSource::paginationNetworkStateLiveData + ) + hasMessageLiveData = Transformations.switchMap( + messageDataSourceFactory.getMessageDataSourceLiveData(), + PrivateMessageDataSource::hasPostLiveData + ) + + whereLiveData.postValue(lemmyPrivateMessageAPI) + + val pagedListConfig: PagedList.Config = PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setPageSize(25) + .build() + messages = Transformations.switchMap>( + whereLiveData + ) { newWhere: LemmyPrivateMessageAPI? -> + messageDataSourceFactory.changeLemmPrivateMessageAPI(whereLiveData.value!!) + LivePagedListBuilder( + messageDataSourceFactory, + pagedListConfig + ).build() + } + + } + + fun hasMessage(): LiveData { + return hasMessageLiveData + } + + fun refresh() { + messageDataSourceFactory.getMessageDataSource().invalidate() + } + + fun retryLoadingMore() { + messageDataSourceFactory.getMessageDataSource().retryLoadingMore() + } + + class Factory( + private val retrofit: Retrofit, + private val locale: Locale, + private val accessToken: String, + private val lemmyPrivateMessageAPI: LemmyPrivateMessageAPI + ) : + ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T { + return PrivateMessageViewModel( + retrofit, + locale, + accessToken, + lemmyPrivateMessageAPI + ) as T + } + } +}