diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 586093d9b94f..73b57975d40d 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,7 @@ 13.3 ----- +* Added ability to change the username via Account Settings + * Add Jetpack performance settings * Sort more than 100 published pages chronologically like Calypso * Significant performance improvements to Stats diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 89dc89ebce14..fa9dc750256f 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -25,7 +25,6 @@ import org.wordpress.android.ui.accounts.LoginMagicLinkInterceptActivity; import org.wordpress.android.ui.accounts.login.LoginEpilogueFragment; import org.wordpress.android.ui.accounts.signup.SignupEpilogueFragment; -import org.wordpress.android.ui.accounts.signup.UsernameChangerFullScreenDialogFragment; import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailFragment; import org.wordpress.android.ui.activitylog.list.ActivityLogListActivity; import org.wordpress.android.ui.activitylog.list.ActivityLogListFragment; @@ -208,8 +207,6 @@ public interface AppComponent extends AndroidInjector { void inject(SignupEpilogueFragment object); - void inject(UsernameChangerFullScreenDialogFragment object); - void inject(SiteCreationActivity object); void inject(SiteCreationSegmentsFragment object); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index ad03190b361d..2245fe71f341 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -6,6 +6,8 @@ import androidx.lifecycle.LiveData; import org.wordpress.android.ui.CommentFullScreenDialogFragment; +import org.wordpress.android.ui.accounts.signup.SettingsUsernameChangerFragment; +import org.wordpress.android.ui.accounts.signup.UsernameChangerFullScreenDialogFragment; import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment.CountryPickerDialogFragment; import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment.StatePickerDialogFragment; import org.wordpress.android.ui.news.LocalNewsService; @@ -18,8 +20,8 @@ import org.wordpress.android.ui.stats.refresh.lists.detail.StatsDetailFragment; import org.wordpress.android.ui.stats.refresh.lists.sections.insights.management.InsightsManagementFragment; import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetColorSelectionDialogFragment; -import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetDataTypeSelectionDialogFragment; import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment; +import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetDataTypeSelectionDialogFragment; import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetSiteSelectionDialogFragment; import org.wordpress.android.ui.stats.refresh.lists.widget.minified.StatsMinifiedWidgetConfigureFragment; import org.wordpress.android.util.wizard.WizardManager; @@ -81,8 +83,15 @@ public static NewsService provideLocalNewsService(Context context) { @ContributesAndroidInjector abstract CommentFullScreenDialogFragment contributecommentFullScreenDialogFragment(); + @ContributesAndroidInjector + abstract UsernameChangerFullScreenDialogFragment contributeUsernameChangerFullScreenDialogFragment(); + + @ContributesAndroidInjector + abstract SettingsUsernameChangerFragment contributeSettingsUsernameChangerFragment(); + @Provides - public static WizardManager provideWizardManager(SiteCreationStepsProvider stepsProvider) { + public static WizardManager provideWizardManager( + SiteCreationStepsProvider stepsProvider) { return new WizardManager<>(stepsProvider.getSteps()); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java index 0639abf1f893..21c818267505 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java @@ -43,6 +43,7 @@ public class FullScreenDialogFragment extends DialogFragment { private OnConfirmListener mOnConfirmListener; private OnDismissListener mOnDismissListener; private String mAction; + private MenuItem mActionItem; private String mSubtitle; private String mTitle; private Toolbar mToolbar; @@ -70,6 +71,8 @@ public interface FullScreenDialogController { void confirm(@Nullable Bundle result); void dismiss(); + + void setActionEnabled(boolean enabled); } public interface OnConfirmListener { @@ -108,7 +111,7 @@ public void onActivityCreated(Bundle savedInstanceState) { getChildFragmentManager() .beginTransaction() .setCustomAnimations(R.anim.full_screen_dialog_fragment_none, 0, 0, - R.anim.full_screen_dialog_fragment_none) + R.anim.full_screen_dialog_fragment_none) .add(R.id.full_screen_dialog_fragment_content, mFragment) .commitNow(); } @@ -132,6 +135,12 @@ public void confirm(@Nullable Bundle result) { public void dismiss() { FullScreenDialogFragment.this.dismiss(); } + + @Override public void setActionEnabled(boolean enabled) { + if (mActionItem != null) { + mActionItem.setEnabled(enabled); + } + } }; } @@ -198,7 +207,7 @@ public void show(FragmentManager manager, String tag) { public int show(FragmentTransaction transaction, String tag) { initBuilderArguments(); transaction.setCustomAnimations(R.anim.full_screen_dialog_fragment_slide_up, 0, 0, - R.anim.full_screen_dialog_fragment_slide_down); + R.anim.full_screen_dialog_fragment_slide_down); return transaction.add(android.R.id.content, this, tag).addToBackStack(null).commit(); } @@ -268,9 +277,9 @@ public void onClick(View view) { if (!mAction.isEmpty()) { Menu menu = mToolbar.getMenu(); - MenuItem action = menu.add(0, ID_ACTION, 0, this.mAction); - action.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - action.setOnMenuItemClickListener( + mActionItem = menu.add(0, ID_ACTION, 0, this.mAction); + mActionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + mActionItem.setOnMenuItemClickListener( new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { @@ -455,11 +464,11 @@ public Builder setAction(@StringRes int textId) { /** * Set {@link Fragment} to be added as dialog, which must implement {@link FullScreenDialogContent}. * - * @param contentClass Fragment class to be instantiated + * @param contentClass Fragment class to be instantiated * @param contentArguments arguments to be added to Fragment * @return {@link Builder} object to allow for chaining of calls to set methods * @throws IllegalArgumentException if content class does not implement - * {@link FullScreenDialogContent} interface + * {@link FullScreenDialogContent} interface */ public Builder setContent(Class contentClass, @Nullable Bundle contentArguments) { if (!FullScreenDialogContent.class.isAssignableFrom(contentClass)) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java similarity index 76% rename from WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.java rename to WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java index 36d5478782a5..1612a260cd3f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java @@ -18,7 +18,6 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; @@ -28,8 +27,8 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.R; -import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.generated.AccountActionBuilder; import org.wordpress.android.fluxc.store.AccountStore.FetchUsernameSuggestionsPayload; @@ -47,24 +46,30 @@ import javax.inject.Inject; -public class UsernameChangerFullScreenDialogFragment extends Fragment implements +import dagger.android.support.DaggerFragment; + +/** + * Created so that the base suggestions functionality can become shareable as similar functionality is being used in the + * the Account settings & sign-up flow to change the username. + */ +public abstract class BaseUsernameChangerFullScreenDialogFragment extends DaggerFragment implements FullScreenDialogContent, OnUsernameSelectedListener { private ProgressBar mProgressBar; - protected FullScreenDialogController mDialogController; - protected Handler mGetSuggestionsHandler; - protected RecyclerView mUsernameSuggestions; - protected Runnable mGetSuggestionsRunnable; - protected String mDisplayName; - protected String mUsername; - protected String mUsernameSelected; - protected String mUsernameSuggestionInput; - protected TextInputEditText mUsernameView; - protected TextView mHeaderView; - protected UsernameChangerRecyclerViewAdapter mUsernamesAdapter; - protected boolean mIsShowingDismissDialog; - protected boolean mShouldWatchText; // Flag handling text watcher to avoid network call on device rotation. - protected int mUsernameSelectedIndex; + private FullScreenDialogController mDialogController; + private Handler mGetSuggestionsHandler; + private RecyclerView mUsernameSuggestions; + private Runnable mGetSuggestionsRunnable; + private String mDisplayName; + private String mUsername; + private String mUsernameSelected; + private String mUsernameSuggestionInput; + private TextInputEditText mUsernameView; + private TextView mHeaderView; + private UsernameChangerRecyclerViewAdapter mUsernamesAdapter; + private boolean mIsShowingDismissDialog; + private boolean mShouldWatchText; // Flag handling text watcher to avoid network call on device rotation. + private int mUsernameSelectedIndex; public static final String EXTRA_DISPLAY_NAME = "EXTRA_DISPLAY_NAME"; public static final String EXTRA_USERNAME = "EXTRA_USERNAME"; @@ -78,19 +83,38 @@ public class UsernameChangerFullScreenDialogFragment extends Fragment implements @Inject protected Dispatcher mDispatcher; - protected static Bundle newBundle(String displayName, String username) { + /** + * Fragments that extend this class are required to provide the event that should be + * tracked in case fetching of the username suggestions fail. + * + * @return {@link Stat} + */ + abstract Stat getSuggestionsFailedStat(); + + /** + * Specifies if the header text should be updated when a new username is selected + * or if the the initial username should remain. + * + * @return true or false + */ + abstract boolean canHeaderTextLiveUpdate(); + + /** + * Creates the text that's displayed in the header. + * + * @param username + * @param display + * @return formatted header template + */ + abstract Spanned getHeaderText(String username, String display); + + public static Bundle newBundle(String displayName, String username) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_DISPLAY_NAME, displayName); bundle.putString(EXTRA_USERNAME, username); return bundle; } - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) getActivity().getApplication()).component().inject(this); - } - @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @@ -153,7 +177,9 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void afterTextChanged(Editable s) { if (s.toString().trim().isEmpty()) { - mHeaderView.setText(getHeaderText(getUsernameOrSelected(), mDisplayName)); + if (canHeaderTextLiveUpdate()) { + mHeaderView.setText(getHeaderText(getUsernameOrSelected(), mDisplayName)); + } mGetSuggestionsHandler.removeCallbacks(mGetSuggestionsRunnable); } else if (mShouldWatchText) { mGetSuggestionsHandler.removeCallbacks(mGetSuggestionsRunnable); @@ -191,9 +217,7 @@ public boolean onConfirmClicked(FullScreenDialogController controller) { ActivityUtils.hideKeyboard(getActivity()); if (mUsernamesAdapter != null && mUsernamesAdapter.mItems != null) { - Bundle result = new Bundle(); - result.putString(RESULT_USERNAME, mUsernamesAdapter.mItems.get(mUsernamesAdapter.getSelectedItem())); - controller.confirm(result); + onUsernameConfirmed(controller, mUsernameSelected); } else { controller.dismiss(); } @@ -201,6 +225,8 @@ public boolean onConfirmClicked(FullScreenDialogController controller) { return true; } + public abstract void onUsernameConfirmed(FullScreenDialogController controller, String usernameSelected); + @Override public boolean onDismissClicked(FullScreenDialogController controller) { ActivityUtils.hideKeyboard(getActivity()); @@ -226,30 +252,22 @@ public void onSaveInstanceState(Bundle outState) { @Override public void onUsernameSelected(String username) { - mHeaderView.setText(getHeaderText(username, mDisplayName)); + if (canHeaderTextLiveUpdate()) { + mHeaderView.setText(getHeaderText(username, mDisplayName)); + } mUsernameSelected = username; mUsernameSelectedIndex = mUsernamesAdapter.getSelectedItem(); } - protected Spanned getHeaderText(String username, String display) { - return Html.fromHtml( - String.format( - getString(R.string.username_changer_header), - "", - username, - "", - "", - display, - "" - ) - ); + private String getUsernameOrSelected() { + return TextUtils.isEmpty(mUsernameSelected) ? mUsername : mUsernameSelected; } - protected String getUsernameOrSelected() { - return TextUtils.isEmpty(mUsernameSelected) ? mUsername : mUsernameSelected; + public String getUsernameSelected() { + return mUsernameSelected; } - protected void getUsernameSuggestions(String usernameQuery) { + private void getUsernameSuggestions(String usernameQuery) { showProgress(true); FetchUsernameSuggestionsPayload payload = new FetchUsernameSuggestionsPayload(usernameQuery); @@ -260,7 +278,7 @@ private String getUsernameQueryFromDisplayName() { return mDisplayName.replace(" ", "").toLowerCase(Locale.ROOT); } - protected boolean hasUsernameChanged() { + public boolean hasUsernameChanged() { return !TextUtils.equals(mUsername, mUsernameSelected); } @@ -282,30 +300,30 @@ private void populateUsernameSuggestions(List suggestions) { private void setUsernameSuggestions(List suggestions) { mUsernamesAdapter = new UsernameChangerRecyclerViewAdapter(getActivity(), suggestions); - mUsernamesAdapter.setOnUsernameSelectedListener(UsernameChangerFullScreenDialogFragment.this); + mUsernamesAdapter.setOnUsernameSelectedListener(BaseUsernameChangerFullScreenDialogFragment.this); mUsernamesAdapter.setSelectedItem(mUsernameSelectedIndex); mUsernameSuggestions.setAdapter(mUsernamesAdapter); } - protected void showDismissDialog() { + private void showDismissDialog() { mIsShowingDismissDialog = true; new AlertDialog.Builder(new ContextThemeWrapper(getContext(), R.style.LoginTheme)) .setMessage(R.string.username_changer_dismiss_message) .setPositiveButton(R.string.username_changer_dismiss_button_positive, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mDialogController.dismiss(); - } - }) + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mDialogController.dismiss(); + } + }) .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mIsShowingDismissDialog = false; - } - }) + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mIsShowingDismissDialog = false; + } + }) .show(); } @@ -328,7 +346,7 @@ public void onUsernameSuggestionsFetched(OnUsernameSuggestionsFetched event) { showProgress(false); if (event.isError()) { - AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_SUGGESTIONS_FAILED); + AnalyticsTracker.track(getSuggestionsFailedStat()); AppLog.e(T.API, "onUsernameSuggestionsFetched: " + event.error.type + " - " + event.error.message); showErrorDialog(new SpannedString(getString(R.string.username_changer_error_generic))); } else if (event.suggestions.size() == 0) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt new file mode 100644 index 000000000000..89b010bebdc4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt @@ -0,0 +1,178 @@ +package org.wordpress.android.ui.accounts.signup + +import android.app.ProgressDialog +import android.os.Bundle +import android.text.Editable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextWatcher +import android.view.ContextThemeWrapper +import android.widget.EditText +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.text.HtmlCompat +import com.google.android.material.snackbar.Snackbar +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.R.style +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ACCOUNT_SETTINGS_CHANGE_USERNAME_FAILED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ACCOUNT_SETTINGS_CHANGE_USERNAME_SUCCEEDED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ACCOUNT_SETTINGS_CHANGE_USERNAME_SUGGESTIONS_FAILED +import org.wordpress.android.fluxc.generated.AccountActionBuilder +import org.wordpress.android.fluxc.store.AccountStore.AccountUsernameActionType.KEEP_OLD_SITE_AND_ADDRESS +import org.wordpress.android.fluxc.store.AccountStore.OnUsernameChanged +import org.wordpress.android.fluxc.store.AccountStore.PushUsernamePayload +import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogController +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T +import org.wordpress.android.widgets.WPDialogSnackbar + +/** + * Allows the user to change their username from the Account Settings screen. + */ +class SettingsUsernameChangerFragment : BaseUsernameChangerFullScreenDialogFragment() { + private lateinit var dialogController: FullScreenDialogController + private var progressDialog: ProgressDialog? = null + + override fun getSuggestionsFailedStat() = ACCOUNT_SETTINGS_CHANGE_USERNAME_SUGGESTIONS_FAILED + override fun canHeaderTextLiveUpdate() = false + override fun getHeaderText(username: String?, display: String?): Spanned = HtmlCompat.fromHtml( + String.format( + getString(R.string.settings_username_changer_header), + "", + username, + "" + ), HtmlCompat.FROM_HTML_MODE_LEGACY + ) + + override fun onViewCreated(controller: FullScreenDialogController) { + super.onViewCreated(controller) + dialogController = controller + + dialogController.setActionEnabled(false) + } + + /** + * The Save Action will only be enabled when a new username has been selected. + */ + override fun onUsernameSelected(username: String?) { + super.onUsernameSelected(username) + dialogController.setActionEnabled(hasUsernameChanged()) + } + + override fun onUsernameConfirmed(controller: FullScreenDialogController, usernameSelected: String) { + showUsernameConfirmationDialog(usernameSelected) + } + + /** + * Shows a confirmation dialog that prompts the user to verify that they want to change the username + * by providing a field for the user to type the username. Once the username is typed correctly + * the "Change Username" button becomes enabled and they are able to save their username. + */ + private fun showUsernameConfirmationDialog(username: String) { + // Created a custom layout that includes an EditText and TextView to replicate the confirmation + // dialog functionality on iOS. + val layout = layoutInflater.inflate(R.layout.settings_username_changer_confirm_dialog, null) + val content = layout.findViewById(R.id.content) + val usernameControl = layout.findViewById(R.id.username_edit) + + content.text = HtmlCompat.fromHtml( + String.format( + getString(R.string.settings_username_changer_confirm_dialog_content), + "", + username, + "" + ), HtmlCompat.FROM_HTML_MODE_LEGACY + ) + + AlertDialog.Builder(ContextThemeWrapper(activity, style.Calypso_Dialog_Alert)).apply { + setTitle(R.string.settings_username_changer_confirm_dialog_title) + setView(layout) + setPositiveButton( + R.string.settings_username_changer_confirm_dialog_positive_action + ) { _, _ -> saveUsername(username) } + setNegativeButton(android.R.string.cancel, null) + create() + }.show().also { alertDialog -> + // The change username button is disabled at start. + val changeUsernameButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).apply { + isEnabled = false + } + + // Toggles the enabled property of the button based on the correctness of the username being typed. + usernameControl.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + editable?.let { + changeUsernameButton.isEnabled = (it.toString() == username) + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + } + }) + usernameControl.requestFocus() + } + } + + private fun saveUsername(username: String) { + showProgress() + val payload = PushUsernamePayload( + username, KEEP_OLD_SITE_AND_ADDRESS + ) + mDispatcher.dispatch(AccountActionBuilder.newPushUsernameAction(payload)) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onUsernameChanged(event: OnUsernameChanged) { + if (event.isError) { + AnalyticsTracker.track(ACCOUNT_SETTINGS_CHANGE_USERNAME_FAILED) + AppLog.e( + T.API, "SettingsUsernameChangerFragment.onUsernameChanged: " + + event.error.type + " - " + event.error.message + ) + endProgress() + showErrorDialog(SpannableStringBuilder(getString(R.string.signup_epilogue_error_generic))) + } else if (event.username != null) { + AnalyticsTracker.track(ACCOUNT_SETTINGS_CHANGE_USERNAME_SUCCEEDED) + endProgress() + val result = Bundle().apply { putString(RESULT_USERNAME, event.username) } + dialogController.confirm(result) + } + } + + fun showProgress() { + if (progressDialog == null || progressDialog?.window == null || progressDialog?.isShowing == false) { + progressDialog = ProgressDialog(context).apply { + isIndeterminate = true + setCancelable(true) + setMessage(getString(R.string.settings_username_changer_progress_dialog)) + setOnCancelListener { showChangeUsernameActionCancelledMessage() } + } + } + + progressDialog?.let { + if (!it.isShowing) { + it.show() + } + } + } + + private fun endProgress() { + progressDialog?.let { + if (it.isShowing && it.window != null) { + it.dismiss() + } + } + progressDialog = null + } + + private fun showChangeUsernameActionCancelledMessage() = view?.let { + WPDialogSnackbar.make(it, getString(R.string.settings_username_changer_snackbar_cancel), Snackbar.LENGTH_LONG) + .show() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt new file mode 100644 index 000000000000..7c2fea8bdc66 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt @@ -0,0 +1,32 @@ +package org.wordpress.android.ui.accounts.signup + +import android.os.Bundle +import android.text.Html +import android.text.Spanned +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_SUGGESTIONS_FAILED +import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogController + +/** + * Implements functionality specific to the Username Changer functionality in the sign-up flow. + */ +class UsernameChangerFullScreenDialogFragment : BaseUsernameChangerFullScreenDialogFragment() { + override fun getSuggestionsFailedStat() = SIGNUP_SOCIAL_EPILOGUE_USERNAME_SUGGESTIONS_FAILED + override fun canHeaderTextLiveUpdate() = true + override fun getHeaderText(username: String?, display: String?): Spanned = Html.fromHtml( + String.format( + getString(R.string.username_changer_header), + "", + username, + "", + "", + display, + "" + ) + ) + + override fun onUsernameConfirmed(controller: FullScreenDialogController, usernameSelected: String) { + val result = Bundle().apply { putString(RESULT_USERNAME, usernameSelected) } + controller.confirm(result) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java index 80da04c6178c..8b02ac8304e5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java @@ -4,6 +4,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceFragment; import android.text.InputType; import android.text.TextUtils; @@ -14,6 +15,8 @@ import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.material.snackbar.Snackbar; @@ -30,11 +33,16 @@ import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload; import org.wordpress.android.fluxc.store.SiteStore; +import org.wordpress.android.ui.FullScreenDialogFragment; +import org.wordpress.android.ui.FullScreenDialogFragment.OnConfirmListener; +import org.wordpress.android.ui.accounts.signup.BaseUsernameChangerFullScreenDialogFragment; +import org.wordpress.android.ui.accounts.signup.SettingsUsernameChangerFragment; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.widgets.WPSnackbar; import java.util.ArrayList; import java.util.HashMap; @@ -43,7 +51,8 @@ import javax.inject.Inject; @SuppressWarnings("deprecation") -public class AccountSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener { +public class AccountSettingsFragment extends PreferenceFragment implements OnPreferenceChangeListener, + OnConfirmListener { private Preference mUsernamePreference; private EditTextPreferenceWithValidation mEmailPreference; private DetailListPreference mPrimarySitePreference; @@ -190,6 +199,7 @@ private void refreshAccountDetails() { mWebAddressPreference.setSummary(account.getWebAddress()); changePrimaryBlogPreference(account.getPrimarySiteId()); checkIfEmailChangeIsPending(); + checkIfUsernameCanBeChanged(); } private void checkIfEmailChangeIsPending() { @@ -328,6 +338,49 @@ public void onAccountChanged(OnAccountChanged event) { } } + /** + * If the username can be changed then the control can be clicked to open to the + * Username Changer screen. + */ + private void checkIfUsernameCanBeChanged() { + AccountModel account = mAccountStore.getAccount(); + mUsernamePreference.setEnabled(account.getUsernameCanBeChanged()); + mUsernamePreference.setOnPreferenceClickListener(preference -> { + showUsernameChangerFragment(); + return true; + }); + } + + private void showUsernameChangerFragment() { + AccountModel account = mAccountStore.getAccount(); + + final Bundle bundle = + SettingsUsernameChangerFragment.newBundle(account.getDisplayName(), account.getUserName()); + + new FullScreenDialogFragment.Builder(getActivity()) + .setTitle(R.string.username_changer_title) + .setAction(R.string.username_changer_action) + .setOnConfirmListener(this) + .setHideActivityBar(true) + .setOnDismissListener(null) + .setContent(SettingsUsernameChangerFragment.class, bundle) + .build() + .show(((AppCompatActivity) getActivity()).getSupportFragmentManager(), FullScreenDialogFragment.TAG); + } + + @Override public void onConfirm(@Nullable Bundle result) { + if (result != null) { + String username = result.getString(BaseUsernameChangerFullScreenDialogFragment.RESULT_USERNAME); + + if (username != null) { + WPSnackbar.make(getView(), + String.format(getString(R.string.settings_username_changer_toast_content), username), + Snackbar.LENGTH_LONG).show(); + mUsernamePreference.setSummary(username); + } + } + } + public static String[] getSiteNamesFromSites(List sites) { List blogNames = new ArrayList<>(); for (SiteModel site : sites) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/PostsAndPagesUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/PostsAndPagesUseCase.kt index 09c968446277..f2fce557f1e6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/PostsAndPagesUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/PostsAndPagesUseCase.kt @@ -7,6 +7,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.stats.LimitMode import org.wordpress.android.fluxc.model.stats.time.PostAndPageViewsModel import org.wordpress.android.fluxc.model.stats.time.PostAndPageViewsModel.ViewsType.HOMEPAGE +import org.wordpress.android.fluxc.model.stats.time.PostAndPageViewsModel.ViewsType.OTHER import org.wordpress.android.fluxc.model.stats.time.PostAndPageViewsModel.ViewsType.PAGE import org.wordpress.android.fluxc.model.stats.time.PostAndPageViewsModel.ViewsType.POST import org.wordpress.android.fluxc.network.utils.StatsGranularity @@ -113,7 +114,7 @@ constructor( items.addAll(domainModel.views.mapIndexed { index, viewsModel -> val icon = when (viewsModel.type) { POST -> R.drawable.ic_posts_white_24dp - HOMEPAGE, PAGE -> R.drawable.ic_pages_white_24dp + OTHER, HOMEPAGE, PAGE -> R.drawable.ic_pages_white_24dp } ListItemWithIcon( icon = icon, @@ -157,7 +158,7 @@ constructor( private fun onLinkClicked(params: LinkClickParams) { val type = when (params.postType) { POST -> ITEM_TYPE_POST - PAGE, HOMEPAGE -> ITEM_TYPE_HOME_PAGE + OTHER, PAGE, HOMEPAGE -> ITEM_TYPE_HOME_PAGE } analyticsTracker.trackGranular(AnalyticsTracker.Stat.STATS_POSTS_AND_PAGES_ITEM_TAPPED, statsGranularity) navigateTo( diff --git a/WordPress/src/main/res/layout/settings_username_changer_confirm_dialog.xml b/WordPress/src/main/res/layout/settings_username_changer_confirm_dialog.xml new file mode 100644 index 000000000000..83bfe4a72233 --- /dev/null +++ b/WordPress/src/main/res/layout/settings_username_changer_confirm_dialog.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index d268135331cb..aa864976da93 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -431,4 +431,7 @@ 161sp 6dp + + 20dp + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index fcfcbc229944..b1a434a090c3 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2318,6 +2318,14 @@ Type to get more suggestions Change username + You are about to change your username, which is currently %1$s%2$s%3$s.You will not be able to change your username back. + Careful! + You are changing your username to %1$s%2$s%3$s. Changing your username will also affect your Gravatar profile and Intense Debate profile addresses. Confirm your new username to continue. + Change Username + Saving username… + Your new username is %1$s + This action can\'t be canceled. Username might have been already updated. + Google took too long to respond. You may need to wait until you have a stronger internet connection. diff --git a/build.gradle b/build.gradle index 9be7d4e399c5..024671ad94b3 100644 --- a/build.gradle +++ b/build.gradle @@ -106,5 +106,5 @@ buildScan { ext { daggerVersion = '2.22.1' - fluxCVersion = '1.3.0' + fluxCVersion = '1.4.0-beta-1' } diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 142bbecd8f17..30ac61030406 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -273,6 +273,9 @@ public enum Stat { OPENED_MEDIA_LIBRARY, OPENED_BLOG_SETTINGS, OPENED_ACCOUNT_SETTINGS, + ACCOUNT_SETTINGS_CHANGE_USERNAME_SUCCEEDED, + ACCOUNT_SETTINGS_CHANGE_USERNAME_FAILED, + ACCOUNT_SETTINGS_CHANGE_USERNAME_SUGGESTIONS_FAILED, OPENED_APP_SETTINGS, OPENED_MY_PROFILE, OPENED_PEOPLE_MANAGEMENT, diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index 695f8a81a18e..d672f9008c28 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -946,6 +946,12 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "site_menu_opened"; case OPENED_ACCOUNT_SETTINGS: return "account_settings_opened"; + case ACCOUNT_SETTINGS_CHANGE_USERNAME_SUCCEEDED: + return "account_settings_change_username_succeeded"; + case ACCOUNT_SETTINGS_CHANGE_USERNAME_FAILED: + return "account_settings_change_username_failed"; + case ACCOUNT_SETTINGS_CHANGE_USERNAME_SUGGESTIONS_FAILED: + return "account_settings_change_username_suggestions_failed"; case OPENED_APP_SETTINGS: return "app_settings_opened"; case OPENED_MY_PROFILE: