Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow conversation by push notification. #15459

Merged
merged 36 commits into from Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
81b18e3
Add bell icon.
develric Oct 14, 2021
9d7e8a4
Adding selector for menu disabled state.
develric Oct 14, 2021
c90b837
Adding string resources.
develric Oct 14, 2021
b50dc32
Adding secondary button style.
develric Oct 14, 2021
eb2c6be
Adding dimension resources.
develric Oct 14, 2021
5ce5422
Adding threaded comments menu resource.
develric Oct 14, 2021
3876c30
Adding feature config.
develric Oct 14, 2021
96e2ec2
Adjust network response parsing and add function to manage push subsc…
develric Oct 14, 2021
7a91675
Adding push notification function to use case and handler.
develric Oct 14, 2021
e8be60f
Changing default visibility for following button.
develric Oct 14, 2021
e982bee
Adding push notification functions to view model.
develric Oct 14, 2021
a04bbeb
Creating bottom sheet for follow conversation management.
develric Oct 14, 2021
b36b037
Adding follow conversation management menu.
develric Oct 14, 2021
f5dc0b3
Fixing unit testing.
develric Oct 14, 2021
e9027d2
Fixing linter.
develric Oct 14, 2021
39aff40
Fixing build.
develric Oct 14, 2021
8a49521
Removing unused dimension.
develric Oct 14, 2021
3a8933c
Removed commented section.
develric Oct 14, 2021
bccbd56
File reformatted.
develric Oct 14, 2021
b66b4a6
Extracting strings into constants.
develric Oct 14, 2021
ed5d7d6
Using named arguments for Success parameters.
develric Oct 14, 2021
fed5c60
Refactoring access to function parameters.
develric Oct 14, 2021
7a2956f
Refactored onCreate as per review feedback.
develric Oct 14, 2021
252413d
Refactored onViewCreated as per review feedback.
develric Oct 14, 2021
e6c661d
Moved vals to consts.
develric Oct 14, 2021
0f5b8d6
Fixing typo.
develric Oct 14, 2021
cd5e35d
Refactor to move a bit of logic in VM.
develric Oct 14, 2021
9c2964c
Merge branch 'develop' into feature/follow-by-notification
develric Oct 14, 2021
fef8a95
Fixing detekt.
develric Oct 14, 2021
54d53f8
Adding unit testing to ReaderCommentListViewModelTest.
develric Oct 15, 2021
4861150
Adding unit testing to ReaderFollowCommentsHandler.
develric Oct 15, 2021
5c52fd2
More unit testing to ReaderFollowCommentsHandler.
develric Oct 15, 2021
bfd6134
Adding unit testing to ReaderCommentsFollowUseCaseTest.
develric Oct 15, 2021
ccb51c7
Updating release notes.
develric Oct 15, 2021
5813fe0
Enabling the feature flag.
develric Oct 15, 2021
fe94325
Merge pull request #15469 from wordpress-mobile/feature/follow-by-not…
develric Oct 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions WordPress/build.gradle
Expand Up @@ -112,6 +112,7 @@ android {
buildConfigField "boolean", "RECOMMEND_THE_APP", "false"
buildConfigField "boolean", "UNIFIED_COMMENTS_COMMENT_EDIT", "false"
buildConfigField "boolean", "MY_SITE_DASHBOARD_PHASE_2", "false"
buildConfigField "boolean", "FOLLOW_BY_PUSH_NOTIFICATION", "false"

manifestPlaceholders = [magicLinkScheme:"wordpress"]
}
Expand Down Expand Up @@ -180,6 +181,8 @@ android {

versionName versionProperties.getProperty("alpha.versionName")
versionCode versionProperties.getProperty("alpha.versionCode").toInteger()

buildConfigField "boolean", "FOLLOW_BY_PUSH_NOTIFICATION", "true"
}

jalapeno { // Pre-Alpha version, used for PR builds, can be installed along release, alpha, beta, dev versions
Expand Down
Expand Up @@ -165,6 +165,7 @@
import org.wordpress.android.ui.publicize.adapters.PublicizeServiceAdapter;
import org.wordpress.android.ui.quickstart.QuickStartFullScreenDialogFragment;
import org.wordpress.android.ui.quickstart.QuickStartReminderReceiver;
import org.wordpress.android.ui.reader.CommentNotificationsBottomSheetFragment;
import org.wordpress.android.ui.reader.ReaderBlogFragment;
import org.wordpress.android.ui.reader.ReaderCommentListActivity;
import org.wordpress.android.ui.reader.ReaderFragment;
Expand Down Expand Up @@ -703,6 +704,8 @@ public interface AppComponent extends AndroidInjector<WordPress> {

void inject(DebugCookiesFragment object);

void inject(CommentNotificationsBottomSheetFragment object);

// Allows us to inject the application without having to instantiate any modules, and provides the Application
// in the app graph
@Component.Builder
Expand Down
@@ -0,0 +1,94 @@
package org.wordpress.android.ui.reader

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SwitchCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.snackbar.Snackbar
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.databinding.CommentNotificationsBottomSheetBinding
import org.wordpress.android.ui.utils.UiHelpers
import org.wordpress.android.viewmodel.ContextProvider
import org.wordpress.android.viewmodel.observeEvent
import org.wordpress.android.widgets.WPSnackbar
import javax.inject.Inject

class CommentNotificationsBottomSheetFragment : BottomSheetDialogFragment() {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var contextProvider: ContextProvider
@Inject lateinit var uiHelpers: UiHelpers
private lateinit var viewModel: ReaderCommentListViewModel

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.comment_notifications_bottom_sheet, container)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val isReceivingPushNotifications = requireArguments().getBoolean(ARG_IS_RECEIVING_PUSH_NOTIFICATIONS)

with(CommentNotificationsBottomSheetBinding.bind(view)) {
viewModel = ViewModelProvider(
requireActivity(),
viewModelFactory
).get(ReaderCommentListViewModel::class.java)

if (savedInstanceState == null) {
enablePushNotifications.isChecked = isReceivingPushNotifications
}

unfollowConversation.setOnClickListener {
viewModel.onUnfollowTapped()
}

enablePushNotifications.setOnClickListener {
viewModel.onChangePushNotificationsRequest((it as SwitchCompat).isChecked)
}

viewModel.snackbarEvents.observeEvent(viewLifecycleOwner, { messageHolder ->
if (!isAdded) return@observeEvent

WPSnackbar.make(
coordinator,
uiHelpers.getTextOfUiString(contextProvider.getContext(), messageHolder.message),
Snackbar.LENGTH_LONG
).show()
})

viewModel.pushNotificationsStatusUpdate.observeEvent(viewLifecycleOwner, { isReceivingPushNotifications ->
enablePushNotifications.isChecked = isReceivingPushNotifications
})
}

(dialog as? BottomSheetDialog)?.apply {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
}
}

override fun onAttach(context: Context) {
super.onAttach(context)
(requireActivity().applicationContext as WordPress).component().inject(this)
}

companion object {
private const val ARG_IS_RECEIVING_PUSH_NOTIFICATIONS = "ARG_IS_RECEIVING_PUSH_NOTIFICATIONS"

@JvmStatic
fun newInstance(isReceivingPushNotifications: Boolean): CommentNotificationsBottomSheetFragment {
val fragment = CommentNotificationsBottomSheetFragment()
val bundle = Bundle()
bundle.putBoolean(ARG_IS_RECEIVING_PUSH_NOTIFICATIONS, isReceivingPushNotifications)
fragment.arguments = bundle
return fragment
}
}
}
Expand Up @@ -2,10 +2,11 @@ package org.wordpress.android.ui.reader

data class FollowCommentsUiState(
val type: FollowCommentsUiStateType,
val showFollowButton: Boolean = false,
val isFollowing: Boolean = false,
val animate: Boolean = false,
val onFollowButtonClick: ((Boolean) -> Unit)? = null
val showFollowButton: Boolean,
val isFollowing: Boolean,
val animate: Boolean,
val onFollowButtonClick: ((Boolean) -> Unit)?,
val isReceivingNotifications: Boolean
)

enum class FollowCommentsUiStateType {
Expand Down
Expand Up @@ -7,6 +7,8 @@
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -20,12 +22,15 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;

import com.facebook.shimmer.ShimmerFrameLayout;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.snackbar.Snackbar;

Expand Down Expand Up @@ -74,6 +79,7 @@
import org.wordpress.android.util.ViewUtilsKt;
import org.wordpress.android.util.WPActivityUtils;
import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource;
import org.wordpress.android.util.config.FollowByPushNotificationFeatureConfig;
import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
import org.wordpress.android.widgets.RecyclerItemDecoration;
import org.wordpress.android.widgets.SuggestionAutoCompleteText;
Expand All @@ -96,6 +102,8 @@ public class ReaderCommentListActivity extends LocaleAwareActivity implements On
private static final String KEY_REPLY_TO_COMMENT_ID = "reply_to_comment_id";
private static final String KEY_HAS_UPDATED_COMMENTS = "has_updated_comments";

private static final String NOTIFICATIONS_BOTTOM_SHEET_TAG = "NOTIFICATIONS_BOTTOM_SHEET_TAG";

private long mPostId;
private long mBlogId;
private ReaderPost mPost;
Expand All @@ -105,6 +113,7 @@ public class ReaderCommentListActivity extends LocaleAwareActivity implements On

private SwipeToRefreshHelper mSwipeToRefreshHelper;
private ReaderRecyclerView mRecyclerView;
private CoordinatorLayout mCoordinator;
private SuggestionAutoCompleteText mEditComment;
private View mSubmitReplyBtn;
private ViewGroup mCommentBox;
Expand All @@ -124,6 +133,7 @@ public class ReaderCommentListActivity extends LocaleAwareActivity implements On
@Inject UiHelpers mUiHelpers;
@Inject ViewModelProvider.Factory mViewModelFactory;
@Inject ReaderTracker mReaderTracker;
@Inject FollowByPushNotificationFeatureConfig mFollowByPushNotificationFeatureConfig;

private ReaderCommentListViewModel mViewModel;

Expand Down Expand Up @@ -167,22 +177,56 @@ public void onCreate(Bundle savedInstanceState) {
}
});

mViewModel.getSnackbarEvents().observe(this, event ->
event.applyIfNotHandled(holder -> {
WPSnackbar.make(mRecyclerView,
mUiHelpers.getTextOfUiString(this, holder.getMessage()),
Snackbar.LENGTH_LONG)
.show();
return Unit.INSTANCE;
})
);
mViewModel.getSnackbarEvents().observe(this, snackbarMessageHolderEvent -> {
ashiagr marked this conversation as resolved.
Show resolved Hide resolved
FragmentManager fm = getSupportFragmentManager();
CommentNotificationsBottomSheetFragment bottomSheet =
(CommentNotificationsBottomSheetFragment) fm.findFragmentByTag(NOTIFICATIONS_BOTTOM_SHEET_TAG);

if (bottomSheet != null) return;

snackbarMessageHolderEvent.applyIfNotHandled(holder -> {
WPSnackbar.make(mCoordinator,
mUiHelpers.getTextOfUiString(ReaderCommentListActivity.this, holder.getMessage()),
Snackbar.LENGTH_LONG)
.setAction(
holder.getButtonTitle() != null
? mUiHelpers.getTextOfUiString(
ReaderCommentListActivity.this,
holder.getButtonTitle())
: null,
v -> holder.getButtonAction().invoke())
.show();
return Unit.INSTANCE;
});
});

mViewModel.getUpdateFollowUiState().observe(this, uiState -> {
if (mCommentAdapter != null) {
mCommentAdapter.updateFollowingState(uiState);
if (!mFollowByPushNotificationFeatureConfig.isEnabled()) {
mViewModel.getUpdateFollowUiState().observe(this, uiState -> {
if (mCommentAdapter != null) {
mCommentAdapter.updateFollowingState(uiState);
}
}
}
);
);
} else {
mViewModel.getShowBottomSheetEvent().observe(this, event ->
event.applyIfNotHandled(isShowingData -> {
FragmentManager fm = getSupportFragmentManager();
CommentNotificationsBottomSheetFragment bottomSheet =
(CommentNotificationsBottomSheetFragment) fm.findFragmentByTag(
NOTIFICATIONS_BOTTOM_SHEET_TAG
);
if (isShowingData.getShow() && bottomSheet == null) {
bottomSheet = CommentNotificationsBottomSheetFragment.newInstance(
isShowingData.isReceivingNotifications()
);
bottomSheet.show(fm, NOTIFICATIONS_BOTTOM_SHEET_TAG);
} else if (!isShowingData.getShow() && bottomSheet != null) {
bottomSheet.dismiss();
}
return Unit.INSTANCE;
})
);
}

Toolbar toolbar = findViewById(R.id.toolbar_main);
setSupportActionBar(toolbar);
Expand Down Expand Up @@ -217,6 +261,8 @@ public void onCreate(Bundle savedInstanceState) {
}
);

mCoordinator = findViewById(R.id.coordinator_layout);

mRecyclerView = findViewById(R.id.recycler_view);
int spacingHorizontal = 0;
int spacingVertical = DisplayUtils.dpToPx(this, 1);
Expand Down Expand Up @@ -399,13 +445,100 @@ public void onEventMainThread(SuggestionEvents.SuggestionNameListUpdated event)
}
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
if (mFollowByPushNotificationFeatureConfig.isEnabled()) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.threaded_comments_menu, menu);

mViewModel.getUpdateFollowUiState().observe(this, uiState -> {
if (menu != null) {
MenuItem bellItem = menu.findItem(R.id.manage_notifications_item);
MenuItem followItem = menu.findItem(R.id.follow_item);

if (bellItem != null && followItem != null) {
ShimmerFrameLayout shimmerView =
followItem.getActionView().findViewById(R.id.shimmer_view_container);
TextView followText = followItem.getActionView().findViewById(R.id.follow_button);

FollowCommentsUiStateType stateType = uiState.getType();
boolean isButtonEnabled = stateType != FollowCommentsUiStateType.DISABLED
&& stateType != FollowCommentsUiStateType.LOADING;

followItem.getActionView().setOnClickListener(v -> mViewModel.onFollowTapped());

bellItem.setOnMenuItemClickListener(item -> {
mViewModel.onManageNotificationsTapped();
return true;
});

setMenuStatesByStateType(stateType,
followItem,
bellItem,
followText,
shimmerView,
isButtonEnabled,
uiState.isFollowing());
}
ashiagr marked this conversation as resolved.
Show resolved Hide resolved
}
}
);
}
return true;
}

private void setMenuStatesByStateType(FollowCommentsUiStateType stateType,
MenuItem followItem,
MenuItem bellItem,
TextView followText,
ShimmerFrameLayout shimmerView,
boolean isButtonEnabled,
boolean isFollowing) {
followItem.getActionView().setEnabled(isButtonEnabled);
followText.setEnabled(isButtonEnabled);
bellItem.setEnabled(isButtonEnabled);

shimmerView.hideShimmer();

switch (stateType) {
case DISABLED:
followItem.getActionView().setEnabled(false);
followText.setEnabled(false);
followItem.getActionView().setOnClickListener(null);
followItem.setVisible(true);
bellItem.setVisible(false);
break;
case LOADING:
followItem.getActionView().setOnClickListener(null);
followItem.setVisible(true);
if (!shimmerView.isShimmerVisible()) {
shimmerView.showShimmer(true);
} else if (!shimmerView.isShimmerStarted()) {
shimmerView.startShimmer();
}

bellItem.setVisible(false);
break;
case GONE:
followItem.setVisible(false);
bellItem.setVisible(false);
break;
case VISIBLE_WITH_STATE:
followItem.setVisible(!isFollowing);
bellItem.setVisible(isFollowing);
break;
}
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else {
return super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}

Expand Down Expand Up @@ -584,7 +717,7 @@ private void doDirectOperation() {
case COMMENT_LIKE:
getCommentAdapter().setHighlightCommentId(mCommentId, false);
if (!mAccountStore.hasAccessToken()) {
WPSnackbar.make(mRecyclerView,
WPSnackbar.make(mCoordinator,
R.string.reader_snackbar_err_cannot_like_post_logged_out,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.sign_in, mSignInClickListener)
Expand Down