diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java index 788ffceb66e0..d162550a675c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java @@ -1,10 +1,6 @@ package org.wordpress.android.ui.comments; import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.Editable; @@ -12,7 +8,6 @@ import android.text.TextWatcher; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; @@ -21,15 +16,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.elevation.ElevationOverlayProvider; -import com.google.android.material.snackbar.Snackbar; import com.gravatar.AvatarQueryOptions; import com.gravatar.AvatarUrl; import com.gravatar.types.Email; @@ -42,7 +33,6 @@ import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.analytics.AnalyticsTracker.Stat; -import org.wordpress.android.databinding.CommentActionFooterBinding; import org.wordpress.android.databinding.CommentDetailFragmentBinding; import org.wordpress.android.databinding.ReaderIncludeCommentBoxBinding; import org.wordpress.android.datasets.ReaderPostTable; @@ -56,10 +46,10 @@ import org.wordpress.android.fluxc.store.CommentStore.OnCommentChanged; import org.wordpress.android.fluxc.store.CommentStore.RemoteCommentPayload; import org.wordpress.android.fluxc.store.CommentStore.RemoteCreateCommentPayload; -import org.wordpress.android.fluxc.store.CommentStore.RemoteLikeCommentPayload; import org.wordpress.android.fluxc.store.CommentsStore; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.fluxc.tools.FluxCImageLoader; +import org.wordpress.android.fluxc.tools.FormattableContentMapper; import org.wordpress.android.models.Note; import org.wordpress.android.models.Note.EnabledActions; import org.wordpress.android.models.UserSuggestion; @@ -73,17 +63,17 @@ import org.wordpress.android.ui.ViewPagerFragment; import org.wordpress.android.ui.comments.CommentActions.OnCommentActionListener; import org.wordpress.android.ui.comments.CommentActions.OnNoteCommentActionListener; +import org.wordpress.android.ui.comments.unified.CommentActionPopupHandler; import org.wordpress.android.ui.comments.unified.CommentIdentifier; -import org.wordpress.android.ui.comments.unified.CommentIdentifier.NotificationCommentIdentifier; -import org.wordpress.android.ui.comments.unified.CommentIdentifier.SiteCommentIdentifier; import org.wordpress.android.ui.comments.unified.CommentSource; import org.wordpress.android.ui.comments.unified.CommentsStoreAdapter; import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditActivity; +import org.wordpress.android.ui.engagement.BottomSheetUiState.UserProfileUiState; +import org.wordpress.android.ui.engagement.UserProfileBottomSheetFragment; import org.wordpress.android.ui.notifications.NotificationEvents; import org.wordpress.android.ui.notifications.NotificationFragment; import org.wordpress.android.ui.notifications.NotificationsDetailListFragment; import org.wordpress.android.ui.reader.ReaderActivityLauncher; -import org.wordpress.android.ui.reader.ReaderAnim; import org.wordpress.android.ui.reader.actions.ReaderActions; import org.wordpress.android.ui.reader.actions.ReaderPostActions; import org.wordpress.android.ui.suggestion.Suggestion; @@ -94,7 +84,6 @@ import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.ColorUtils; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.EditTextUtils; import org.wordpress.android.util.NetworkUtils; @@ -103,11 +92,9 @@ import org.wordpress.android.util.WPAvatarUtils; import org.wordpress.android.util.WPLinkMovementMethod; import org.wordpress.android.util.analytics.AnalyticsUtils; -import org.wordpress.android.util.extensions.ContextExtensionsKt; import org.wordpress.android.util.extensions.ViewExtensionsKt; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; -import org.wordpress.android.widgets.WPSnackbar; import java.util.EnumSet; import java.util.List; @@ -139,7 +126,6 @@ public abstract class CommentDetailFragment extends ViewPagerFragment implements protected static final String KEY_COMMENT_ID = "KEY_COMMENT_ID"; protected static final String KEY_NOTE_ID = "KEY_NOTE_ID"; protected static final int INTENT_COMMENT_EDITOR = 1010; - protected static final float NORMAL_OPACITY = 1f; @Nullable protected CommentModel mComment; @Nullable protected SiteModel mSite; @@ -161,6 +147,7 @@ public abstract class CommentDetailFragment extends ViewPagerFragment implements @Inject ImageManager mImageManager; @Inject CommentsStore mCommentsStore; @Inject LocalCommentCacheUpdateHandler mLocalCommentCacheUpdateHandler; + @Inject FormattableContentMapper mContentMapper; private boolean mIsSubmittingReply = false; @Nullable private NotificationsDetailListFragment mNotificationsDetailListFragment; @@ -178,7 +165,19 @@ public abstract class CommentDetailFragment extends ViewPagerFragment implements @Nullable protected CommentDetailFragmentBinding mBinding = null; @Nullable protected ReaderIncludeCommentBoxBinding mReplyBinding = null; - @Nullable protected CommentActionFooterBinding mActionBinding = null; + + private final OnActionClickListener mOnActionClickListener = new OnActionClickListener() { + @Override public void onEditCommentClicked() { + editComment(); + } + + @Override public void onUserInfoClicked() { + UserProfileBottomSheetFragment.newInstance(getUserProfileUiState()) + .show(getChildFragmentManager(), UserProfileBottomSheetFragment.TAG); + } + }; + + abstract UserProfileUiState getUserProfileUiState(); @Override @SuppressWarnings("deprecation") @@ -214,7 +213,6 @@ public View onCreateView( ) { mBinding = CommentDetailFragmentBinding.inflate(inflater, container, false); mReplyBinding = mBinding.layoutCommentBox; - mActionBinding = CommentActionFooterBinding.inflate(inflater, null, false); mMediumOpacity = ResourcesCompat.getFloat( getResources(), @@ -290,16 +288,14 @@ public void afterTextChanged(@NonNull Editable s) { ViewExtensionsKt.redirectContextClickToLongPressListener(mReplyBinding.buttonExpand); setReplyUniqueId(mReplyBinding, mSite, mComment, mNote); - // hide comment like button until we know it can be enabled in showCommentAsNotification() - mActionBinding.btnLike.setVisibility(View.GONE); - - // hide moderation buttons until updateModerationButtons() is called - mActionBinding.layoutButtons.setVisibility(View.GONE); - // this is necessary in order for anchor tags in the comment text to be clickable mBinding.textContent.setLinksClickable(true); mBinding.textContent.setMovementMethod(WPLinkMovementMethod.getInstance()); + mBinding.imageMore.setOnClickListener(v -> + CommentActionPopupHandler.show(mBinding.imageMore, mOnActionClickListener) + ); + mReplyBinding.editComment.setHint(R.string.reader_hint_comment_on_comment); mReplyBinding.editComment.setOnEditorActionListener((v, actionId, event) -> { if (mSite != null && mComment != null @@ -320,32 +316,6 @@ public void afterTextChanged(@NonNull Editable s) { } }); - mActionBinding.btnSpam.setOnClickListener(v -> { - if (mSite != null && mComment != null) { - if (CommentStatus.fromString(mComment.getStatus()) == CommentStatus.SPAM) { - moderateComment(mBinding, mActionBinding, mSite, mComment, mNote, CommentStatus.APPROVED); - announceCommentStatusChangeForAccessibility(CommentStatus.UNSPAM); - } else { - moderateComment(mBinding, mActionBinding, mSite, mComment, mNote, CommentStatus.SPAM); - announceCommentStatusChangeForAccessibility(CommentStatus.SPAM); - } - } - }); - - mActionBinding.btnLike.setOnClickListener(v -> { - if (mSite != null && mComment != null) { - likeComment(mActionBinding, mSite, mComment, false); - } - }); - - mActionBinding.btnMore.setOnClickListener(v -> { - if (mSite != null && mComment != null) { - showMoreMenu(mBinding, mActionBinding, mSite, mComment, mNote, v); - } - }); - // hide more button until we know it can be enabled - mActionBinding.btnMore.setVisibility(View.GONE); - if (mSite != null) { setupSuggestionServiceAndAdapter(mReplyBinding, mSite); } @@ -445,8 +415,8 @@ protected void setComment( // notification about a reply to a comment this user posted on someone else's blog mIsUsersBlog = (comment != null); - if (mBinding != null && mReplyBinding != null && mActionBinding != null) { - showComment(mBinding, mReplyBinding, mActionBinding, mSite, mComment, mNote); + if (mBinding != null && mReplyBinding != null) { + showComment(mBinding, mReplyBinding, mSite, mComment, mNote); } // Reset the reply unique id since mComment just changed. @@ -493,8 +463,8 @@ public void onStart() { super.onStart(); EventBus.getDefault().register(this); mCommentsStoreAdapter.register(this); - if (mBinding != null && mReplyBinding != null && mActionBinding != null && mSite != null) { - showComment(mBinding, mReplyBinding, mActionBinding, mSite, mComment, mNote); + if (mBinding != null && mReplyBinding != null && mSite != null) { + showComment(mBinding, mReplyBinding, mSite, mComment, mNote); } } @@ -556,7 +526,7 @@ private void reloadComment( * open the comment for editing */ @SuppressWarnings("deprecation") - private void editComment(@NonNull SiteModel site) { + private void editComment() { if (!isAdded()) { return; } @@ -564,19 +534,19 @@ private void editComment(@NonNull SiteModel site) { AnalyticsUtils.trackCommentActionWithSiteDetails( Stat.COMMENT_EDITOR_OPENED, mCommentSource.toAnalyticsCommentActionSource(), - site + mSite ); } // IMPORTANT: don't use getActivity().startActivityForResult() or else onActivityResult() // won't be called in this fragment // https://code.google.com/p/android/issues/detail?id=15394#c45 - final CommentIdentifier commentIdentifier = mapCommentIdentifier(); + final CommentIdentifier commentIdentifier = getCommentIdentifier(); if (commentIdentifier != null) { final Intent intent = UnifiedCommentsEditActivity.createIntent( requireActivity(), commentIdentifier, - site + mSite ); startActivityForResult(intent, INTENT_COMMENT_EDITOR); } else { @@ -584,26 +554,7 @@ private void editComment(@NonNull SiteModel site) { } } - @Nullable - private CommentIdentifier mapCommentIdentifier() { - if (mCommentSource == null) return null; - switch (mCommentSource) { - case SITE_COMMENTS: - if (mComment != null) { - return new SiteCommentIdentifier(mComment.getId(), mComment.getRemoteCommentId()); - } else { - return null; - } - case NOTIFICATION: - if (mNote != null) { - return new NotificationCommentIdentifier(mNote.getId(), mNote.getCommentId()); - } else { - return null; - } - default: - return null; - } - } + abstract CommentIdentifier getCommentIdentifier(); /* * display the current comment @@ -611,7 +562,6 @@ private CommentIdentifier mapCommentIdentifier() { protected void showComment( @NonNull CommentDetailFragmentBinding binding, @NonNull ReaderIncludeCommentBoxBinding replyBinding, - @NonNull CommentActionFooterBinding actionBinding, @NonNull SiteModel site, @Nullable CommentModel comment, @Nullable Note note @@ -622,16 +572,15 @@ protected void showComment( if (comment == null) { // Hide container views when comment is null (will happen when opened from a notification). - showCommentWhenNull(binding, replyBinding, actionBinding, note); + showCommentWhenNull(binding, replyBinding, note); } else { - showCommentWhenNonNull(binding, replyBinding, actionBinding, site, comment, note); + showCommentWhenNonNull(binding, replyBinding, site, comment); } } private void showCommentWhenNull( @NonNull CommentDetailFragmentBinding binding, @NonNull ReaderIncludeCommentBoxBinding replyBinding, - @NonNull CommentActionFooterBinding actionBinding, @Nullable Note note ) { // These two views contain all the other views except the progress bar. @@ -650,7 +599,7 @@ private void showCommentWhenNull( CommentModel comment = mCommentsStoreAdapter.getCommentBySiteAndRemoteId(site, note.getCommentId()); if (comment != null) { // It exists, then show it as a "Notification" - showCommentAsNotification(binding, replyBinding, actionBinding, site, comment, note); + showCommentAsNotification(binding, replyBinding, site, comment, note); } else { // It's not in our store yet, request it. RemoteCommentPayload payload = new RemoteCommentPayload(site, note.getCommentId()); @@ -659,7 +608,7 @@ private void showCommentWhenNull( // Show a "temporary" comment built from the note data, the view will be refreshed once the // comment has been fetched. - showCommentAsNotification(binding, replyBinding, actionBinding, site, null, note); + showCommentAsNotification(binding, replyBinding, site, null, note); } } } @@ -667,28 +616,16 @@ private void showCommentWhenNull( private void showCommentWhenNonNull( @NonNull CommentDetailFragmentBinding binding, @NonNull ReaderIncludeCommentBoxBinding replyBinding, - @NonNull CommentActionFooterBinding actionBinding, @NonNull SiteModel site, - @NonNull CommentModel comment, - @Nullable Note note + @NonNull CommentModel comment ) { // These two views contain all the other views except the progress bar. binding.nestedScrollView.setVisibility(View.VISIBLE); binding.layoutBottom.setVisibility(View.VISIBLE); - // Add action buttons footer - if (note == null && actionBinding.layoutButtons.getParent() == null) { - binding.commentContentContainer.addView(actionBinding.layoutButtons); - } - binding.textName.setText( comment.getAuthorName() == null ? getString(R.string.anonymous) : comment.getAuthorName() ); - binding.textDate.setText( - DateTimeUtils.javaDateToTimeSpan( - DateTimeUtils.dateFromIso8601(comment.getDatePublished()), WordPress.getContext() - ) - ); String renderingError = getString(R.string.comment_unable_to_show_error); binding.textContent.post(() -> CommentUtils.displayHtmlComment( @@ -709,26 +646,17 @@ private void showCommentWhenNonNull( } mImageManager.loadIntoCircle(binding.imageAvatar, ImageType.AVATAR_WITH_BACKGROUND, avatarUrl); - updateStatusViews(binding, actionBinding, site, comment, note); - // navigate to author's blog when avatar or name clicked if (comment.getAuthorUrl() != null) { View.OnClickListener authorListener = v -> ReaderActivityLauncher.openUrl(getActivity(), comment.getAuthorUrl()); binding.imageAvatar.setOnClickListener(authorListener); binding.textName.setOnClickListener(authorListener); - binding.textName.setTextColor(ContextExtensionsKt.getColorFromAttribute( - binding.textName.getContext(), - com.google.android.material.R.attr.colorPrimary) - ); - } else { - binding.textName.setTextColor(ContextExtensionsKt.getColorFromAttribute( - binding.textName.getContext(), - com.google.android.material.R.attr.colorOnSurface) - ); } showPostTitle(binding, comment, site); + binding.textSite.setText(DateTimeUtils.javaDateToTimeSpan( + DateTimeUtils.dateFromIso8601(comment.getDatePublished()), WordPress.getContext())); // make sure reply box is showing if (replyBinding.layoutContainer.getVisibility() != View.VISIBLE && canReply()) { @@ -922,8 +850,6 @@ private void trackModerationEvent(final CommentStatus newStatus) { * approve, disapprove, spam, or trash the current comment */ private void moderateComment( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, @NonNull SiteModel site, @NonNull CommentModel comment, @Nullable Note note, @@ -961,8 +887,6 @@ private void moderateComment( // Sad, but onModerateComment does the moderation itself (due to the undo bar), this should be refactored, // That's why we don't call dispatchModerationAction() here. } - - updateStatusViews(binding, actionBinding, site, comment, note); } private void dispatchModerationAction( @@ -1032,223 +956,16 @@ private void submitReply( ); } - /* - * update the text, drawable & click listener for mBtnModerate based on - * the current status of the comment, show mBtnSpam if the comment isn't - * already marked as spam, and show the current status of the comment - */ - private void updateStatusViews( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, - @NonNull SiteModel site, - @NonNull CommentModel comment, - @Nullable Note note - ) { - if (!isAdded()) { - return; - } - - final int statusTextResId; // string resource id for status text - final int statusColor; // color for status text - - CommentStatus commentStatus = CommentStatus.fromString(comment.getStatus()); - switch (commentStatus) { - case APPROVED: - statusTextResId = R.string.comment_status_approved; - statusColor = ContextExtensionsKt.getColorFromAttribute( - requireActivity(), - R.attr.wpColorWarningDark - ); - break; - case UNAPPROVED: - statusTextResId = R.string.comment_status_unapproved; - statusColor = ContextExtensionsKt.getColorFromAttribute( - requireActivity(), - R.attr.wpColorWarningDark - ); - break; - case SPAM: - statusTextResId = R.string.comment_status_spam; - statusColor = ContextExtensionsKt.getColorFromAttribute( - requireActivity(), - com.google.android.material.R.attr.colorError - ); - break; - case DELETED: - case ALL: - case UNREPLIED: - case UNSPAM: - case UNTRASH: - case TRASH: - default: - statusTextResId = R.string.comment_status_trash; - statusColor = ContextExtensionsKt.getColorFromAttribute( - requireActivity(), - com.google.android.material.R.attr.colorError - ); - break; - } - - if (canLike(site)) { - actionBinding.btnLike.setVisibility(View.VISIBLE); - toggleLikeButton(actionBinding, comment.getILike()); - } - - // comment status is only shown if this comment is from one of this user's blogs and the - // comment hasn't been CommentStatus.APPROVED - if (mIsUsersBlog && commentStatus != CommentStatus.APPROVED) { - binding.textStatus.setText(getString(statusTextResId).toUpperCase(Locale.getDefault())); - binding.textStatus.setTextColor(statusColor); - if (binding.textStatus.getVisibility() != View.VISIBLE) { - binding.textStatus.clearAnimation(); - AniUtils.fadeIn(binding.textStatus, AniUtils.Duration.LONG); - } - } else { - binding.textStatus.setVisibility(View.GONE); - } - - if (canModerate()) { - setModerateButtonForStatus(actionBinding, commentStatus); - actionBinding.btnModerate.setOnClickListener( - v -> performModerateAction(binding, actionBinding, site, comment, note) - ); - actionBinding.btnModerate.setVisibility(View.VISIBLE); - } else { - actionBinding.btnModerate.setVisibility(View.GONE); - } - - if (canMarkAsSpam()) { - actionBinding.btnSpam.setVisibility(View.VISIBLE); - if (commentStatus == CommentStatus.SPAM) { - actionBinding.btnSpamText.setText(R.string.mnu_comment_unspam); - } else { - actionBinding.btnSpamText.setText(R.string.mnu_comment_spam); - } - } else { - actionBinding.btnSpam.setVisibility(View.GONE); - } - - if (canTrash()) { - if (commentStatus == CommentStatus.TRASH) { - ColorUtils.setImageResourceWithTint( - actionBinding.btnModerateIcon, - R.drawable.ic_undo_white_24dp, - ContextExtensionsKt.getColorResIdFromAttribute( - actionBinding.btnModerateText.getContext(), - com.google.android.material.R.attr.colorOnSurface - ) - ); - actionBinding.btnModerateText.setText(R.string.mnu_comment_untrash); - } - } - - if (canShowMore()) { - actionBinding.btnMore.setVisibility(View.VISIBLE); - } else { - actionBinding.btnMore.setVisibility(View.GONE); - } - - actionBinding.layoutButtons.setVisibility(View.VISIBLE); - } - - private void performModerateAction( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, - @NonNull SiteModel site, - @NonNull CommentModel comment, - @Nullable Note note - ) { - if (!isAdded() || !NetworkUtils.checkConnection(getActivity())) { - return; - } - - CommentStatus newStatus = CommentStatus.APPROVED; - CommentStatus currentStatus = CommentStatus.fromString(comment.getStatus()); - if (currentStatus == CommentStatus.APPROVED) { - newStatus = CommentStatus.UNAPPROVED; - } - announceCommentStatusChangeForAccessibility( - currentStatus == CommentStatus.TRASH ? CommentStatus.UNTRASH : newStatus); - - setModerateButtonForStatus(actionBinding, newStatus); - AniUtils.startAnimation(actionBinding.btnModerateIcon, R.anim.notifications_button_scale); - moderateComment(binding, actionBinding, site, comment, note, newStatus); - } - - private void setModerateButtonForStatus( - @NonNull CommentActionFooterBinding actionBinding, - CommentStatus status - ) { - int color; - - if (status == CommentStatus.APPROVED) { - color = ContextExtensionsKt.getColorResIdFromAttribute( - actionBinding.btnModerateText.getContext(), - com.google.android.material.R.attr.colorSecondary - ); - actionBinding.btnModerateText.setText(R.string.comment_status_approved); - actionBinding.btnModerateText.setAlpha(NORMAL_OPACITY); - actionBinding.btnModerateIcon.setAlpha(NORMAL_OPACITY); - } else { - color = ContextExtensionsKt.getColorResIdFromAttribute( - actionBinding.btnModerateText.getContext(), - com.google.android.material.R.attr.colorOnSurface - ); - actionBinding.btnModerateText.setText(R.string.mnu_comment_approve); - actionBinding.btnModerateText.setAlpha(mMediumOpacity); - actionBinding.btnModerateIcon.setAlpha(mMediumOpacity); - } - - ColorUtils.setImageResourceWithTint( - actionBinding.btnModerateIcon, - R.drawable.ic_checkmark_white_24dp, color - ); - actionBinding.btnModerateText.setTextColor(ContextCompat.getColor(requireContext(), color)); - } - - /* - * does user have permission to moderate/reply/spam this comment? - */ - private boolean canModerate() { - return mEnabledActions.contains(EnabledActions.ACTION_APPROVE) - || mEnabledActions.contains(EnabledActions.ACTION_UNAPPROVE); - } - - private boolean canMarkAsSpam() { - return mEnabledActions.contains(EnabledActions.ACTION_SPAM); - } - private boolean canReply() { return mEnabledActions.contains(EnabledActions.ACTION_REPLY); } - private boolean canTrash() { - return canModerate(); - } - - private boolean canEdit(@NonNull SiteModel site) { - return site.getHasCapabilityEditOthersPosts() || site.isSelfHostedAdmin(); - } - - private boolean canLike(@NonNull SiteModel site) { - return mEnabledActions.contains(EnabledActions.ACTION_LIKE_COMMENT) - && SiteUtils.isAccessedViaWPComRest(site); - } - - /* - * The more button contains controls which only moderates can use - */ - private boolean canShowMore() { - return canModerate(); - } - /* * display the comment associated with the passed notification */ private void showCommentAsNotification( @NonNull CommentDetailFragmentBinding binding, @NonNull ReaderIncludeCommentBoxBinding replyBinding, - @NonNull CommentActionFooterBinding actionBinding, @NonNull SiteModel site, @Nullable CommentModel comment, @Nullable Note note @@ -1283,7 +1000,7 @@ private void showCommentAsNotification( } if (note != null) { - addDetailFragment(binding, actionBinding, note.getId()); + addDetailFragment(binding, note.getId()); } requireActivity().invalidateOptionsMenu(); @@ -1291,107 +1008,17 @@ private void showCommentAsNotification( private void addDetailFragment( @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, String noteId ) { // Now we'll add a detail fragment list FragmentManager fragmentManager = getChildFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); mNotificationsDetailListFragment = NotificationsDetailListFragment.newInstance(noteId); - mNotificationsDetailListFragment.setFooterView(actionBinding.layoutButtons); + mNotificationsDetailListFragment.setOnEditCommentListener(mOnActionClickListener); fragmentTransaction.replace(binding.commentContentContainer.getId(), mNotificationsDetailListFragment); fragmentTransaction.commitAllowingStateLoss(); } - private void likeComment( - @NonNull CommentActionFooterBinding actionBinding, - @NonNull SiteModel site, - @NonNull CommentModel comment, - @SuppressWarnings("SameParameterValue") boolean forceLike - ) { - if (!isAdded()) { - return; - } - if (forceLike && actionBinding.btnLike.isActivated()) { - return; - } - - toggleLikeButton(actionBinding, !actionBinding.btnLike.isActivated()); - - ReaderAnim.animateLikeButton(actionBinding.btnLikeIcon, actionBinding.btnLike.isActivated()); - - // Bump analytics - // TODO klymyam remove legacy comment tracking after new comments are shipped and new funnels are made - if (mCommentSource == CommentSource.NOTIFICATION) { - AnalyticsTracker.track( - actionBinding.btnLike.isActivated() ? Stat.NOTIFICATION_LIKED : Stat.NOTIFICATION_UNLIKED - ); - } - if (mCommentSource != null) { - AnalyticsUtils.trackCommentActionWithSiteDetails( - actionBinding.btnLike.isActivated() ? Stat.COMMENT_LIKED : Stat.COMMENT_UNLIKED, - mCommentSource.toAnalyticsCommentActionSource(), - site - ); - } - - if (mNotificationsDetailListFragment != null) { - // Optimistically set comment to approved when liking an unapproved comment - // WP.com will set a comment to approved if it is liked while unapproved - if (actionBinding.btnLike.isActivated() - && CommentStatus.fromString(comment.getStatus()) == CommentStatus.UNAPPROVED) { - comment.setStatus(CommentStatus.APPROVED.toString()); - mNotificationsDetailListFragment.refreshBlocksForCommentStatus(CommentStatus.APPROVED); - setModerateButtonForStatus(actionBinding, CommentStatus.APPROVED); - } - } - mCommentsStoreAdapter.dispatch(CommentActionBuilder.newLikeCommentAction( - new RemoteLikeCommentPayload(site, comment, actionBinding.btnLike.isActivated())) - ); - if (mNote != null) { - EventBus.getDefault().postSticky(new NotificationEvents - .OnNoteCommentLikeChanged(mNote, actionBinding.btnLike.isActivated())); - } - - actionBinding.btnLike.announceForAccessibility( - getText(actionBinding.btnLike.isActivated() ? R.string.comment_liked_talkback - : R.string.comment_unliked_talkback) - ); - } - - private void toggleLikeButton( - @NonNull CommentActionFooterBinding actionBinding, - boolean isLiked - ) { - int color; - int drawable; - - if (isLiked) { - color = ContextExtensionsKt.getColorResIdFromAttribute( - actionBinding.btnLikeIcon.getContext(), - com.google.android.material.R.attr.colorSecondary - ); - drawable = R.drawable.ic_star_white_24dp; - actionBinding.btnLikeText.setText(getResources().getString(R.string.mnu_comment_liked)); - actionBinding.btnLike.setActivated(true); - actionBinding.btnLikeText.setAlpha(NORMAL_OPACITY); - actionBinding.btnLikeIcon.setAlpha(NORMAL_OPACITY); - } else { - color = ContextExtensionsKt.getColorResIdFromAttribute( - actionBinding.btnLikeIcon.getContext(), - com.google.android.material.R.attr.colorOnSurface - ); - drawable = R.drawable.ic_star_outline_white_24dp; - actionBinding.btnLikeText.setText(getResources().getString(R.string.reader_label_like)); - actionBinding.btnLike.setActivated(false); - actionBinding.btnLikeText.setAlpha(mMediumOpacity); - actionBinding.btnLikeIcon.setAlpha(mMediumOpacity); - } - - ColorUtils.setImageResourceWithTint(actionBinding.btnLikeIcon, drawable, color); - actionBinding.btnLikeText.setTextColor(ContextCompat.getColor(requireContext(), color)); - } - private void setProgressVisible( @NonNull CommentDetailFragmentBinding binding, boolean visible @@ -1402,8 +1029,6 @@ private void setProgressVisible( } private void onCommentModerated( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, @NonNull SiteModel site, @NonNull CommentModel comment, @Nullable Note note, @@ -1420,7 +1045,6 @@ private void onCommentModerated( if (event.isError()) { comment.setStatus(mPreviousStatus); - updateStatusViews(binding, actionBinding, site, comment, note); ToastUtils.showToast(requireActivity(), R.string.error_moderate_comment); } else { reloadComment(site, comment, note); @@ -1429,9 +1053,7 @@ private void onCommentModerated( @SuppressWarnings("deprecation") private void onCommentCreated( - @NonNull CommentDetailFragmentBinding binding, @NonNull ReaderIncludeCommentBoxBinding replyBinding, - @NonNull CommentActionFooterBinding actionBinding, @NonNull SiteModel site, @NonNull CommentModel comment, @Nullable Note note, @@ -1441,7 +1063,6 @@ private void onCommentCreated( replyBinding.editComment.setEnabled(true); replyBinding.btnSubmitReply.setVisibility(View.VISIBLE); replyBinding.progressSubmitComment.setVisibility(View.GONE); - updateStatusViews(binding, actionBinding, site, comment, note); if (event.isError()) { if (isAdded()) { @@ -1474,23 +1095,7 @@ private void onCommentCreated( // approve the comment if (!(CommentStatus.fromString(comment.getStatus()) == CommentStatus.APPROVED)) { - moderateComment(binding, actionBinding, site, comment, note, CommentStatus.APPROVED); - } - } - - private void onCommentLiked( - @NonNull CommentActionFooterBinding actionBinding, - @Nullable Note note, - OnCommentChanged event - ) { - // send signal for listeners to perform any needed updates - if (note != null) { - EventBus.getDefault().postSticky(new NotificationEvents.NoteLikeOrModerationStatusChanged(note.getId())); - } - - if (event.isError()) { - // Revert button state in case of an error - toggleLikeButton(actionBinding, !actionBinding.btnLike.isActivated()); + moderateComment(site, comment, note, CommentStatus.APPROVED); } } @@ -1507,29 +1112,23 @@ public void onCommentChanged(OnCommentChanged event) { (coroutineScope, continuation) -> mLocalCommentCacheUpdateHandler.requestCommentsUpdate(continuation) ); - if (mBinding != null && mReplyBinding != null && mActionBinding != null) { + if (mBinding != null && mReplyBinding != null) { setProgressVisible(mBinding, false); if (mSite != null && mComment != null) { // Moderating comment if (event.causeOfChange == CommentAction.PUSH_COMMENT) { - onCommentModerated(mBinding, mActionBinding, mSite, mComment, mNote, event); + onCommentModerated(mSite, mComment, mNote, event); mPreviousStatus = null; return; } // New comment (reply) if (event.causeOfChange == CommentAction.CREATE_NEW_COMMENT) { - onCommentCreated(mBinding, mReplyBinding, mActionBinding, mSite, mComment, mNote, event); + onCommentCreated(mReplyBinding, mSite, mComment, mNote, event); return; } } - - // Like/Unlike - if (event.causeOfChange == CommentAction.LIKE_COMMENT) { - onCommentLiked(mActionBinding, mNote, event); - return; - } } if (event.isError()) { @@ -1540,168 +1139,6 @@ public void onCommentChanged(OnCommentChanged event) { } } - private void announceCommentStatusChangeForAccessibility(CommentStatus newStatus) { - int resId = -1; - switch (newStatus) { - case APPROVED: - resId = R.string.comment_approved_talkback; - break; - case UNAPPROVED: - resId = R.string.comment_unapproved_talkback; - break; - case SPAM: - resId = R.string.comment_spam_talkback; - break; - case TRASH: - resId = R.string.comment_trash_talkback; - break; - case DELETED: - resId = R.string.comment_delete_talkback; - break; - case UNSPAM: - resId = R.string.comment_unspam_talkback; - break; - case UNTRASH: - resId = R.string.comment_untrash_talkback; - break; - case UNREPLIED: - case ALL: - // ignore - break; - default: - AppLog.w(T.COMMENTS, - "AnnounceCommentStatusChangeForAccessibility - Missing switch branch for comment status: " - + newStatus); - } - if (resId != -1 && getView() != null) { - getView().announceForAccessibility(getText(resId)); - } - } - - // Handle More Menu - private void showMoreMenu( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, - @NonNull SiteModel site, - @NonNull CommentModel comment, - @Nullable Note note, - View view - ) { - androidx.appcompat.widget.PopupMenu morePopupMenu = - new androidx.appcompat.widget.PopupMenu(requireContext(), view); - morePopupMenu.setOnMenuItemClickListener(item -> { - if (item.getItemId() == R.id.action_edit) { - editComment(site); - return true; - } - if (item.getItemId() == R.id.action_trash) { - trashComment(binding, actionBinding, site, comment, note); - return true; - } - if (item.getItemId() == R.id.action_copy_link_address) { - copyCommentLinkAddress(binding, comment); - return true; - } - return false; - }); - - morePopupMenu.inflate(R.menu.menu_comment_more); - - MenuItem trashMenuItem = morePopupMenu.getMenu().findItem(R.id.action_trash); - MenuItem copyLinkAddress = morePopupMenu.getMenu().findItem(R.id.action_copy_link_address); - if (canTrash()) { - CommentStatus commentStatus = CommentStatus.fromString(comment.getStatus()); - if (commentStatus == CommentStatus.TRASH) { - copyLinkAddress.setVisible(false); - trashMenuItem.setTitle(R.string.mnu_comment_delete_permanently); - } else { - trashMenuItem.setTitle(R.string.mnu_comment_trash); - copyLinkAddress.setVisible(commentStatus != CommentStatus.SPAM); - } - } else { - trashMenuItem.setVisible(false); - copyLinkAddress.setVisible(false); - } - - MenuItem editMenuItem = morePopupMenu.getMenu().findItem(R.id.action_edit); - editMenuItem.setVisible(false); - if (canEdit(site)) { - editMenuItem.setVisible(true); - } - morePopupMenu.show(); - } - - private void trashComment( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentActionFooterBinding actionBinding, - @NonNull SiteModel site, - @NonNull CommentModel comment, - @Nullable Note note - ) { - if (!isAdded()) { - return; - } - - CommentStatus status = CommentStatus.fromString(comment.getStatus()); - // If the comment status is trash or spam, next deletion is a permanent deletion. - if (status == CommentStatus.TRASH || status == CommentStatus.SPAM) { - AlertDialog.Builder dialogBuilder = new MaterialAlertDialogBuilder(requireActivity()); - dialogBuilder.setTitle(getResources().getText(R.string.delete)); - dialogBuilder.setMessage(getResources().getText(R.string.dlg_sure_to_delete_comment)); - dialogBuilder.setPositiveButton(getResources().getText(R.string.yes), - (dialog, whichButton) -> { - moderateComment(binding, actionBinding, site, comment, note, CommentStatus.DELETED); - announceCommentStatusChangeForAccessibility(CommentStatus.DELETED); - }); - dialogBuilder.setNegativeButton( - getResources().getText(R.string.no), - null); - dialogBuilder.setCancelable(true); - dialogBuilder.create().show(); - } else { - moderateComment(binding, actionBinding, site, comment, note, CommentStatus.TRASH); - announceCommentStatusChangeForAccessibility(CommentStatus.TRASH); - } - } - - private void copyCommentLinkAddress( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentModel comment - ) { - try { - ClipboardManager clipboard = - (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText("CommentLinkAddress", comment.getUrl())); - showSnackBar(binding, comment, getString(R.string.comment_q_action_copied_url)); - } catch (Exception e) { - AppLog.e(T.UTILS, e); - showSnackBar(binding, comment, getString(R.string.error_copy_to_clipboard)); - } - } - - private void showSnackBar( - @NonNull CommentDetailFragmentBinding binding, - @NonNull CommentModel comment, - String message - ) { - Snackbar snackBar = WPSnackbar.make(binding.getRoot(), message, Snackbar.LENGTH_LONG) - .setAction(getString(R.string.share_action), - v -> { - try { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, comment.getUrl()); - startActivity(Intent.createChooser(intent, - getString(R.string.comment_share_link_via))); - } catch (ActivityNotFoundException exception) { - ToastUtils.showToast(binding.getRoot().getContext(), - R.string.comment_toast_err_share_intent); - } - }) - .setAnchorView(binding.layoutBottom); - snackBar.show(); - } - @Nullable @Override public View getScrollableViewForUniqueIdProvision() { @@ -1717,7 +1154,6 @@ public void onDestroyView() { super.onDestroyView(); mBinding = null; mReplyBinding = null; - mActionBinding = null; } @Override @@ -1727,4 +1163,12 @@ public void onDestroy() { } super.onDestroy(); } + + /** + * Listener for handling comment actions in [NotificationsDetailListFragment] + */ + public interface OnActionClickListener { + void onEditCommentClicked(); + void onUserInfoClicked(); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt index aed60956b56d..906db4bd8683 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt @@ -6,7 +6,12 @@ import android.os.Bundle import androidx.core.view.isGone import org.wordpress.android.R import org.wordpress.android.datasets.NotificationsTable +import org.wordpress.android.fluxc.tools.FormattableRangeType +import org.wordpress.android.models.Note +import org.wordpress.android.ui.comments.unified.CommentIdentifier import org.wordpress.android.ui.comments.unified.CommentSource +import org.wordpress.android.ui.engagement.BottomSheetUiState +import org.wordpress.android.ui.reader.tracker.ReaderTracker.Companion.SOURCE_NOTIF_COMMENT_USER_PROFILE import org.wordpress.android.util.AppLog import org.wordpress.android.util.ToastUtils @@ -16,6 +21,9 @@ import org.wordpress.android.util.ToastUtils * It'd be better to have multiple fragments for different sources for different purposes */ class NotificationCommentDetailFragment : CommentDetailFragment() { + private val note: Note // note will be non-null after onCreate + get() = mNote!! + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -26,6 +34,26 @@ class NotificationCommentDetailFragment : CommentDetailFragment() { } } + override fun getUserProfileUiState(): BottomSheetUiState.UserProfileUiState { + val user = mContentMapper.mapToFormattableContentList(note.body.toString()) + .find { FormattableRangeType.fromString(it.type) == FormattableRangeType.USER } + + return BottomSheetUiState.UserProfileUiState( + userAvatarUrl = note.iconURL, + blavatarUrl = "", + userName = user?.text ?: getString(R.string.anonymous), + userLogin = mComment?.authorEmail.orEmpty(), + userBio = "", + siteTitle = user?.meta?.titles?.home ?: getString(R.string.user_profile_untitled_site), + siteUrl = user?.ranges?.firstOrNull()?.url.orEmpty(), + siteId = user?.meta?.ids?.site ?: 0L, + blogPreviewSource = SOURCE_NOTIF_COMMENT_USER_PROFILE + ) + } + + override fun getCommentIdentifier(): CommentIdentifier = + CommentIdentifier.NotificationCommentIdentifier(note.id, note.commentId); + override fun handleHeaderVisibility() { mBinding?.headerView?.isGone = true } @@ -42,10 +70,10 @@ class NotificationCommentDetailFragment : CommentDetailFragment() { mSite = mSiteStore.getSiteBySiteId(note.siteId.toLong()) if (mSite == null) { // This should not exist, we should clean that screen so a note without a site/comment can be displayed - mSite = createDummyWordPressComSite(mNote!!.siteId.toLong()) + mSite = createDummyWordPressComSite(note.siteId.toLong()) } - if (mBinding != null && mReplyBinding != null && mActionBinding != null) { - showComment(mBinding!!, mReplyBinding!!, mActionBinding!!, mSite!!, mComment, mNote) + if (mBinding != null && mReplyBinding != null) { + showComment(mBinding!!, mReplyBinding!!, mSite!!, mComment, note) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt index 801c92ee0646..0f81aabcdd2f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt @@ -4,9 +4,17 @@ package org.wordpress.android.ui.comments import android.os.Bundle import androidx.core.view.isVisible +import com.gravatar.AvatarQueryOptions +import com.gravatar.AvatarUrl +import com.gravatar.types.Email +import org.wordpress.android.R import org.wordpress.android.fluxc.model.CommentModel import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.comments.unified.CommentIdentifier import org.wordpress.android.ui.comments.unified.CommentSource +import org.wordpress.android.ui.engagement.BottomSheetUiState +import org.wordpress.android.ui.reader.tracker.ReaderTracker +import org.wordpress.android.util.WPAvatarUtils /** * Used when called from comment list @@ -14,6 +22,9 @@ import org.wordpress.android.ui.comments.unified.CommentSource * It'd be better to have multiple fragments for different sources for different purposes */ class SiteCommentDetailFragment : CommentDetailFragment() { + private val comment: CommentModel + get() = mComment!! + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState != null) { @@ -23,6 +34,28 @@ class SiteCommentDetailFragment : CommentDetailFragment() { } } + override fun getUserProfileUiState(): BottomSheetUiState.UserProfileUiState { + return BottomSheetUiState.UserProfileUiState( + userAvatarUrl = CommentExtension.getAvatarUrl( + comment, + resources.getDimensionPixelSize(R.dimen.avatar_sz_large) + ), + blavatarUrl = "", + userName = comment.authorName ?: getString(R.string.anonymous), + userLogin = comment.authorEmail.orEmpty(), + // keep them empty because there's no data for displaying on UI + userBio = "", + siteTitle = "", + siteUrl = "", + siteId = 0L, + blogPreviewSource = ReaderTracker.SOURCE_SITE_COMMENTS_USER_PROFILE + ) + } + + override fun getCommentIdentifier(): CommentIdentifier = + CommentIdentifier.SiteCommentIdentifier(comment.id, comment.remoteCommentId) + + override fun handleHeaderVisibility() { mBinding?.headerView?.isVisible = true } @@ -48,3 +81,19 @@ class SiteCommentDetailFragment : CommentDetailFragment() { } } } + +object CommentExtension { + fun getAvatarUrl(comment: CommentModel, size: Int): String = when { + comment.authorProfileImageUrl != null -> WPAvatarUtils.rewriteAvatarUrl( + comment.authorProfileImageUrl!!, + size + ) + + comment.authorEmail != null -> AvatarUrl( + Email(comment.authorEmail!!), + AvatarQueryOptions(size, null, null, null) + ).url().toString() + + else -> "" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentActionPopupHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentActionPopupHandler.kt new file mode 100644 index 000000000000..984007e7d176 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentActionPopupHandler.kt @@ -0,0 +1,41 @@ +@file:Suppress("DEPRECATION") + +package org.wordpress.android.ui.comments.unified + +import android.view.LayoutInflater +import android.view.View +import android.widget.PopupWindow +import org.wordpress.android.R +import org.wordpress.android.databinding.CommentActionsBinding +import org.wordpress.android.ui.comments.CommentDetailFragment +import org.wordpress.android.util.ToastUtils + +object CommentActionPopupHandler { + @JvmStatic + fun show(anchorView: View, listener: CommentDetailFragment.OnActionClickListener?) { + val popupWindow = PopupWindow(anchorView.context, null, R.style.WordPress) + popupWindow.isOutsideTouchable = true + popupWindow.elevation = anchorView.context.resources.getDimension(R.dimen.popup_over_toolbar_elevation) + popupWindow.contentView = CommentActionsBinding + .inflate(LayoutInflater.from(anchorView.context)) + .apply { + textUserInfo.setOnClickListener { + listener?.onUserInfoClicked() + popupWindow.dismiss() + } + textShare.setOnClickListener { + ToastUtils.showToast(it.context, "not yet implemented") + popupWindow.dismiss() + } + textEditComment.setOnClickListener { + listener?.onEditCommentClicked() + popupWindow.dismiss() + } + textChangeStatus.setOnClickListener { + ToastUtils.showToast(it.context, "not yet implemented") + popupWindow.dismiss() + } + }.root + popupWindow.showAsDropDown(anchorView) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/BottomSheetUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/BottomSheetUiState.kt index 45b0fc146195..f9b7f68a3931 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/BottomSheetUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/BottomSheetUiState.kt @@ -1,6 +1,9 @@ package org.wordpress.android.ui.engagement +import java.io.Serializable + sealed class BottomSheetUiState { + @Suppress("SerialVersionUIDInSerializableClass") data class UserProfileUiState( val userAvatarUrl: String, val blavatarUrl: String, @@ -10,9 +13,8 @@ sealed class BottomSheetUiState { val siteTitle: String, val siteUrl: String, val siteId: Long, - val onSiteClickListener: ((siteId: Long, siteUrl: String, source: String) -> Unit)? = null, val blogPreviewSource: String - ) : BottomSheetUiState() { + ) : BottomSheetUiState(), Serializable { val hasSiteUrl: Boolean = siteUrl.isNotBlank() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt index 937d57fa33c6..86a01ede483c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt @@ -111,14 +111,14 @@ class EngagedPeopleListFragment : Fragment() { recycler.layoutManager = layoutManager userProfileViewModel.onBottomSheetAction.observeEvent(viewLifecycleOwner) { state -> - var bottomSheet = childFragmentManager.findFragmentByTag(USER_PROFILE_BOTTOM_SHEET_TAG) + var bottomSheet = childFragmentManager.findFragmentByTag(UserProfileBottomSheetFragment.TAG) as? UserProfileBottomSheetFragment when (state) { ShowBottomSheet -> { if (bottomSheet == null) { bottomSheet = UserProfileBottomSheetFragment.newInstance(USER_PROFILE_VM_KEY) - bottomSheet.show(childFragmentManager, USER_PROFILE_BOTTOM_SHEET_TAG) + bottomSheet.show(childFragmentManager, UserProfileBottomSheetFragment.TAG) } } @@ -192,7 +192,7 @@ class EngagedPeopleListFragment : Fragment() { } is OpenUserProfileBottomSheet -> { - userProfileViewModel.onBottomSheetOpen(event.userProfile, event.onClick, event.source) + userProfileViewModel.onBottomSheetOpen(event.userProfile, event.source) } } @@ -304,8 +304,6 @@ class EngagedPeopleListFragment : Fragment() { private const val KEY_LIST_SCENARIO = "list_scenario" private const val KEY_LIST_STATE = "list_state" - private const val USER_PROFILE_BOTTOM_SHEET_TAG = "USER_PROFILE_BOTTOM_SHEET_TAG" - @JvmStatic fun newInstance(listScenario: ListScenario): EngagedPeopleListFragment { val args = Bundle() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt index 17af0b826348..16550d33c791 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt @@ -42,7 +42,7 @@ class ListScenarioUtils @Inject constructor( imageManager, notificationsUtilsWrapper ) - headerNoteBlock.setIsComment(note.isCommentType) + headerNoteBlock.setReplyToComment(note.isCommentReplyType) val spannable: Spannable = notificationsUtilsWrapper.getSpannableContentForRanges(headerNoteBlock.getHeader(0)) val spans = spannable.getSpans( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt index b0c6e5a13d83..5745e9d1cf03 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileBottomSheetFragment.kt @@ -1,15 +1,17 @@ +@file:Suppress("DEPRECATION") + package org.wordpress.android.ui.engagement import android.content.Context import android.content.DialogInterface +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.ViewCompat +import androidx.core.view.isGone import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -17,15 +19,19 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.wordpress.android.R import org.wordpress.android.WordPress +import org.wordpress.android.databinding.UserProfileBottomSheetBinding +import org.wordpress.android.ui.WPWebViewActivity import org.wordpress.android.ui.engagement.BottomSheetUiState.UserProfileUiState +import org.wordpress.android.ui.reader.ReaderActivityLauncher +import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.PhotonUtils -import org.wordpress.android.util.PhotonUtils.Quality.HIGH import org.wordpress.android.util.UrlUtils import org.wordpress.android.util.WPAvatarUtils +import org.wordpress.android.util.WPUrlUtils +import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.util.image.ImageType.AVATAR_WITH_BACKGROUND -import org.wordpress.android.util.image.ImageType.BLAVATAR +import org.wordpress.android.util.image.ImageType import org.wordpress.android.viewmodel.ResourceProvider import javax.inject.Inject import com.google.android.material.R as MaterialR @@ -43,41 +49,71 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() { @Inject lateinit var resourceProvider: ResourceProvider - private lateinit var viewModel: UserProfileViewModel - - companion object { - const val USER_PROFILE_VIEW_MODEL_KEY = "user_profile_view_model_key" - - fun newInstance(viewModelKey: String): UserProfileBottomSheetFragment { - val fragment = UserProfileBottomSheetFragment() - val bundle = Bundle() + @Inject + lateinit var analyticsUtilsWrapper: AnalyticsUtilsWrapper - bundle.putString(USER_PROFILE_VIEW_MODEL_KEY, viewModelKey) + @Inject + lateinit var readerTracker: ReaderTracker + + private var viewModel: UserProfileViewModel? = null + private var binding: UserProfileBottomSheetBinding? = null + private val state by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getSerializable(USER_PROFILE_STATE, UserProfileUiState::class.java) + } else { + requireArguments().getSerializable(USER_PROFILE_STATE) as? UserProfileUiState + } + } - fragment.arguments = bundle + companion object { + private const val USER_PROFILE_VIEW_MODEL_KEY = "user_profile_view_model_key" + private const val USER_PROFILE_STATE = "user_profile_state" + + const val TAG = "USER_PROFILE_BOTTOM_SHEET_TAG" + + /** + * For displaying the user profile when users are from Likes + */ + fun newInstance(viewModelKey: String) = UserProfileBottomSheetFragment() + .apply { + arguments = Bundle().apply { + putString(USER_PROFILE_VIEW_MODEL_KEY, viewModelKey) + } + } - return fragment - } + /** + * For displaying the user profile when users are from Comments or Notifications + */ + @JvmStatic + fun newInstance(state: UserProfileUiState) = UserProfileBottomSheetFragment() + .apply { + arguments = Bundle().apply { + putSerializable(USER_PROFILE_STATE, state) + } + } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.user_profile_bottom_sheet, container) - } + ): View = UserProfileBottomSheetBinding.inflate(inflater, container, false) + .apply { binding = this } + .root override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val vmKey = requireArguments().getString(USER_PROFILE_VIEW_MODEL_KEY)!! + val vmKey = requireArguments().getString(USER_PROFILE_VIEW_MODEL_KEY) ViewCompat.setAccessibilityPaneTitle(view, getString(R.string.user_profile_bottom_sheet_description)) - viewModel = ViewModelProvider(parentFragment as ViewModelStoreOwner, viewModelFactory) - .get(vmKey, UserProfileViewModel::class.java) + vmKey?.let { + viewModel = ViewModelProvider(parentFragment as ViewModelStoreOwner, viewModelFactory) + .get(vmKey, UserProfileViewModel::class.java) + initObservers() + } - initObservers(view) + state?.let { binding?.setup(it) } dialog?.setOnShowListener { dialogInterface -> val sheetDialog = dialogInterface as? BottomSheetDialog @@ -94,68 +130,81 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() { } } - private fun initObservers(view: View) { - val userAvatar = view.findViewById(R.id.user_avatar) - val blavatar = view.findViewById(R.id.user_site_blavatar) - val userName = view.findViewById(R.id.user_name) - val userLogin = view.findViewById(R.id.user_login) - val userBio = view.findViewById(R.id.user_bio) - val siteTitle = view.findViewById(R.id.site_title) - val siteUrl = view.findViewById(R.id.site_url) - val siteSectionHeader = view.findViewById(R.id.site_section_header) - val siteData = view.findViewById(R.id.site_data) - - viewModel.bottomSheetUiState.observe(viewLifecycleOwner, { state -> + private fun initObservers() { + viewModel?.bottomSheetUiState?.observe(viewLifecycleOwner) { state -> when (state) { is UserProfileUiState -> { - val avatarSz = resourceProvider.getDimensionPixelSize(R.dimen.user_profile_bottom_sheet_avatar_sz) - val blavatarSz = resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium) - - imageManager.loadIntoCircle( - userAvatar, - AVATAR_WITH_BACKGROUND, - WPAvatarUtils.rewriteAvatarUrl(state.userAvatarUrl, avatarSz) - ) - userName.text = state.userName - userLogin.text = if (state.userLogin.isNotBlank()) { - getString(R.string.at_username, state.userLogin) - } else { - "" - } - if (state.userBio.isNotBlank()) { - userBio.text = state.userBio - userBio.visibility = View.VISIBLE - } else { - userBio.visibility = View.GONE - } - - imageManager.load( - blavatar, - BLAVATAR, - PhotonUtils.getPhotonImageUrl(state.blavatarUrl, blavatarSz, blavatarSz, HIGH) - ) - - if (state.hasSiteUrl) { - siteTitle.text = state.siteTitle - siteUrl.text = UrlUtils.getHost(state.siteUrl) - siteData.setOnClickListener { - state.onSiteClickListener?.invoke( - state.siteId, - state.siteUrl, - state.blogPreviewSource - ) - } - siteSectionHeader.visibility = View.VISIBLE - blavatar.visibility = View.VISIBLE - siteData.visibility = View.VISIBLE - } else { - siteSectionHeader.visibility = View.GONE - blavatar.visibility = View.GONE - siteData.visibility = View.GONE - } + binding?.setup(state) } } - }) + } + } + + private fun UserProfileBottomSheetBinding.setup(state: UserProfileUiState) { + val avatarSz = + resourceProvider.getDimensionPixelSize(R.dimen.user_profile_bottom_sheet_avatar_sz) + val blavatarSz = resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium) + + imageManager.loadIntoCircle( + userAvatar, + ImageType.AVATAR_WITH_BACKGROUND, + WPAvatarUtils.rewriteAvatarUrl(state.userAvatarUrl, avatarSz) + ) + userName.text = state.userName + userLogin.text = state.userLogin.ifBlank { + userLogin.isGone = true + "" + } + if (state.userBio.isNotBlank()) { + userBio.text = state.userBio + userBio.visibility = View.VISIBLE + } else { + userBio.visibility = View.GONE + } + + imageManager.load( + userSiteBlavatar, + ImageType.BLAVATAR, + PhotonUtils.getPhotonImageUrl(state.blavatarUrl, blavatarSz, blavatarSz, PhotonUtils.Quality.HIGH) + ) + + if (state.hasSiteUrl) { + siteTitle.text = state.siteTitle + siteUrl.text = UrlUtils.getHost(state.siteUrl) + siteData.setOnClickListener { + if (state.siteId <= 0L && state.siteUrl.isNotEmpty()) { + openSiteUrl(state.siteUrl, state.blogPreviewSource) + } else { + openSiteId(state.siteId, state.blogPreviewSource) + } + } + siteSectionHeader.visibility = View.VISIBLE + userSiteBlavatar.visibility = View.VISIBLE + siteData.visibility = View.VISIBLE + } else { + siteSectionHeader.visibility = View.GONE + userSiteBlavatar.visibility = View.GONE + siteData.visibility = View.GONE + } + } + + private fun openSiteId(siteId: Long, source: String) { + ReaderActivityLauncher.showReaderBlogPreview( + context, + siteId, + false, + source, + readerTracker + ) + } + + private fun openSiteUrl(url: String, source: String) { + analyticsUtilsWrapper.trackBlogPreviewedByUrl(source) + if (WPUrlUtils.isWordPressCom(url)) { + WPWebViewActivity.openUrlByUsingGlobalWPCOMCredentials(context, url) + } else { + WPWebViewActivity.openURL(context, url) + } } override fun onAttach(context: Context) { @@ -165,6 +214,11 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() { override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) - viewModel.onBottomSheetCancelled() + viewModel?.onBottomSheetCancelled() + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileViewModel.kt index fab1bced1f12..a2b6e94b8c19 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/UserProfileViewModel.kt @@ -28,7 +28,6 @@ class UserProfileViewModel @Inject constructor( fun onBottomSheetOpen( userProfile: UserProfile, - onClick: ((siteId: Long, siteUrl: String, source: String) -> Unit)?, source: EngagementNavigationSource? ) { _bottomSheetUiState.value = with(userProfile) { @@ -36,7 +35,7 @@ class UserProfileViewModel @Inject constructor( userAvatarUrl = userAvatarUrl, blavatarUrl = blavatarUrl, userName = userName, - userLogin = userLogin, + userLogin = if (userLogin.isNotEmpty()) "@$userLogin" else userLogin, userBio = userBio, siteTitle = if (siteTitle.isBlank()) { resourceProvider.getString(R.string.user_profile_untitled_site) @@ -45,7 +44,6 @@ class UserProfileViewModel @Inject constructor( }, siteUrl = siteUrl, siteId = siteId, - onSiteClickListener = onClick, blogPreviewSource = source?.let { when (it) { LIKE_NOTIFICATION_LIST -> ReaderTracker.SOURCE_NOTIF_LIKE_LIST_USER_PROFILE diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt index 008135b4428b..557d77c8e514 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt @@ -43,6 +43,8 @@ import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.ScrollableViewInitializedListener import org.wordpress.android.ui.ViewPagerFragment.Companion.restoreOriginalViewId import org.wordpress.android.ui.ViewPagerFragment.Companion.setUniqueIdToView +import org.wordpress.android.ui.comments.CommentDetailFragment +import org.wordpress.android.ui.comments.unified.CommentActionPopupHandler import org.wordpress.android.ui.engagement.ListScenarioUtils import org.wordpress.android.ui.notifications.adapters.NoteBlockAdapter import org.wordpress.android.ui.notifications.blocks.BlockType @@ -73,6 +75,7 @@ import javax.inject.Inject import javax.inject.Named class NotificationsDetailListFragment : ListFragment(), NotificationFragment { + private var onActionClickListener: CommentDetailFragment.OnActionClickListener? = null private var restoredListPosition = 0 private var notification: Note? = null private var rootLayout: LinearLayout? = null @@ -271,6 +274,10 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { } } + override fun showActionPopup(view: View) { + CommentActionPopupHandler.show(view, onActionClickListener) + } + fun handleNoteBlockSpanClick( activity: NotificationsDetailActivity, clickedSpan: NoteBlockClickableSpan @@ -349,7 +356,7 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { imageManager, notificationsUtilsWrapper ) - headerNoteBlock.setIsComment(note.isCommentType) + headerNoteBlock.setReplyToComment(note.isCommentReplyType) noteList.add(headerNoteBlock) } @@ -457,7 +464,11 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { if (isPingback) { noteBlock.setIsPingback() } - noteList.add(noteBlock) + if (isRepliedFooter(noteObject).not()) { + // we don't handle replied footer at the moment + // it'd be better if we can display the replied comment + noteList.add(noteBlock) + } } catch (e: JSONException) { AppLog.e(NOTIFS, "Invalid note data, could not parse.") } @@ -559,16 +570,32 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { return false } + return requireNotNull(notification).let { note -> + if (isRepliedFooter(blockObject)) { + true + } else if (note.isFollowType || note.isLikeType) { + // User list notifications have a footer if they have 10 or more users in the body + // The last block will not have a type, so we can use that to determine if it is the footer + blockObject.type == null + } else { + false + } + } + } + + /** + * Check if the block is a footer for a comment notification that has been replied to + */ + private fun isRepliedFooter(blockObject: FormattableContent?): Boolean { + if (notification == null || blockObject == null) { + return false + } return requireNotNull(notification).let { note -> if (note.isCommentType) { val commentReplyId = blockObject.getRangeIdOrZero(1) // Check if this is a comment notification that has been replied to // The block will not have a type, and its id will match the comment reply id in the Note. (blockObject.type == null && note.commentReplyId == commentReplyId) - } else if (note.isFollowType || note.isLikeType) { - // User list notifications have a footer if they have 10 or more users in the body - // The last block will not have a type, so we can use that to determine if it is the footer - blockObject.type == null } else { false } @@ -635,6 +662,10 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { } } + fun setOnEditCommentListener(listener: CommentDetailFragment.OnActionClickListener){ + onActionClickListener = listener + } + companion object { private const val KEY_NOTE_ID = "noteId" private const val KEY_LIST_POSITION = "listPosition" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.kt index 28c4e5db7dee..94f423ad21d5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.kt @@ -8,8 +8,6 @@ import android.text.TextUtils import android.view.View import android.widget.ImageView import android.widget.TextView -import androidx.core.text.HtmlCompat -import androidx.core.view.ViewCompat import org.wordpress.android.R import org.wordpress.android.fluxc.model.CommentStatus import org.wordpress.android.fluxc.tools.FormattableContent @@ -23,21 +21,21 @@ import org.wordpress.android.util.image.ImageType // A user block with slightly different formatting for display in a comment detail @Suppress("LongParameterList") class CommentUserNoteBlock( - private val mContext: Context?, noteObject: FormattableContent, - private val mCommentData: FormattableContent?, + private val context: Context?, noteObject: FormattableContent, + private val commentData: FormattableContent?, private val timestamp: Long, onNoteBlockTextClickListener: OnNoteBlockTextClickListener?, onGravatarClickedListener: OnGravatarClickedListener?, imageManager: ImageManager, notificationsUtilsWrapper: NotificationsUtilsWrapper ) : UserNoteBlock( - mContext, noteObject, onNoteBlockTextClickListener, onGravatarClickedListener, imageManager, + context, noteObject, onNoteBlockTextClickListener, onGravatarClickedListener, imageManager, notificationsUtilsWrapper ) { - private var mCommentStatus = CommentStatus.APPROVED - private var mNormalBackgroundColor = 0 - private var mIndentedLeftPadding = 0 - private var mStatusChanged = false - private var mNoteBlockHolder: CommentUserNoteBlockHolder? = null + private var commentStatus = CommentStatus.APPROVED + private var normalBackgroundColor = 0 + private var indentedLeftPadding = 0 + private var statusChanged = false + private var holder: CommentUserNoteBlockHolder? = null override val blockType: BlockType get() = BlockType.USER_COMMENT @@ -45,7 +43,7 @@ class CommentUserNoteBlock( get() = R.layout.note_block_comment_user init { - avatarSize = mContext?.resources?.getDimensionPixelSize(R.dimen.avatar_sz_small) ?: 0 + avatarSize = context?.resources?.getDimensionPixelSize(R.dimen.avatar_sz_small) ?: 0 } interface OnCommentStatusChangeListener { @@ -54,124 +52,65 @@ class CommentUserNoteBlock( @SuppressLint("ClickableViewAccessibility") // fixed by setting a click listener to avatarImageView override fun configureView(view: View): View { - mNoteBlockHolder = view.tag as CommentUserNoteBlockHolder + holder = view.tag as CommentUserNoteBlockHolder setUserName() setUserCommentAgo() - setUserCommentSite() setUserAvatar() setUserComment() - setCommentStatus(view) return view } private fun setUserName() { - mNoteBlockHolder?.mNameTextView?.text = HtmlCompat.fromHtml( - "$noteText", - HtmlCompat.FROM_HTML_MODE_LEGACY - ) + holder?.textName?.text = noteText.toString() } private fun setUserCommentAgo() { - mNoteBlockHolder?.mAgoTextView?.text = DateTimeUtils.timeSpanFromTimestamp( + holder?.textDate?.text = DateTimeUtils.timeSpanFromTimestamp( timestamp, - mNoteBlockHolder?.mAgoTextView?.context + holder?.textDate?.context ) } - private fun setUserCommentSite() { - if (!TextUtils.isEmpty(metaHomeTitle) || !TextUtils.isEmpty(metaSiteUrl)) { - mNoteBlockHolder?.mBulletTextView?.visibility = View.VISIBLE - mNoteBlockHolder?.mSiteTextView?.visibility = View.VISIBLE - if (!TextUtils.isEmpty(metaHomeTitle)) { - mNoteBlockHolder?.mSiteTextView?.text = metaHomeTitle - } else { - mNoteBlockHolder?.mSiteTextView?.text = - metaSiteUrl?.replace("http://", "")?.replace("https://", "") - } - } else { - mNoteBlockHolder?.mBulletTextView?.visibility = View.GONE - mNoteBlockHolder?.mSiteTextView?.visibility = View.GONE - } - mNoteBlockHolder?.mSiteTextView?.importantForAccessibility = - View.IMPORTANT_FOR_ACCESSIBILITY_NO - } - @SuppressLint("ClickableViewAccessibility") private fun setUserAvatar() { var imageUrl = "" if (hasImageMediaItem()) { noteMediaItem?.url?.let { imageUrl = WPAvatarUtils.rewriteAvatarUrl(it, avatarSize) } - mNoteBlockHolder?.mAvatarImageView?.contentDescription = - mContext?.getString(R.string.profile_picture, noteText.toString()) + holder?.imageAvatar?.contentDescription = + context?.getString(R.string.profile_picture, noteText.toString()) if (!TextUtils.isEmpty(userUrl)) { - mNoteBlockHolder?.mAvatarImageView?.setOnClickListener { showBlogPreview() } - mNoteBlockHolder?.mAvatarImageView?.setOnTouchListener(mOnGravatarTouchListener) + holder?.imageAvatar?.setOnClickListener { showBlogPreview() } + holder?.imageAvatar?.setOnTouchListener(mOnGravatarTouchListener) } else { - mNoteBlockHolder?.mAvatarImageView?.setOnClickListener(null) - mNoteBlockHolder?.mAvatarImageView?.setOnTouchListener(null) - mNoteBlockHolder?.mAvatarImageView?.contentDescription = null + holder?.imageAvatar?.setOnClickListener(null) + holder?.imageAvatar?.setOnTouchListener(null) + holder?.imageAvatar?.contentDescription = null } } else { - mNoteBlockHolder?.mAvatarImageView?.setOnClickListener(null) - mNoteBlockHolder?.mAvatarImageView?.setOnTouchListener(null) - mNoteBlockHolder?.mAvatarImageView?.contentDescription = null + holder?.imageAvatar?.setOnClickListener(null) + holder?.imageAvatar?.setOnTouchListener(null) + holder?.imageAvatar?.contentDescription = null } - mNoteBlockHolder?.mAvatarImageView?.let { + holder?.imageAvatar?.let { mImageManager.loadIntoCircle(it, ImageType.AVATAR_WITH_BACKGROUND, imageUrl) } } private fun setUserComment() { - val spannable = getCommentTextOfNotification(mNoteBlockHolder) + val spannable = getCommentTextOfNotification(holder) val spans = spannable.getSpans(0, spannable.length, NoteBlockClickableSpan::class.java) - mContext?.let { + context?.let { for (span in spans) { span.enableColors(it) } } - mNoteBlockHolder?.mCommentTextView?.text = spannable - } - - @Suppress("MagicNumber") - private fun setCommentStatus(view: View) { - // Change display based on comment status and type: - // 1. Comment replies are indented and have a 'pipe' background - // 2. Unapproved comments have different background and text color - var paddingStart = ViewCompat.getPaddingStart(view) - val paddingTop = view.paddingTop - val paddingEnd = ViewCompat.getPaddingEnd(view) - val paddingBottom = view.paddingBottom - if (mCommentStatus == CommentStatus.UNAPPROVED) { - if (hasCommentNestingLevel()) { - paddingStart = mIndentedLeftPadding - view.setBackgroundResource(R.drawable.bg_rectangle_warning_surface_with_padding) - } else { - view.setBackgroundResource(R.drawable.bg_rectangle_warning_surface) - } - mNoteBlockHolder?.mDividerView?.visibility = View.INVISIBLE - } else { - if (hasCommentNestingLevel()) { - paddingStart = mIndentedLeftPadding - view.setBackgroundResource(R.drawable.comment_reply_background) - mNoteBlockHolder?.mDividerView?.visibility = View.INVISIBLE - } else { - view.setBackgroundColor(mNormalBackgroundColor) - mNoteBlockHolder?.mDividerView?.visibility = View.VISIBLE - } - } - ViewCompat.setPaddingRelative(view, paddingStart, paddingTop, paddingEnd, paddingBottom) - // If status was changed, fade in the view - if (mStatusChanged) { - mStatusChanged = false - view.alpha = 0.4f - view.animate().alpha(1.0f).start() - } + holder?.textComment?.text = spannable } private fun getCommentTextOfNotification(noteBlockHolder: CommentUserNoteBlockHolder?): Spannable { val builder = mNotificationsUtilsWrapper.getSpannableContentForRanges( - mCommentData, - noteBlockHolder?.mCommentTextView, + commentData, + noteBlockHolder?.textComment, onNoteBlockTextClickListener, false ) @@ -192,48 +131,42 @@ class CommentUserNoteBlock( return builder } - private fun hasCommentNestingLevel(): Boolean = mCommentData?.nestLevel?.let { return it > 0 } ?: false - override fun getViewHolder(view: View): Any = CommentUserNoteBlockHolder(view) private inner class CommentUserNoteBlockHolder constructor(view: View) { - val mAvatarImageView: ImageView = view.findViewById(R.id.user_avatar) - val mNameTextView: TextView = view.findViewById(R.id.user_name) - val mBulletTextView: TextView = view.findViewById(R.id.user_comment_bullet) - val mDividerView: View = view.findViewById(R.id.divider_view) - val mAgoTextView: TextView = view.findViewById(R.id.user_comment_ago).apply { + val imageAvatar: ImageView = view.findViewById(R.id.user_avatar) + val textName: TextView = view.findViewById(R.id.user_name) + val textDate: TextView = view.findViewById(R.id.user_comment_ago).apply { visibility = View.VISIBLE } - val mCommentTextView: TextView = view.findViewById(R.id.user_comment).apply { + val textComment: TextView = view.findViewById(R.id.user_comment).apply { movementMethod = NoteBlockLinkMovementMethod() setOnClickListener { // show all comments on this post when user clicks the comment text onNoteBlockTextClickListener?.showReaderPostComments() } } - val mSiteTextView: TextView = view.findViewById(R.id.user_comment_site).apply { - setOnClickListener { - onNoteBlockTextClickListener?.showSitePreview(metaSiteId, metaSiteUrl) - } + val buttonMore = view.findViewById(R.id.image_more).apply { + setOnClickListener { onNoteBlockTextClickListener?.showActionPopup(this) } } } fun configureResources(context: Context?) { - mNormalBackgroundColor = context?.getColorFromAttribute(com.google.android.material.R.attr.colorSurface) ?: 0 + normalBackgroundColor = context?.getColorFromAttribute(com.google.android.material.R.attr.colorSurface) ?: 0 // Double margin_extra_large for increased indent in comment replies - mIndentedLeftPadding = (context?.resources?.getDimensionPixelSize(R.dimen.margin_extra_large) ?: 0) * 2 + indentedLeftPadding = (context?.resources?.getDimensionPixelSize(R.dimen.margin_extra_large) ?: 0) * 2 } val onCommentChangeListener: OnCommentStatusChangeListener = object : OnCommentStatusChangeListener { override fun onCommentStatusChanged(newStatus: CommentStatus) { - mCommentStatus = newStatus - mStatusChanged = true + commentStatus = newStatus + statusChanged = true } } fun setCommentStatus(status: CommentStatus) { - mCommentStatus = status + commentStatus = status } companion object { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.kt index 824da022f8b4..1ba70f90da17 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/HeaderNoteBlock.kt @@ -10,6 +10,7 @@ import android.view.View.OnTouchListener import android.view.animation.DecelerateInterpolator import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import org.wordpress.android.R import org.wordpress.android.fluxc.tools.FormattableContent import org.wordpress.android.ui.notifications.blocks.UserNoteBlock.OnGravatarClickedListener @@ -39,7 +40,7 @@ class HeaderNoteBlock( notificationsUtilsWrapper, onNoteBlockTextClickListener ) { - private var mIsComment = false + private var replyToComment = false private var mAvatarSize = 0 override val blockType: BlockType get() = BlockType.USER_HEADER @@ -94,6 +95,7 @@ class HeaderNoteBlock( noteBlockHolder.mAvatarImageView.setOnClickListener(null) noteBlockHolder.mAvatarImageView.setOnTouchListener(null) } + noteBlockHolder.mAvatarImageView.isVisible = replyToComment noteBlockHolder.mSnippetTextView.text = snippet return view } @@ -102,8 +104,11 @@ class HeaderNoteBlock( fun getHeader(headerIndex: Int): FormattableContent? = mHeadersList?.getOrNull(headerIndex) - fun setIsComment(isComment: Boolean) { - mIsComment = isComment + /** + * Set whether this is a reply to a comment + */ + fun setReplyToComment(isReplyToComment: Boolean) { + replyToComment = isReplyToComment } private inner class NoteHeaderBlockHolder internal constructor(view: View) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.kt index a80ee032d694..2724891f89ed 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/NoteBlock.kt @@ -50,6 +50,7 @@ open class NoteBlock( fun showDetailForNoteIds() fun showReaderPostComments() fun showSitePreview(siteId: Long, siteUrl: String?) + fun showActionPopup(view: View) } open val layoutResourceId: Int diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt index 1788e4148ef2..dd47dc87fca5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt @@ -477,6 +477,8 @@ class ReaderTracker @Inject constructor( const val SOURCE_READER_LIKE_LIST = "reader_like_list" const val SOURCE_READER_LIKE_LIST_USER_PROFILE = "reader_like_list_user_profile" const val SOURCE_NOTIF_LIKE_LIST_USER_PROFILE = "notif_like_list_user_profile" + const val SOURCE_SITE_COMMENTS_USER_PROFILE = "site_comments_user_profile" + const val SOURCE_NOTIF_COMMENT_USER_PROFILE = "notif_comment_user_profile" const val SOURCE_USER_PROFILE_UNKNOWN = "user_profile_source_unknown" const val SOURCE_ACTIVITY_LOG_DETAIL = "activity_log_detail" const val SOURCE_BLOGGING_PROMPTS_VIEW_ANSWERS = "blogging_prompts_my_site_card_view_answers" @@ -496,11 +498,14 @@ class ReaderTracker @Inject constructor( AnalyticsTracker.track(stat, properties) } - fun isUserProfileSource(source: String): Boolean { - return (source == SOURCE_READER_LIKE_LIST_USER_PROFILE || - source == SOURCE_NOTIF_LIKE_LIST_USER_PROFILE || - source == SOURCE_USER_PROFILE_UNKNOWN) - } + fun isUserProfileSource(source: String): Boolean = + listOf( + SOURCE_READER_LIKE_LIST_USER_PROFILE, + SOURCE_NOTIF_LIKE_LIST_USER_PROFILE, + SOURCE_SITE_COMMENTS_USER_PROFILE, + SOURCE_NOTIF_COMMENT_USER_PROFILE, + SOURCE_USER_PROFILE_UNKNOWN + ).contains(source) } } diff --git a/WordPress/src/main/res/anim/notifications_button_scale.xml b/WordPress/src/main/res/anim/notifications_button_scale.xml deleted file mode 100644 index f95e66bd4a05..000000000000 --- a/WordPress/src/main/res/anim/notifications_button_scale.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/WordPress/src/main/res/drawable-hdpi/ic_time_neutral_400_16dp.png b/WordPress/src/main/res/drawable-hdpi/ic_time_neutral_400_16dp.png deleted file mode 100644 index f09acf8570ec..000000000000 Binary files a/WordPress/src/main/res/drawable-hdpi/ic_time_neutral_400_16dp.png and /dev/null differ diff --git a/WordPress/src/main/res/drawable-ldrtl/bg_rectangle_warning_surface.xml b/WordPress/src/main/res/drawable-ldrtl/bg_rectangle_warning_surface.xml deleted file mode 100644 index 1f246053cd9f..000000000000 --- a/WordPress/src/main/res/drawable-ldrtl/bg_rectangle_warning_surface.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable-ldrtl/bg_rectangle_warning_surface_with_padding.xml b/WordPress/src/main/res/drawable-ldrtl/bg_rectangle_warning_surface_with_padding.xml deleted file mode 100644 index e16836bd0485..000000000000 --- a/WordPress/src/main/res/drawable-ldrtl/bg_rectangle_warning_surface_with_padding.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable-ldrtl/comment_reply_background.xml b/WordPress/src/main/res/drawable-ldrtl/comment_reply_background.xml deleted file mode 100644 index 6959a71d6bd1..000000000000 --- a/WordPress/src/main/res/drawable-ldrtl/comment_reply_background.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable-xhdpi/ic_time_neutral_400_16dp.png b/WordPress/src/main/res/drawable-xhdpi/ic_time_neutral_400_16dp.png deleted file mode 100644 index 83fb565331a8..000000000000 Binary files a/WordPress/src/main/res/drawable-xhdpi/ic_time_neutral_400_16dp.png and /dev/null differ diff --git a/WordPress/src/main/res/drawable-xxhdpi/ic_time_neutral_400_16dp.png b/WordPress/src/main/res/drawable-xxhdpi/ic_time_neutral_400_16dp.png deleted file mode 100644 index ca90db846b9a..000000000000 Binary files a/WordPress/src/main/res/drawable-xxhdpi/ic_time_neutral_400_16dp.png and /dev/null differ diff --git a/WordPress/src/main/res/drawable/arrow_down.xml b/WordPress/src/main/res/drawable/arrow_down.xml new file mode 100644 index 000000000000..a662968d2236 --- /dev/null +++ b/WordPress/src/main/res/drawable/arrow_down.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/WordPress/src/main/res/drawable/bg_rectangle_warning_surface.xml b/WordPress/src/main/res/drawable/bg_rectangle_warning_surface.xml deleted file mode 100644 index 14f35576d8f6..000000000000 --- a/WordPress/src/main/res/drawable/bg_rectangle_warning_surface.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable/bg_rectangle_warning_surface_with_padding.xml b/WordPress/src/main/res/drawable/bg_rectangle_warning_surface_with_padding.xml deleted file mode 100644 index a7d384a6dda9..000000000000 --- a/WordPress/src/main/res/drawable/bg_rectangle_warning_surface_with_padding.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable/comment_author_avatar.xml b/WordPress/src/main/res/drawable/comment_author_avatar.xml new file mode 100644 index 000000000000..e0cad028e9fe --- /dev/null +++ b/WordPress/src/main/res/drawable/comment_author_avatar.xml @@ -0,0 +1,10 @@ + + + diff --git a/WordPress/src/main/res/drawable/comment_reply_background.xml b/WordPress/src/main/res/drawable/comment_reply_background.xml deleted file mode 100644 index 8e62422e2933..000000000000 --- a/WordPress/src/main/res/drawable/comment_reply_background.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/drawable/edit.xml b/WordPress/src/main/res/drawable/edit.xml new file mode 100644 index 000000000000..fd70b32a1b5e --- /dev/null +++ b/WordPress/src/main/res/drawable/edit.xml @@ -0,0 +1,14 @@ + + + + diff --git a/WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml b/WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml deleted file mode 100644 index 57341d697972..000000000000 --- a/WordPress/src/main/res/drawable/notifications_list_divider_full_width.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/WordPress/src/main/res/layout/comment_action_footer.xml b/WordPress/src/main/res/layout/comment_action_footer.xml deleted file mode 100644 index fb683870028a..000000000000 --- a/WordPress/src/main/res/layout/comment_action_footer.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/layout/comment_actions.xml b/WordPress/src/main/res/layout/comment_actions.xml new file mode 100644 index 000000000000..bf173c4ddd05 --- /dev/null +++ b/WordPress/src/main/res/layout/comment_actions.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/comment_detail_fragment.xml b/WordPress/src/main/res/layout/comment_detail_fragment.xml index 496634f2f744..b769660585cc 100644 --- a/WordPress/src/main/res/layout/comment_detail_fragment.xml +++ b/WordPress/src/main/res/layout/comment_detail_fragment.xml @@ -4,23 +4,48 @@ comment detail displayed from both the notification list and the comment list --> + + + + + + + + + + app:cardElevation="0dp" + app:layout_constraintTop_toTopOf="parent"> + + - - @@ -76,9 +102,9 @@ android:id="@+id/nested_scroll_view" android:layout_width="match_parent" android:layout_height="0dp" - app:layout_constraintTop_toBottomOf="@+id/header_view" + android:fillViewport="true" app:layout_constraintBottom_toTopOf="@+id/layout_bottom" - android:fillViewport="true"> + app:layout_constraintTop_toBottomOf="@+id/header_view"> - + android:layout_marginStart="@dimen/margin_extra_large" + android:layout_marginTop="@dimen/margin_extra_small_large"> + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintBottom_toTopOf="@+id/text_site" + app:layout_constraintEnd_toStartOf="@+id/image_more" + app:layout_constraintStart_toEndOf="@+id/image_avatar" + app:layout_constraintTop_toTopOf="@+id/image_avatar" + tools:text="Bob Ross" /> + - - - - - + android:textAppearance="?attr/textAppearanceBody2" + android:textColor="@color/material_on_surface_emphasis_medium" + app:layout_constraintBottom_toBottomOf="@+id/image_avatar" + app:layout_constraintEnd_toStartOf="@+id/image_more" + app:layout_constraintStart_toEndOf="@+id/image_avatar" + app:layout_constraintTop_toBottomOf="@+id/text_name" + tools:text="bobross" /> + + #F2F2F7 + #1C1C1E #2C2C2E diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index 17d63e9f5d84..71213008eb88 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -157,6 +157,9 @@ @color/white #3C3C43 + + #993C3C43 + #F2F2F7 #2C2C2E diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index e659b3615736..828d6c5213ac 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -263,9 +263,9 @@ 40dp 8dp 72dp + 200dp - 6dp 240dp 10dp 0dp @@ -273,8 +273,6 @@ 20dp 17dp 22dp - 14dp - 6dp 8dp 12dp 18dp diff --git a/WordPress/src/main/res/values/reader_styles.xml b/WordPress/src/main/res/values/reader_styles.xml index f9aea6b3e587..64b2b7484182 100644 --- a/WordPress/src/main/res/values/reader_styles.xml +++ b/WordPress/src/main/res/values/reader_styles.xml @@ -124,14 +124,6 @@ bold - - - - - - -