diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 8d292613c065..342574d534eb 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,9 @@ *** PLEASE FOLLOW THIS FORMAT: [] [] +25.2 +----- + + 25.1 ----- * [*] [internal] Block editor: Add onContentUpdate bridge functionality [https://github.com/wordpress-mobile/gutenberg-mobile/pull/20852] diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 0ad75722b44b..6b85e13e1d23 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -154,6 +154,7 @@ android { buildConfigField "boolean", "READER_TAGS_FEED", "false" buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "false" buildConfigField "boolean", "VOICE_TO_CONTENT", "false" + buildConfigField "boolean", "READER_FLOATING_BUTTON", "false" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" diff --git a/WordPress/jetpack_metadata/release_notes.txt b/WordPress/jetpack_metadata/release_notes.txt index 928c57d6d98a..e53b804daa16 100644 --- a/WordPress/jetpack_metadata/release_notes.txt +++ b/WordPress/jetpack_metadata/release_notes.txt @@ -1,3 +1,2 @@ -The Tags feed is live! You can now see content with specific tags, all in one place. Tag, you’re it. +* [*] [internal] Block editor: Add onContentUpdate bridge functionality [https://github.com/wordpress-mobile/gutenberg-mobile/pull/20852] -We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing. diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt index 257e18b25fcd..e53b804daa16 100644 --- a/WordPress/metadata/release_notes.txt +++ b/WordPress/metadata/release_notes.txt @@ -1 +1,2 @@ -We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing. +* [*] [internal] Block editor: Add onContentUpdate bridge functionality [https://github.com/wordpress-mobile/gutenberg-mobile/pull/20852] + diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index b1bcea58b88c..774e8b9cfb2d 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ + diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskExecutor.kt b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskExecutor.kt new file mode 100644 index 000000000000..5b63e65e5b8e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskExecutor.kt @@ -0,0 +1,51 @@ +package org.wordpress.android.datasets + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Helper class to handle asynchronous I/O tasks using coroutines + * @see Introduction + */ +object AsyncTaskExecutor { + /** + * Execute a data loading task in the IO thread and handle the result on the main thread + */ + @JvmStatic + fun executeIo(scope: CoroutineScope, backgroundTask: () -> T, callback: AsyncTaskCallback) { + execute(scope, Dispatchers.IO, backgroundTask, callback) + } + + /** + * Execute a data loading task in the default thread and handle the result on the main thread + */ + @JvmStatic + fun executeDefault(scope: CoroutineScope, backgroundTask: () -> T, callback: AsyncTaskCallback) { + execute(scope, Dispatchers.Default, backgroundTask, callback) + } + + private fun execute( + scope: CoroutineScope, + dispatcher: CoroutineDispatcher, + backgroundTask: () -> T, + callback: AsyncTaskCallback + ) { + scope.launch(dispatcher) { + // handle the background task + val result = backgroundTask() + + withContext(Dispatchers.Main) { + // handle the result on the main thread + callback.onTaskFinished(result) + } + } + } + + interface AsyncTaskCallback { + fun onTaskFinished(result: T) + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskHandler.kt b/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskHandler.kt deleted file mode 100644 index b1bdfecde5a8..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/datasets/AsyncTaskHandler.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.wordpress.android.datasets - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * Helper class to handle async tasks by using coroutines - * @see Introduction - */ -object AsyncTaskHandler { - /** - * Load data in the background and handle the result on the main thread - */ - @JvmStatic - fun load(backgroundTask: () -> T, callback: AsyncTaskCallback) { - CoroutineScope(Dispatchers.IO).launch { - // handle the background task - val result = backgroundTask() - - withContext(Dispatchers.Main) { - // handle the result on the main thread - callback.onTaskFinished(result) - } - } - } - - interface AsyncTaskCallback { - fun onTaskFinished(result: T) - } -} - diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt index abf15b094bfd..6bbc8cefe071 100644 --- a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt @@ -21,7 +21,7 @@ class InAppUpdateAnalyticsTracker @Inject constructor( } fun trackAppRestartToCompleteUpdate() { - tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART) + tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART_BY_USER) } private fun createPropertyMap(updateType: Int): Map { diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt index 11ac7fc132e1..23809e8796ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt @@ -22,6 +22,10 @@ import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_T import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_NOT_AVAILABLE import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_FLEXIBLE_REQUEST_CODE import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_IMMEDIATE_REQUEST_CODE @@ -33,6 +37,7 @@ import javax.inject.Singleton @Suppress("TooManyFunctions") class InAppUpdateManagerImpl( @ApplicationContext private val applicationContext: Context, + private val coroutineScope: CoroutineScope, private val appUpdateManager: AppUpdateManager, private val remoteConfigWrapper: RemoteConfigWrapper, private val buildConfigWrapper: BuildConfigWrapper, @@ -51,8 +56,16 @@ class InAppUpdateManagerImpl( } override fun completeAppUpdate() { - inAppUpdateAnalyticsTracker.trackAppRestartToCompleteUpdate() - appUpdateManager.completeUpdate() + coroutineScope.launch(Dispatchers.Main) { + // Track the app restart to complete update + inAppUpdateAnalyticsTracker.trackAppRestartToCompleteUpdate() + + // Delay so the event above can be logged + delay(RESTART_DELAY_IN_MILLIS) + + // Complete the update + appUpdateManager.completeUpdate() + } } override fun cancelAppUpdate(updateType: Int) { @@ -226,5 +239,6 @@ class InAppUpdateManagerImpl( private const val TAG = "AppUpdateChecker" private const val PREF_NAME = "in_app_update_prefs" private const val KEY_LAST_APP_UPDATE_CHECK_VERSION = "last_app_update_check_version" + private const val RESTART_DELAY_IN_MILLIS = 500L } } 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 ddd839347355..a2d0f248d15b 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -39,6 +39,8 @@ import org.wordpress.android.viewmodel.helpers.ConnectionStatus; import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData; +import javax.inject.Named; + import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -46,6 +48,8 @@ import dagger.hilt.InstallIn; import dagger.hilt.android.qualifiers.ApplicationContext; import dagger.hilt.components.SingletonComponent; +import kotlinx.coroutines.CoroutineScope; +import static org.wordpress.android.modules.ThreadModuleKt.APPLICATION_SCOPE; @InstallIn(SingletonComponent.class) @Module(includes = AndroidInjectionModule.class) @@ -98,6 +102,7 @@ public static AppUpdateManager provideAppUpdateManager(@ApplicationContext Conte @Provides public static IInAppUpdateManager provideInAppUpdateManager( @ApplicationContext Context context, + @Named(APPLICATION_SCOPE) CoroutineScope appScope, AppUpdateManager appUpdateManager, RemoteConfigWrapper remoteConfigWrapper, BuildConfigWrapper buildConfigWrapper, @@ -108,6 +113,7 @@ public static IInAppUpdateManager provideInAppUpdateManager( return inAppUpdatesFeatureConfig.isEnabled() ? new InAppUpdateManagerImpl( context, + appScope, appUpdateManager, remoteConfigWrapper, buildConfigWrapper, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java index 12992ae5e703..dae2831880dc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java @@ -48,7 +48,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { protected void addSignupEpilogueFragment(String name, String email, String photoUrl, String username, boolean isEmail) { - SignupEpilogueFragment signupEpilogueSocialFragment = SignupEpilogueFragment.newInstance( + SignupEpilogueFragment signupEpilogueSocialFragment = SignupEpilogueFragment.Companion.newInstance( name, email, photoUrl, username, isEmail); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.fragment_container, signupEpilogueSocialFragment, SignupEpilogueFragment.TAG); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.java deleted file mode 100644 index 4e7704cd2f76..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.java +++ /dev/null @@ -1,860 +0,0 @@ -package org.wordpress.android.ui.accounts.signup; - -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.core.widget.NestedScrollView; -import androidx.core.widget.NestedScrollView.OnScrollChangeListener; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.gravatar.services.AvatarService; -import com.gravatar.services.ErrorType; -import com.gravatar.services.GravatarListener; -import com.gravatar.types.Email; -import com.yalantis.ucrop.UCrop; -import com.yalantis.ucrop.UCropActivity; - -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.action.AccountAction; -import org.wordpress.android.fluxc.generated.AccountActionBuilder; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.fluxc.store.AccountStore.AccountUsernameActionType; -import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; -import org.wordpress.android.fluxc.store.AccountStore.OnUsernameChanged; -import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload; -import org.wordpress.android.fluxc.store.AccountStore.PushUsernamePayload; -import org.wordpress.android.login.LoginBaseFormFragment; -import org.wordpress.android.login.widgets.WPLoginInputRow; -import org.wordpress.android.ui.FullScreenDialogFragment; -import org.wordpress.android.ui.FullScreenDialogFragment.OnConfirmListener; -import org.wordpress.android.ui.FullScreenDialogFragment.OnDismissListener; -import org.wordpress.android.ui.FullScreenDialogFragment.OnShownListener; -import org.wordpress.android.ui.RequestCodes; -import org.wordpress.android.ui.accounts.UnifiedLoginTracker; -import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click; -import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Step; -import org.wordpress.android.ui.photopicker.MediaPickerConstants; -import org.wordpress.android.ui.photopicker.MediaPickerLauncher; -import org.wordpress.android.ui.photopicker.PhotoPickerActivity; -import org.wordpress.android.ui.prefs.AppPrefsWrapper; -import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic; -import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.MediaUtils; -import org.wordpress.android.util.StringUtils; -import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.WPAvatarUtils; -import org.wordpress.android.util.WPMediaUtils; -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.ImageManager.RequestListener; -import org.wordpress.android.util.image.ImageType; -import org.wordpress.android.widgets.WPTextView; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - -import javax.inject.Inject; - -import static org.wordpress.android.analytics.AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_GALLERY_PICKED; -import static org.wordpress.android.analytics.AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_SHOT_NEW; - -import kotlin.Unit; - -public class SignupEpilogueFragment extends LoginBaseFormFragment - implements OnConfirmListener, OnDismissListener, OnShownListener { - private EditText mEditTextDisplayName; - private EditText mEditTextUsername; - private FullScreenDialogFragment mDialog; - private SignupEpilogueListener mSignupEpilogueListener; - - protected ImageView mHeaderAvatarAdd; - protected String mDisplayName; - protected String mEmailAddress; - protected String mPhotoUrl; - protected String mUsername; - protected WPLoginInputRow mInputPassword; - protected ImageView mHeaderAvatar; - protected WPTextView mHeaderDisplayName; - protected WPTextView mHeaderEmailAddress; - protected View mBottomShadow; - protected NestedScrollView mScrollView; - protected boolean mIsAvatarAdded; - protected boolean mIsEmailSignup; - - private boolean mIsUpdatingDisplayName = false; - private boolean mIsUpdatingPassword = false; - private boolean mHasUpdatedPassword = false; - private boolean mHasMadeUpdates = false; - - private static final String ARG_DISPLAY_NAME = "ARG_DISPLAY_NAME"; - private static final String ARG_EMAIL_ADDRESS = "ARG_EMAIL_ADDRESS"; - private static final String ARG_IS_EMAIL_SIGNUP = "ARG_IS_EMAIL_SIGNUP"; - private static final String ARG_PHOTO_URL = "ARG_PHOTO_URL"; - private static final String ARG_USERNAME = "ARG_USERNAME"; - private static final String KEY_DISPLAY_NAME = "KEY_DISPLAY_NAME"; - private static final String KEY_EMAIL_ADDRESS = "KEY_EMAIL_ADDRESS"; - private static final String KEY_IS_AVATAR_ADDED = "KEY_IS_AVATAR_ADDED"; - private static final String KEY_PHOTO_URL = "KEY_PHOTO_URL"; - private static final String KEY_USERNAME = "KEY_USERNAME"; - private static final String KEY_IS_UPDATING_DISPLAY_NAME = "KEY_IS_UPDATING_DISPLAY_NAME"; - private static final String KEY_IS_UPDATING_PASSWORD = "KEY_IS_UPDATING_PASSWORD"; - private static final String KEY_HAS_UPDATED_PASSWORD = "KEY_HAS_UPDATED_PASSWORD"; - private static final String KEY_HAS_MADE_UPDATES = "KEY_HAS_MADE_UPDATES"; - - private static final String SOURCE = "source"; - private static final String SOURCE_SIGNUP_EPILOGUE = "signup_epilogue"; - - public static final String TAG = "signup_epilogue_fragment_tag"; - - @Inject protected AccountStore mAccount; - @Inject protected Dispatcher mDispatcher; - @Inject protected ImageManager mImageManager; - @Inject protected AppPrefsWrapper mAppPrefsWrapper; - @Inject protected UnifiedLoginTracker mUnifiedLoginTracker; - @Inject protected SignupUtils mSignupUtils; - @Inject protected MediaPickerLauncher mMediaPickerLauncher; - @Inject protected AvatarService mAvatarService; - - public static SignupEpilogueFragment newInstance(String displayName, String emailAddress, - String photoUrl, String username, - boolean isEmailSignup) { - SignupEpilogueFragment signupEpilogueFragment = new SignupEpilogueFragment(); - Bundle args = new Bundle(); - args.putString(ARG_DISPLAY_NAME, displayName); - args.putString(ARG_EMAIL_ADDRESS, emailAddress); - args.putString(ARG_PHOTO_URL, photoUrl); - args.putString(ARG_USERNAME, username); - args.putBoolean(ARG_IS_EMAIL_SIGNUP, isEmailSignup); - signupEpilogueFragment.setArguments(args); - return signupEpilogueFragment; - } - - @Override - protected @LayoutRes int getContentLayout() { - return 0; // no content layout; entire view is inflated in createMainView - } - - @Override - protected @LayoutRes int getProgressBarText() { - return R.string.signup_updating_account; - } - - @Override - protected void setupLabel(@NonNull TextView label) { - // no label in this screen - } - - @Override - protected ViewGroup createMainView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return (ViewGroup) inflater.inflate(R.layout.signup_epilogue, container, false); - } - - @Override - protected void setupContent(ViewGroup rootView) { - final FrameLayout headerAvatarLayout = rootView.findViewById(R.id.login_epilogue_header_avatar_layout); - headerAvatarLayout.setEnabled(mIsEmailSignup); - headerAvatarLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mUnifiedLoginTracker.trackClick(Click.SELECT_AVATAR); - mMediaPickerLauncher.showGravatarPicker(SignupEpilogueFragment.this); - } - }); - headerAvatarLayout.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - ToastUtils.showToast(getActivity(), getString(R.string.content_description_add_avatar), - ToastUtils.Duration.SHORT); - return true; - } - }); - ViewExtensionsKt.redirectContextClickToLongPressListener(headerAvatarLayout); - mHeaderAvatarAdd = rootView.findViewById(R.id.login_epilogue_header_avatar_add); - mHeaderAvatarAdd.setVisibility(mIsEmailSignup ? View.VISIBLE : View.GONE); - mHeaderAvatar = rootView.findViewById(R.id.login_epilogue_header_avatar); - mHeaderDisplayName = rootView.findViewById(R.id.login_epilogue_header_title); - mHeaderDisplayName.setText(mDisplayName); - mHeaderEmailAddress = rootView.findViewById(R.id.login_epilogue_header_subtitle); - mHeaderEmailAddress.setText(mEmailAddress); - WPLoginInputRow inputDisplayName = rootView.findViewById(R.id.signup_epilogue_input_display); - mEditTextDisplayName = inputDisplayName.getEditText(); - mEditTextDisplayName.setText(mDisplayName); - mEditTextDisplayName.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - mDisplayName = s.toString(); - mHeaderDisplayName.setText(mDisplayName); - } - }); - WPLoginInputRow inputUsername = rootView.findViewById(R.id.signup_epilogue_input_username); - mEditTextUsername = inputUsername.getEditText(); - mEditTextUsername.setText(mUsername); - mEditTextUsername.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mUnifiedLoginTracker.trackClick(Click.EDIT_USERNAME); - launchDialog(); - } - }); - mEditTextUsername.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - if (hasFocus) { - launchDialog(); - } - } - }); - mEditTextUsername.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View view, int keyCode, KeyEvent event) { - // Consume keyboard events except for Enter (i.e. click/tap) and Tab (i.e. focus/navigation). - // The onKey method returns true if the listener has consumed the event and false otherwise - // allowing hardware keyboard users to tap and navigate, but not input text as expected. - // This allows the username changer to launch using the keyboard. - return !(keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_TAB); - } - }); - mInputPassword = rootView.findViewById(R.id.signup_epilogue_input_password); - mInputPassword.setVisibility(mIsEmailSignup ? View.VISIBLE : View.GONE); - final WPTextView passwordDetail = rootView.findViewById(R.id.signup_epilogue_input_password_detail); - passwordDetail.setVisibility(mIsEmailSignup ? View.VISIBLE : View.GONE); - - // Set focus on static text field to avoid showing keyboard on start. - mHeaderEmailAddress.requestFocus(); - - mBottomShadow = rootView.findViewById(R.id.bottom_shadow); - mScrollView = rootView.findViewById(R.id.scroll_view); - mScrollView.setOnScrollChangeListener( - (OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> showBottomShadowIfNeeded()); - // We must use onGlobalLayout here otherwise canScrollVertically will always return false - mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override public void onGlobalLayout() { - mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - showBottomShadowIfNeeded(); - } - }); - } - - private void showBottomShadowIfNeeded() { - if (mScrollView != null) { - final boolean canScrollDown = mScrollView.canScrollVertically(1); - if (mBottomShadow != null) { - mBottomShadow.setVisibility(canScrollDown ? View.VISIBLE : View.GONE); - } - } - } - - @Override - protected void setupBottomButton(Button button) { - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mUnifiedLoginTracker.trackClick(Click.CONTINUE); - updateAccountOrContinue(); - } - }); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) getActivity().getApplication()).component().inject(this); - mDispatcher.dispatch(AccountActionBuilder.newFetchAccountAction()); - - mDisplayName = getArguments().getString(ARG_DISPLAY_NAME); - mEmailAddress = getArguments().getString(ARG_EMAIL_ADDRESS); - mPhotoUrl = StringUtils.notNullStr(getArguments().getString(ARG_PHOTO_URL)); - mUsername = getArguments().getString(ARG_USERNAME); - mIsEmailSignup = getArguments().getBoolean(ARG_IS_EMAIL_SIGNUP); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (savedInstanceState == null) { - // Start loading reader tags so they will be available asap - ReaderUpdateServiceStarter.startService(WordPress.getContext(), - EnumSet.of(ReaderUpdateLogic.UpdateTask.TAGS)); - - mUnifiedLoginTracker.track(Step.SUCCESS); - if (mIsEmailSignup) { - AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_VIEWED); - - // Start progress and wait for account to be fetched before populating views when - // email does not exist in account store. - if (TextUtils.isEmpty(mAccountStore.getAccount().getEmail())) { - startProgress(false); - } else { - // Skip progress and populate views when email does exist in account store. - populateViews(); - } - } else { - AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_VIEWED); - new DownloadAvatarAndUploadGravatarThread(mPhotoUrl, mEmailAddress, mAccount.getAccessToken()).start(); - mImageManager.loadIntoCircle(mHeaderAvatar, ImageType.AVATAR_WITHOUT_BACKGROUND, mPhotoUrl); - } - } else { - mDialog = (FullScreenDialogFragment) getParentFragmentManager() - .findFragmentByTag(FullScreenDialogFragment.TAG); - - if (mDialog != null) { - mDialog.setOnConfirmListener(this); - mDialog.setOnDismissListener(this); - } - - mDisplayName = savedInstanceState.getString(KEY_DISPLAY_NAME); - mUsername = savedInstanceState.getString(KEY_USERNAME); - mIsAvatarAdded = savedInstanceState.getBoolean(KEY_IS_AVATAR_ADDED); - - if (mIsEmailSignup) { - mPhotoUrl = StringUtils.notNullStr(savedInstanceState.getString(KEY_PHOTO_URL)); - mEmailAddress = savedInstanceState.getString(KEY_EMAIL_ADDRESS); - mHeaderEmailAddress.setText(mEmailAddress); - mHeaderAvatarAdd.setVisibility(mIsAvatarAdded ? View.GONE : View.VISIBLE); - } - mImageManager.loadIntoCircle(mHeaderAvatar, ImageType.AVATAR_WITHOUT_BACKGROUND, mPhotoUrl); - - mIsUpdatingDisplayName = savedInstanceState.getBoolean(KEY_IS_UPDATING_DISPLAY_NAME); - mIsUpdatingPassword = savedInstanceState.getBoolean(KEY_IS_UPDATING_PASSWORD); - mHasUpdatedPassword = savedInstanceState.getBoolean(KEY_HAS_UPDATED_PASSWORD); - mHasMadeUpdates = savedInstanceState.getBoolean(KEY_HAS_MADE_UPDATES); - } - } - - @Override - @SuppressWarnings("deprecation") - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (isAdded()) { - switch (resultCode) { - case Activity.RESULT_OK: - switch (requestCode) { - case RequestCodes.PHOTO_PICKER: - if (data != null) { - String[] mediaUriStringsArray = - data.getStringArrayExtra(MediaPickerConstants.EXTRA_MEDIA_URIS); - - if (mediaUriStringsArray != null && mediaUriStringsArray.length > 0) { - PhotoPickerActivity.PhotoPickerMediaSource source = - PhotoPickerActivity.PhotoPickerMediaSource.fromString( - data.getStringExtra(MediaPickerConstants.EXTRA_MEDIA_SOURCE) - ); - AnalyticsTracker.Stat stat = - source == PhotoPickerActivity.PhotoPickerMediaSource.ANDROID_CAMERA - ? SIGNUP_EMAIL_EPILOGUE_GRAVATAR_SHOT_NEW - : SIGNUP_EMAIL_EPILOGUE_GRAVATAR_GALLERY_PICKED; - AnalyticsTracker.track(stat); - Uri imageUri = Uri.parse(mediaUriStringsArray[0]); - - if (imageUri != null) { - boolean wasSuccess = WPMediaUtils.fetchMediaAndDoNext( - getActivity(), imageUri, - new WPMediaUtils.MediaFetchDoNext() { - @Override - public void doNext(Uri uri) { - startCropActivity(uri); - } - }); - - if (!wasSuccess) { - AppLog.e(T.UTILS, "Can't download picked or captured image"); - } - } else { - AppLog.e(T.UTILS, "Can't parse media string"); - } - } else { - AppLog.e(T.UTILS, "Can't resolve picked or captured image"); - } - } - - break; - case UCrop.REQUEST_CROP: - AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_CROPPED); - WPMediaUtils.fetchMediaAndDoNext(getActivity(), UCrop.getOutput(data), - new WPMediaUtils.MediaFetchDoNext() { - @Override - public void doNext(Uri uri) { - startGravatarUpload(MediaUtils.getRealPathFromURI( - getActivity(), uri)); - } - }); - - break; - } - - break; - case UCrop.RESULT_ERROR: - AppLog.e(T.NUX, "Image cropping failed", UCrop.getError(data)); - ToastUtils.showToast(getActivity(), R.string.error_cropping_image, ToastUtils.Duration.SHORT); - break; - } - } - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - - if (context instanceof SignupEpilogueListener) { - mSignupEpilogueListener = (SignupEpilogueListener) context; - } else { - throw new RuntimeException(context.toString() + " must implement SignupEpilogueListener"); - } - } - - @Override - public void onConfirm(@Nullable Bundle result) { - if (result != null) { - mUsername = result.getString(UsernameChangerFullScreenDialogFragment.RESULT_USERNAME); - mEditTextUsername.setText(mUsername); - } - } - - @Override - public void onDismiss() { - Map props = new HashMap<>(); - props.put(SOURCE, SOURCE_SIGNUP_EPILOGUE); - AnalyticsTracker.track(Stat.CHANGE_USERNAME_DISMISSED, props); - mDialog = null; - } - - @Override - public void onShown() { - Map props = new HashMap<>(); - props.put(SOURCE, SOURCE_SIGNUP_EPILOGUE); - AnalyticsTracker.track(AnalyticsTracker.Stat.CHANGE_USERNAME_DISPLAYED, props); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(KEY_PHOTO_URL, mPhotoUrl); - outState.putString(KEY_DISPLAY_NAME, mDisplayName); - outState.putString(KEY_EMAIL_ADDRESS, mEmailAddress); - outState.putString(KEY_USERNAME, mUsername); - outState.putBoolean(KEY_IS_AVATAR_ADDED, mIsAvatarAdded); - outState.putBoolean(KEY_IS_UPDATING_DISPLAY_NAME, mIsUpdatingDisplayName); - outState.putBoolean(KEY_IS_UPDATING_PASSWORD, mIsUpdatingPassword); - outState.putBoolean(KEY_HAS_UPDATED_PASSWORD, mHasUpdatedPassword); - outState.putBoolean(KEY_HAS_MADE_UPDATES, mHasMadeUpdates); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - } - - @Override - protected void onHelp() { - } - - @Override - protected void onLoginFinished() { - endProgress(); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onAccountChanged(OnAccountChanged event) { - if (event.isError()) { - if (mIsUpdatingDisplayName) { - mIsUpdatingDisplayName = false; - AnalyticsTracker.track(mIsEmailSignup - ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED - : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED); - } else if (mIsUpdatingPassword) { - mIsUpdatingPassword = false; - } - - AppLog.e(T.API, "SignupEpilogueFragment.onAccountChanged: " - + event.error.type + " - " + event.error.message); - endProgress(); - - if (isPasswordInErrorMessage(event.error.message)) { - showErrorDialogWithCloseButton(event.error.message); - } else { - showErrorDialog(getString(R.string.signup_epilogue_error_generic)); - } - // Wait to populate epilogue for email interface until account is fetched and email address - // is available since flow is coming from magic link with no instance argument values. - } else if (mIsEmailSignup && event.causeOfChange == AccountAction.FETCH_ACCOUNT - && !TextUtils.isEmpty(mAccountStore.getAccount().getEmail())) { - endProgress(); - populateViews(); - } else if (event.causeOfChange == AccountAction.PUSH_SETTINGS) { - mHasMadeUpdates = true; - - if (mIsUpdatingDisplayName) { - mIsUpdatingDisplayName = false; - AnalyticsTracker.track(mIsEmailSignup - ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED - : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED); - } else if (mIsUpdatingPassword) { - mIsUpdatingPassword = false; - mHasUpdatedPassword = true; - } - - updateAccountOrContinue(); - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUsernameChanged(OnUsernameChanged event) { - if (event.isError()) { - AnalyticsTracker.track(mIsEmailSignup - ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_FAILED - : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_FAILED); - AppLog.e(T.API, "SignupEpilogueFragment.onUsernameChanged: " - + event.error.type + " - " + event.error.message); - endProgress(); - showErrorDialog(getString(R.string.signup_epilogue_error_generic)); - } else { - mHasMadeUpdates = true; - AnalyticsTracker.track(mIsEmailSignup - ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED - : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED); - updateAccountOrContinue(); - } - } - - protected boolean changedDisplayName() { - return !TextUtils.equals(mAccount.getAccount().getDisplayName(), mDisplayName); - } - - protected boolean changedPassword() { - return !TextUtils.isEmpty(mInputPassword.getEditText().getText().toString()); - } - - protected boolean changedUsername() { - return !TextUtils.equals(mAccount.getAccount().getUserName(), mUsername); - } - - private boolean isPasswordInErrorMessage(String message) { - String lowercaseMessage = message.toLowerCase(Locale.getDefault()); - String lowercasePassword = getString(R.string.password).toLowerCase(Locale.getDefault()); - return lowercaseMessage.contains(lowercasePassword); - } - - protected void launchDialog() { - AnalyticsTracker.track(mIsEmailSignup - ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_USERNAME_TAPPED - : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_TAPPED); - - final Bundle bundle = UsernameChangerFullScreenDialogFragment.newBundle( - mEditTextDisplayName.getText().toString(), mEditTextUsername.getText().toString()); - - mDialog = new FullScreenDialogFragment.Builder(getContext()) - .setTitle(R.string.username_changer_title) - .setAction(R.string.username_changer_action) - .setToolbarTheme(org.wordpress.android.login.R.style.ThemeOverlay_LoginFlow_Toolbar) - .setOnConfirmListener(this) - .setOnDismissListener(this) - .setOnShownListener(this) - .setContent(UsernameChangerFullScreenDialogFragment.class, bundle) - .build(); - - mDialog.show(requireActivity().getSupportFragmentManager(), FullScreenDialogFragment.TAG); - } - - protected void loadAvatar(final String avatarUrl, String injectFilePath) { - final boolean newAvatarUploaded = injectFilePath != null && !injectFilePath.isEmpty(); - if (newAvatarUploaded) { - // Remove specific URL entry from bitmap cache. Update it via injected request cache. - WordPress.getBitmapCache().removeSimilar(avatarUrl); - // Changing the signature invalidates Glide's cache - mAppPrefsWrapper.setAvatarVersion(mAppPrefsWrapper.getAvatarVersion() + 1); - } - - Bitmap bitmap = WordPress.getBitmapCache().get(avatarUrl); - // Avatar's API doesn't synchronously update the image at avatarUrl. There is a replication lag - // (cca 5s), before the old avatar is replaced with the new avatar. Therefore we need to use this workaround, - // which temporary saves the new image into a local bitmap cache. - if (bitmap != null) { - mImageManager.load(mHeaderAvatar, bitmap); - } else { - mImageManager.loadIntoCircle(mHeaderAvatar, ImageType.AVATAR_WITHOUT_BACKGROUND, - newAvatarUploaded ? injectFilePath : avatarUrl, new RequestListener() { - @Override - public void onLoadFailed(@Nullable Exception e, @Nullable Object model) { - AppLog.e(T.NUX, "Uploading image to Gravatar succeeded, but setting image view failed"); - showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar_view)); - } - - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Object model) { - if (newAvatarUploaded && resource instanceof BitmapDrawable) { - Bitmap bitmap = ((BitmapDrawable) resource).getBitmap(); - // create a copy since the original bitmap may by automatically recycled - bitmap = bitmap.copy(bitmap.getConfig(), true); - WordPress.getBitmapCache().put(avatarUrl, bitmap); - } - } - }, mAppPrefsWrapper.getAvatarVersion()); - } - } - - private void populateViews() { - mEmailAddress = mAccountStore.getAccount().getEmail(); - mDisplayName = mSignupUtils.createDisplayNameFromEmail(mEmailAddress); - mUsername = !TextUtils.isEmpty(mAccountStore.getAccount().getUserName()) - ? mAccountStore.getAccount().getUserName() : mSignupUtils.createUsernameFromEmail(mEmailAddress); - mHeaderDisplayName.setText(mDisplayName); - mHeaderEmailAddress.setText(mEmailAddress); - mEditTextDisplayName.setText(mDisplayName); - mEditTextUsername.setText(mUsername); - // Set fragment arguments to know if account should be updated when values change. - Bundle args = new Bundle(); - args.putString(ARG_DISPLAY_NAME, mDisplayName); - args.putString(ARG_EMAIL_ADDRESS, mEmailAddress); - args.putString(ARG_PHOTO_URL, mPhotoUrl); - args.putString(ARG_USERNAME, mUsername); - args.putBoolean(ARG_IS_EMAIL_SIGNUP, mIsEmailSignup); - setArguments(args); - } - - protected void showErrorDialog(String message) { - DialogInterface.OnClickListener dialogListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_NEGATIVE: - undoChanges(); - break; - case DialogInterface.BUTTON_POSITIVE: - updateAccountOrContinue(); - break; - // DialogInterface.BUTTON_NEUTRAL is intentionally ignored. Just dismiss dialog. - } - } - }; - - AlertDialog dialog = new MaterialAlertDialogBuilder(getActivity()) - .setMessage(message) - .setNeutralButton(R.string.login_error_button, dialogListener) - .setNegativeButton(R.string.signup_epilogue_error_button_negative, dialogListener) - .setPositiveButton(R.string.signup_epilogue_error_button_positive, dialogListener) - .create(); - dialog.show(); - } - - protected void showErrorDialogWithCloseButton(String message) { - AlertDialog dialog = new MaterialAlertDialogBuilder(getActivity()) - .setMessage(message) - .setPositiveButton(R.string.login_error_button, null) - .create(); - dialog.show(); - } - - protected void startCropActivity(Uri uri) { - final Context baseContext = getActivity(); - - if (baseContext != null) { - final Context context = new ContextThemeWrapper(baseContext, R.style.WordPress_NoActionBar); - - UCrop.Options options = new UCrop.Options(); - options.setShowCropGrid(false); - options.setStatusBarColor(ContextExtensionsKt.getColorFromAttribute( - context, android.R.attr.statusBarColor - )); - options.setToolbarColor(ContextExtensionsKt.getColorFromAttribute(context, R.attr.wpColorAppBar)); - options.setToolbarWidgetColor(ContextExtensionsKt.getColorFromAttribute( - context, com.google.android.material.R.attr.colorOnSurface - )); - options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE); - options.setHideBottomControls(true); - - UCrop.of(uri, Uri.fromFile(new File(context.getCacheDir(), "cropped.jpg"))) - .withAspectRatio(1, 1) - .withOptions(options) - .start(context, this); - } - } - - protected void startGravatarUpload(final String filePath) { - if (!TextUtils.isEmpty(filePath)) { - final File file = new File(filePath); - if (file.exists()) { - startProgress(false); - mAvatarService.upload(file, new Email(mAccountStore.getAccount().getEmail()), - Objects.requireNonNull(mAccountStore.getAccessToken()), - new GravatarListener() { - @Override - public void onSuccess(@NonNull Unit response) { - endProgress(); - AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOADED); - mPhotoUrl = WPAvatarUtils.rewriteAvatarUrl(mAccount.getAccount().getAvatarUrl(), - getResources().getDimensionPixelSize(R.dimen.avatar_sz_large)); - loadAvatar(mPhotoUrl, filePath); - mHeaderAvatarAdd.setVisibility(View.GONE); - mIsAvatarAdded = true; - } - - @Override - public void onError(@NonNull ErrorType errorType) { - endProgress(); - showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar)); - Map properties = new HashMap<>(); - properties.put("error_type", errorType); - AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties); - AppLog.e(T.NUX, "Uploading image to Gravatar failed"); - } - }); - } else { - ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT); - } - } else { - ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT); - } - } - - protected void undoChanges() { - mDisplayName = !TextUtils.isEmpty(mAccountStore.getAccount().getDisplayName()) - ? mAccountStore.getAccount().getDisplayName() : getArguments().getString(ARG_DISPLAY_NAME); - mEditTextDisplayName.setText(mDisplayName); - mUsername = !TextUtils.isEmpty(mAccountStore.getAccount().getUserName()) - ? mAccountStore.getAccount().getUserName() : getArguments().getString(ARG_USERNAME); - mEditTextUsername.setText(mUsername); - mInputPassword.getEditText().setText(""); - updateAccountOrContinue(); - } - - protected void updateAccountOrContinue() { - if (changedUsername()) { - startProgressIfNeeded(); - updateUsername(); - } else if (changedDisplayName()) { - startProgressIfNeeded(); - mIsUpdatingDisplayName = true; - updateDisplayName(); - } else if (changedPassword() && !mHasUpdatedPassword) { - startProgressIfNeeded(); - mIsUpdatingPassword = true; - updatePassword(); - } else if (mSignupEpilogueListener != null) { - if (!mHasMadeUpdates) { - AnalyticsTracker.track(mIsEmailSignup - ? AnalyticsTracker.Stat.SIGNUP_EMAIL_EPILOGUE_UNCHANGED - : AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_UNCHANGED); - } - endProgressIfNeeded(); - mSignupEpilogueListener.onContinue(); - } - } - - private void updateDisplayName() { - PushAccountSettingsPayload payload = new PushAccountSettingsPayload(); - payload.params = new HashMap<>(); - payload.params.put("display_name", mDisplayName); - mDispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload)); - } - - private void updatePassword() { - PushAccountSettingsPayload payload = new PushAccountSettingsPayload(); - payload.params = new HashMap<>(); - payload.params.put("password", mInputPassword.getEditText().getText().toString()); - mDispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload)); - } - - private void updateUsername() { - PushUsernamePayload payload = new PushUsernamePayload( - mUsername, AccountUsernameActionType.KEEP_OLD_SITE_AND_ADDRESS); - mDispatcher.dispatch(AccountActionBuilder.newPushUsernameAction(payload)); - } - - private class DownloadAvatarAndUploadGravatarThread extends Thread { - private String mEmail; - private String mToken; - private String mUrl; - - DownloadAvatarAndUploadGravatarThread(String url, String email, String token) { - mUrl = url; - mEmail = email; - mToken = token; - } - - @Override - public void run() { - try { - Uri uri = MediaUtils.downloadExternalMedia(getContext(), Uri.parse(mUrl)); - File file = new File(new URI(uri.toString())); - mAvatarService.upload(file, new Email(mEmail), mToken, - new GravatarListener() { - @Override - public void onSuccess(@NonNull Unit response) { - AppLog.i(T.NUX, "Google avatar download and Gravatar upload succeeded."); - AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOADED); - } - - @Override - public void onError(@NonNull ErrorType errorType) { - AppLog.i(T.NUX, "Google avatar download and Gravatar upload failed."); - Map properties = new HashMap<>(); - properties.put("error_type", errorType); - AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties); - } - }); - } catch (NullPointerException | URISyntaxException exception) { - AppLog.e(T.NUX, "Google avatar download and Gravatar upload failed - " - + exception.toString() + " - " + exception.getMessage()); - } - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.kt new file mode 100644 index 000000000000..fbe47aef6cc4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SignupEpilogueFragment.kt @@ -0,0 +1,906 @@ +@file:Suppress("DEPRECATION") + +package org.wordpress.android.ui.accounts.signup + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.Button +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.widget.NestedScrollView +import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.gravatar.services.AvatarService +import com.gravatar.services.Result +import com.gravatar.types.Email +import com.yalantis.ucrop.UCrop +import com.yalantis.ucrop.UCropActivity +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.WordPress.Companion.getBitmapCache +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.action.AccountAction +import org.wordpress.android.fluxc.generated.AccountActionBuilder +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.AccountStore.AccountUsernameActionType +import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged +import org.wordpress.android.fluxc.store.AccountStore.OnUsernameChanged +import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload +import org.wordpress.android.fluxc.store.AccountStore.PushUsernamePayload +import org.wordpress.android.login.LoginBaseFormFragment +import org.wordpress.android.login.widgets.WPLoginInputRow +import org.wordpress.android.ui.FullScreenDialogFragment +import org.wordpress.android.ui.FullScreenDialogFragment.OnShownListener +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.accounts.UnifiedLoginTracker +import org.wordpress.android.ui.photopicker.MediaPickerConstants +import org.wordpress.android.ui.photopicker.MediaPickerLauncher +import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask +import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.MediaUtils +import org.wordpress.android.util.StringUtils +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.WPAvatarUtils +import org.wordpress.android.util.WPMediaUtils +import org.wordpress.android.util.extensions.getColorFromAttribute +import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener +import org.wordpress.android.util.image.ImageManager +import org.wordpress.android.util.image.ImageType +import org.wordpress.android.widgets.WPTextView +import java.io.File +import java.net.URI +import java.net.URISyntaxException +import java.util.EnumSet +import java.util.Locale +import javax.inject.Inject + +@Suppress("LargeClass") +class SignupEpilogueFragment : LoginBaseFormFragment(), + FullScreenDialogFragment.OnConfirmListener, FullScreenDialogFragment.OnDismissListener, + OnShownListener { + private lateinit var mEditTextDisplayName: EditText + private lateinit var mEditTextUsername: EditText + private var mDialog: FullScreenDialogFragment? = null + private var mSignupEpilogueListener: SignupEpilogueListener? = null + + private lateinit var mHeaderAvatarAdd: ImageView + private var mDisplayName: String? = null + private lateinit var mEmailAddress: String + private lateinit var mPhotoUrl: String + private var mUsername: String? = null + private lateinit var mInputPassword: WPLoginInputRow + private lateinit var mHeaderAvatar: ImageView + private lateinit var mHeaderDisplayName: WPTextView + private lateinit var mHeaderEmailAddress: WPTextView + private var mBottomShadow: View? = null + private lateinit var mScrollView: NestedScrollView + private var mIsAvatarAdded: Boolean = false + private var mIsEmailSignup: Boolean = false + + private var mIsUpdatingDisplayName = false + private var mIsUpdatingPassword = false + private var mHasUpdatedPassword = false + private var mHasMadeUpdates = false + + @Inject + lateinit var mAccount: AccountStore + + @Inject + lateinit var dispatcher: Dispatcher + + @Inject + lateinit var mImageManager: ImageManager + + @Inject + lateinit var mAppPrefsWrapper: AppPrefsWrapper + + @Inject + lateinit var mUnifiedLoginTracker: UnifiedLoginTracker + + @Inject + lateinit var mSignupUtils: SignupUtils + + @Inject + lateinit var mMediaPickerLauncher: MediaPickerLauncher + + @Inject + lateinit var mAvatarService: AvatarService + + @LayoutRes + override fun getContentLayout(): Int { + return 0 // no content layout; entire view is inflated in createMainView + } + + @LayoutRes + override fun getProgressBarText(): Int { + return R.string.signup_updating_account + } + + override fun setupLabel(label: TextView) { + // no label in this screen + } + + override fun createMainView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): ViewGroup { + return inflater.inflate(R.layout.signup_epilogue, container, false) as ViewGroup + } + + @Suppress("LongMethod") + override fun setupContent(rootView: ViewGroup) { + val headerAvatarLayout = + rootView.findViewById(R.id.login_epilogue_header_avatar_layout) + headerAvatarLayout.isEnabled = mIsEmailSignup + headerAvatarLayout.setOnClickListener { + mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.SELECT_AVATAR) + mMediaPickerLauncher.showGravatarPicker(this@SignupEpilogueFragment) + } + headerAvatarLayout.setOnLongClickListener { + ToastUtils.showToast( + activity, getString(R.string.content_description_add_avatar), + ToastUtils.Duration.SHORT + ) + true + } + headerAvatarLayout.redirectContextClickToLongPressListener() + mHeaderAvatarAdd = rootView.findViewById(R.id.login_epilogue_header_avatar_add) + mHeaderAvatarAdd.setVisibility(if (mIsEmailSignup) View.VISIBLE else View.GONE) + mHeaderAvatar = rootView.findViewById(R.id.login_epilogue_header_avatar) + mHeaderDisplayName = rootView.findViewById(R.id.login_epilogue_header_title) + mHeaderDisplayName.text = mDisplayName + mHeaderEmailAddress = rootView.findViewById(R.id.login_epilogue_header_subtitle) + mHeaderEmailAddress.text = mEmailAddress + val inputDisplayName = + rootView.findViewById(R.id.signup_epilogue_input_display) + mEditTextDisplayName = inputDisplayName.editText + mEditTextDisplayName.setText(mDisplayName) + mEditTextDisplayName.addTextChangedListener(object : TextWatcher { + @Suppress("EmptyFunctionBlock") + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + @Suppress("EmptyFunctionBlock") + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + } + + override fun afterTextChanged(s: Editable) { + mDisplayName = s.toString() + mHeaderDisplayName.text = mDisplayName + } + }) + val inputUsername = + rootView.findViewById(R.id.signup_epilogue_input_username) + mEditTextUsername = inputUsername.editText + mEditTextUsername.setText(mUsername) + mEditTextUsername.setOnClickListener { + mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.EDIT_USERNAME) + launchDialog() + } + mEditTextUsername.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + launchDialog() + } + } + mEditTextUsername.setOnKeyListener { _, keyCode, _ -> + // Consume keyboard events except for Enter (i.e. click/tap) and Tab (i.e. focus/navigation). + // The onKey method returns true if the listener has consumed the event and false otherwise + // allowing hardware keyboard users to tap and navigate, but not input text as expected. + // This allows the username changer to launch using the keyboard. + !(keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_TAB) + } + mInputPassword = rootView.findViewById(R.id.signup_epilogue_input_password) + mInputPassword.visibility = if (mIsEmailSignup) View.VISIBLE else View.GONE + val passwordDetail = + rootView.findViewById(R.id.signup_epilogue_input_password_detail) + passwordDetail.visibility = if (mIsEmailSignup) View.VISIBLE else View.GONE + + // Set focus on static text field to avoid showing keyboard on start. + mHeaderEmailAddress.requestFocus() + + mBottomShadow = rootView.findViewById(R.id.bottom_shadow) + mScrollView = rootView.findViewById(R.id.scroll_view) + mScrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, _: Int, _: Int, _: Int -> + showBottomShadowIfNeeded() + } + // We must use onGlobalLayout here otherwise canScrollVertically will always return false + mScrollView.getViewTreeObserver() + .addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this) + showBottomShadowIfNeeded() + } + }) + } + + private fun showBottomShadowIfNeeded() { + val canScrollDown = mScrollView.canScrollVertically(1) + if (mBottomShadow != null) { + mBottomShadow!!.visibility = + if (canScrollDown) View.VISIBLE else View.GONE + } + } + + override fun setupBottomButton(button: Button) { + button.setOnClickListener { + mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.CONTINUE) + updateAccountOrContinue() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + dispatcher.dispatch(AccountActionBuilder.newFetchAccountAction()) + + mDisplayName = requireArguments().getString(ARG_DISPLAY_NAME) + mEmailAddress = StringUtils.notNullStr(requireArguments().getString(ARG_EMAIL_ADDRESS)) + mPhotoUrl = StringUtils.notNullStr(requireArguments().getString(ARG_PHOTO_URL)) + mUsername = requireArguments().getString(ARG_USERNAME) + mIsEmailSignup = requireArguments().getBoolean(ARG_IS_EMAIL_SIGNUP) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + if (savedInstanceState == null) { + // Start loading reader tags so they will be available asap + ReaderUpdateServiceStarter.startService( + WordPress.getContext(), + EnumSet.of(UpdateTask.TAGS) + ) + + mUnifiedLoginTracker.track(step = UnifiedLoginTracker.Step.SUCCESS) + if (mIsEmailSignup) { + AnalyticsTracker.track(Stat.SIGNUP_EMAIL_EPILOGUE_VIEWED) + + // Start progress and wait for account to be fetched before populating views when + // email does not exist in account store. + if (TextUtils.isEmpty(mAccountStore.account.email)) { + startProgress(false) + } else { + // Skip progress and populate views when email does exist in account store. + populateViews() + } + } else { + AnalyticsTracker.track(Stat.SIGNUP_SOCIAL_EPILOGUE_VIEWED) + mAccount.accessToken?.let { accessToken -> + DownloadAvatarAndUploadGravatarThread( + mPhotoUrl, + mEmailAddress, + accessToken + ).start() + mImageManager.loadIntoCircle( + mHeaderAvatar, + ImageType.AVATAR_WITHOUT_BACKGROUND, + (mPhotoUrl) + ) + } + } + } else { + mDialog = parentFragmentManager + .findFragmentByTag(FullScreenDialogFragment.TAG) as FullScreenDialogFragment? + + if (mDialog != null) { + mDialog!!.setOnConfirmListener(this) + mDialog!!.setOnDismissListener(this) + } + + mDisplayName = savedInstanceState.getString(KEY_DISPLAY_NAME) + mUsername = savedInstanceState.getString(KEY_USERNAME) + mIsAvatarAdded = savedInstanceState.getBoolean(KEY_IS_AVATAR_ADDED) + + if (mIsEmailSignup) { + mPhotoUrl = StringUtils.notNullStr(savedInstanceState.getString(KEY_PHOTO_URL)) + mEmailAddress = StringUtils.notNullStr(savedInstanceState.getString(KEY_EMAIL_ADDRESS)) + mHeaderEmailAddress.text = mEmailAddress + mHeaderAvatarAdd.visibility = if (mIsAvatarAdded) View.GONE else View.VISIBLE + } + mImageManager.loadIntoCircle( + mHeaderAvatar, + ImageType.AVATAR_WITHOUT_BACKGROUND, + mPhotoUrl + ) + + mIsUpdatingDisplayName = savedInstanceState.getBoolean(KEY_IS_UPDATING_DISPLAY_NAME) + mIsUpdatingPassword = savedInstanceState.getBoolean(KEY_IS_UPDATING_PASSWORD) + mHasUpdatedPassword = savedInstanceState.getBoolean(KEY_HAS_UPDATED_PASSWORD) + mHasMadeUpdates = savedInstanceState.getBoolean(KEY_HAS_MADE_UPDATES) + } + } + + @Suppress("deprecation", "NestedBlockDepth", "LongMethod") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (isAdded) { + when (resultCode) { + Activity.RESULT_OK -> when (requestCode) { + RequestCodes.PHOTO_PICKER -> if (data != null) { + val mediaUriStringsArray = + data.getStringArrayExtra(MediaPickerConstants.EXTRA_MEDIA_URIS) + + if (mediaUriStringsArray != null && mediaUriStringsArray.size > 0) { + val source = + PhotoPickerMediaSource.fromString( + data.getStringExtra(MediaPickerConstants.EXTRA_MEDIA_SOURCE) + ) + val stat = + if (source == PhotoPickerMediaSource.ANDROID_CAMERA) { + Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_SHOT_NEW + } else { + Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_GALLERY_PICKED + } + AnalyticsTracker.track(stat) + val imageUri = Uri.parse(mediaUriStringsArray[0]) + + if (imageUri != null) { + val wasSuccess = WPMediaUtils.fetchMediaAndDoNext( + activity, imageUri + ) { uri -> startCropActivity(uri) } + + if (!wasSuccess) { + AppLog.e( + AppLog.T.UTILS, + "Can't download picked or captured image" + ) + } + } else { + AppLog.e(AppLog.T.UTILS, "Can't parse media string") + } + } else { + AppLog.e(AppLog.T.UTILS, "Can't resolve picked or captured image") + } + } + + UCrop.REQUEST_CROP -> { + AnalyticsTracker.track(Stat.SIGNUP_EMAIL_EPILOGUE_GRAVATAR_CROPPED) + WPMediaUtils.fetchMediaAndDoNext( + activity, UCrop.getOutput((data)!!) + ) { uri -> + startGravatarUpload( + MediaUtils.getRealPathFromURI( + activity, uri + ) + ) + } + } + } + + UCrop.RESULT_ERROR -> { + AppLog.e( + AppLog.T.NUX, "Image cropping failed", UCrop.getError( + (data)!! + ) + ) + ToastUtils.showToast( + activity, + R.string.error_cropping_image, + ToastUtils.Duration.SHORT + ) + } + } + } + } + + @Suppress("TooGenericExceptionThrown") + override fun onAttach(context: Context) { + super.onAttach(context) + + if (context is SignupEpilogueListener) { + mSignupEpilogueListener = context + } else { + throw RuntimeException("$context must implement SignupEpilogueListener") + } + } + + override fun onConfirm(result: Bundle?) { + if (result != null) { + mUsername = result.getString(BaseUsernameChangerFullScreenDialogFragment.RESULT_USERNAME) + mEditTextUsername.setText(mUsername) + } + } + + override fun onDismiss() { + val props: MutableMap = HashMap() + props[SOURCE] = SOURCE_SIGNUP_EPILOGUE + AnalyticsTracker.track(Stat.CHANGE_USERNAME_DISMISSED, props) + mDialog = null + } + + override fun onShown() { + val props: MutableMap = HashMap() + props[SOURCE] = SOURCE_SIGNUP_EPILOGUE + AnalyticsTracker.track(Stat.CHANGE_USERNAME_DISPLAYED, props) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(KEY_PHOTO_URL, mPhotoUrl) + outState.putString(KEY_DISPLAY_NAME, mDisplayName) + outState.putString(KEY_EMAIL_ADDRESS, mEmailAddress) + outState.putString(KEY_USERNAME, mUsername) + outState.putBoolean(KEY_IS_AVATAR_ADDED, mIsAvatarAdded) + outState.putBoolean(KEY_IS_UPDATING_DISPLAY_NAME, mIsUpdatingDisplayName) + outState.putBoolean(KEY_IS_UPDATING_PASSWORD, mIsUpdatingPassword) + outState.putBoolean(KEY_HAS_UPDATED_PASSWORD, mHasUpdatedPassword) + outState.putBoolean(KEY_HAS_MADE_UPDATES, mHasMadeUpdates) + } + + @Suppress("EmptyFunctionBlock") + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + @Suppress("EmptyFunctionBlock") + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + } + + @Suppress("EmptyFunctionBlock") + override fun afterTextChanged(s: Editable) { + } + + @Suppress("EmptyFunctionBlock") + override fun onHelp() { + } + + override fun onLoginFinished() { + endProgress() + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + override fun onAccountChanged(event: OnAccountChanged) { + if (event.isError) { + if (mIsUpdatingDisplayName) { + mIsUpdatingDisplayName = false + AnalyticsTracker.track( + if (mIsEmailSignup + ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED + else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_FAILED + ) + } else if (mIsUpdatingPassword) { + mIsUpdatingPassword = false + } + + AppLog.e( + AppLog.T.API, ("SignupEpilogueFragment.onAccountChanged: " + + event.error.type + " - " + event.error.message) + ) + endProgress() + + if (isPasswordInErrorMessage(event.error.message)) { + showErrorDialogWithCloseButton(event.error.message) + } else { + showErrorDialog(getString(R.string.signup_epilogue_error_generic)) + } + // Wait to populate epilogue for email interface until account is fetched and email address + // is available since flow is coming from magic link with no instance argument values. + } else if (mIsEmailSignup && (event.causeOfChange == AccountAction.FETCH_ACCOUNT + ) && !TextUtils.isEmpty(mAccountStore.account.email) + ) { + endProgress() + populateViews() + } else if (event.causeOfChange == AccountAction.PUSH_SETTINGS) { + mHasMadeUpdates = true + + if (mIsUpdatingDisplayName) { + mIsUpdatingDisplayName = false + AnalyticsTracker.track( + if (mIsEmailSignup + ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED + else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_DISPLAY_NAME_SUCCEEDED + ) + } else if (mIsUpdatingPassword) { + mIsUpdatingPassword = false + mHasUpdatedPassword = true + } + + updateAccountOrContinue() + } + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onUsernameChanged(event: OnUsernameChanged) { + if (event.isError) { + AnalyticsTracker.track( + if (mIsEmailSignup + ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_FAILED + else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_FAILED + ) + AppLog.e( + AppLog.T.API, ("SignupEpilogueFragment.onUsernameChanged: " + + event.error.type + " - " + event.error.message) + ) + endProgress() + showErrorDialog(getString(R.string.signup_epilogue_error_generic)) + } else { + mHasMadeUpdates = true + AnalyticsTracker.track( + if (mIsEmailSignup + ) Stat.SIGNUP_EMAIL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED + else Stat.SIGNUP_SOCIAL_EPILOGUE_UPDATE_USERNAME_SUCCEEDED + ) + updateAccountOrContinue() + } + } + + private fun changedDisplayName(): Boolean { + return !TextUtils.equals(mAccount.account.displayName, mDisplayName) + } + + private fun changedPassword(): Boolean { + return !TextUtils.isEmpty(mInputPassword.editText.text.toString()) + } + + private fun changedUsername(): Boolean { + return !TextUtils.equals(mAccount.account.userName, mUsername) + } + + private fun isPasswordInErrorMessage(message: String): Boolean { + val lowercaseMessage = message.lowercase(Locale.getDefault()) + val lowercasePassword = getString(R.string.password).lowercase(Locale.getDefault()) + return lowercaseMessage.contains(lowercasePassword) + } + + private fun launchDialog() { + AnalyticsTracker.track( + if (mIsEmailSignup + ) Stat.SIGNUP_EMAIL_EPILOGUE_USERNAME_TAPPED + else Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_TAPPED + ) + + val bundle: Bundle = BaseUsernameChangerFullScreenDialogFragment.newBundle( + mEditTextDisplayName.text.toString(), mEditTextUsername.text.toString() + ) + + mDialog = FullScreenDialogFragment.Builder(requireContext()) + .setTitle(R.string.username_changer_title) + .setAction(R.string.username_changer_action) + .setToolbarTheme(org.wordpress.android.login.R.style.ThemeOverlay_LoginFlow_Toolbar) + .setOnConfirmListener(this) + .setOnDismissListener(this) + .setOnShownListener(this) + .setContent(UsernameChangerFullScreenDialogFragment::class.java, bundle) + .build() + + mDialog?.show(requireActivity().supportFragmentManager, FullScreenDialogFragment.TAG) + } + + private fun loadAvatar(avatarUrl: String, injectFilePath: String) { + val newAvatarUploaded = injectFilePath.isNotEmpty() + if (newAvatarUploaded) { + // Remove specific URL entry from bitmap cache. Update it via injected request cache. + getBitmapCache().removeSimilar(avatarUrl) + // Changing the signature invalidates Glide's cache + mAppPrefsWrapper.avatarVersion += 1 + } + + val bitmap = getBitmapCache()[avatarUrl] + // Avatar's API doesn't synchronously update the image at avatarUrl. There is a replication lag + // (cca 5s), before the old avatar is replaced with the new avatar. Therefore we need to use this workaround, + // which temporary saves the new image into a local bitmap cache. + if (bitmap != null) { + mImageManager.load((mHeaderAvatar), bitmap) + } else { + mImageManager.loadIntoCircle( + mHeaderAvatar, + ImageType.AVATAR_WITHOUT_BACKGROUND, + if (newAvatarUploaded) { + injectFilePath + } else { + avatarUrl + }, + object : ImageManager.RequestListener { + override fun onLoadFailed(e: Exception?, model: Any?) { + AppLog.e( + AppLog.T.NUX, + "Uploading image to Gravatar succeeded, but setting image view failed" + ) + showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar_view)) + } + + @Suppress("NAME_SHADOWING") + override fun onResourceReady(resource: Drawable, model: Any?) { + if (newAvatarUploaded && resource is BitmapDrawable) { + var bitmap = resource.bitmap + // create a copy since the original bitmap may by automatically recycled + bitmap = bitmap.copy(bitmap.config, true) + getBitmapCache().put((avatarUrl), bitmap) + } + } + }, + mAppPrefsWrapper.avatarVersion + ) + } + } + + private fun populateViews() { + mEmailAddress = mAccountStore.account.email + mDisplayName = mSignupUtils.createDisplayNameFromEmail(mEmailAddress) + mUsername = if (!TextUtils.isEmpty(mAccountStore.account.userName) + ) mAccountStore.account.userName else mSignupUtils.createUsernameFromEmail(mEmailAddress) + mHeaderDisplayName.text = mDisplayName + mHeaderEmailAddress.text = mEmailAddress + mEditTextDisplayName.setText(mDisplayName) + mEditTextUsername.setText(mUsername) + // Set fragment arguments to know if account should be updated when values change. + val args = Bundle() + args.putString(ARG_DISPLAY_NAME, mDisplayName) + args.putString(ARG_EMAIL_ADDRESS, mEmailAddress) + args.putString(ARG_PHOTO_URL, mPhotoUrl) + args.putString(ARG_USERNAME, mUsername) + args.putBoolean(ARG_IS_EMAIL_SIGNUP, mIsEmailSignup) + arguments = args + } + + private fun showErrorDialog(message: String?) { + val dialogListener: DialogInterface.OnClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_NEGATIVE -> undoChanges() + DialogInterface.BUTTON_POSITIVE -> updateAccountOrContinue() + } + } + + val dialog = MaterialAlertDialogBuilder(requireActivity()) + .setMessage(message) + .setNeutralButton(R.string.login_error_button, dialogListener) + .setNegativeButton(R.string.signup_epilogue_error_button_negative, dialogListener) + .setPositiveButton(R.string.signup_epilogue_error_button_positive, dialogListener) + .create() + dialog.show() + } + + private fun showErrorDialogWithCloseButton(message: String?) { + val dialog = MaterialAlertDialogBuilder(requireActivity()) + .setMessage(message) + .setPositiveButton(R.string.login_error_button, null) + .create() + dialog.show() + } + + private fun startCropActivity(uri: Uri?) { + val baseContext: Context? = activity + + if (baseContext != null) { + val context: Context = ContextThemeWrapper(baseContext, R.style.WordPress_NoActionBar) + + val options = UCrop.Options() + options.setShowCropGrid(false) + options.setStatusBarColor( + context.getColorFromAttribute( + android.R.attr.statusBarColor + ) + ) + options.setToolbarColor(context.getColorFromAttribute(R.attr.wpColorAppBar)) + options.setToolbarWidgetColor( + context.getColorFromAttribute( + com.google.android.material.R.attr.colorOnSurface + ) + ) + options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE) + options.setHideBottomControls(true) + + UCrop.of((uri)!!, Uri.fromFile(File(context.cacheDir, "cropped.jpg"))) + .withAspectRatio(1f, 1f) + .withOptions(options) + .start(context, this) + } + } + + private fun startGravatarUpload(filePath: String) { + if (!TextUtils.isEmpty(filePath)) { + val file = File(filePath) + if (file.exists()) { + mAccountStore.accessToken?.let { accessToken -> + startProgress(false) + lifecycleScope.launch { + val result = mAvatarService.upload( + file, Email(mAccountStore.account.email), + accessToken + ) + when (result) { + is Result.Success -> { + endProgress() + AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOADED) + mPhotoUrl = WPAvatarUtils.rewriteAvatarUrl( + mAccount.account.avatarUrl, + resources.getDimensionPixelSize(R.dimen.avatar_sz_large) + ) + loadAvatar(mPhotoUrl, filePath) + mHeaderAvatarAdd.visibility = View.GONE + mIsAvatarAdded = true + } + + is Result.Failure -> { + endProgress() + showErrorDialogWithCloseButton(getString(R.string.signup_epilogue_error_avatar)) + val properties: MutableMap = HashMap() + properties["error_type"] = result.error + AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties) + AppLog.e(AppLog.T.NUX, "Uploading image to Gravatar failed") + } + } + } + } + } else { + ToastUtils.showToast( + activity, + R.string.error_locating_image, + ToastUtils.Duration.SHORT + ) + } + } else { + ToastUtils.showToast(activity, R.string.error_locating_image, ToastUtils.Duration.SHORT) + } + } + + private fun undoChanges() { + mDisplayName = if (!TextUtils.isEmpty(mAccountStore.account.displayName) + ) mAccountStore.account.displayName else requireArguments().getString(ARG_DISPLAY_NAME) + mEditTextDisplayName.setText(mDisplayName) + mUsername = if (!TextUtils.isEmpty(mAccountStore.account.userName) + ) mAccountStore.account.userName else requireArguments().getString(ARG_USERNAME) + mEditTextUsername.setText(mUsername) + mInputPassword.editText.setText("") + updateAccountOrContinue() + } + + private fun updateAccountOrContinue() { + if (changedUsername()) { + startProgressIfNeeded() + updateUsername() + } else if (changedDisplayName()) { + startProgressIfNeeded() + mIsUpdatingDisplayName = true + updateDisplayName() + } else if (changedPassword() && !mHasUpdatedPassword) { + startProgressIfNeeded() + mIsUpdatingPassword = true + updatePassword() + } else if (mSignupEpilogueListener != null) { + if (!mHasMadeUpdates) { + AnalyticsTracker.track( + if (mIsEmailSignup + ) Stat.SIGNUP_EMAIL_EPILOGUE_UNCHANGED + else Stat.SIGNUP_SOCIAL_EPILOGUE_UNCHANGED + ) + } + endProgressIfNeeded() + mSignupEpilogueListener!!.onContinue() + } + } + + private fun updateDisplayName() { + val payload = PushAccountSettingsPayload() + payload.params = HashMap() + payload.params["display_name"] = mDisplayName + dispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload)) + } + + private fun updatePassword() { + val payload = PushAccountSettingsPayload() + payload.params = HashMap() + payload.params["password"] = mInputPassword.editText.text.toString() + dispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload)) + } + + private fun updateUsername() { + mUsername?.let { + val payload = PushUsernamePayload(it, AccountUsernameActionType.KEEP_OLD_SITE_AND_ADDRESS) + dispatcher.dispatch(AccountActionBuilder.newPushUsernameAction(payload)) + } + } + + private inner class DownloadAvatarAndUploadGravatarThread( + private val mUrl: String, + private val mEmail: String, + private val mToken: String + ) : Thread() { + override fun run() { + @Suppress("TooGenericExceptionCaught") + try { + val uri = MediaUtils.downloadExternalMedia(context, Uri.parse(mUrl)) + val file = File(URI(uri.toString())) + lifecycleScope.launch { + when (val result = mAvatarService.upload(file, Email(mEmail), mToken)) { + is Result.Success -> { + AppLog.i( + AppLog.T.NUX, + "Google avatar download and Gravatar upload succeeded." + ) + AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOADED) + } + + is Result.Failure -> { + AppLog.i( + AppLog.T.NUX, + "Google avatar download and Gravatar upload failed." + ) + val properties: MutableMap = HashMap() + properties["error_type"] = result.error + AnalyticsTracker.track(Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties) + } + } + } + } catch (exception: NullPointerException) { + AppLog.e( + AppLog.T.NUX, ("Google avatar download and Gravatar upload failed - " + + exception.toString() + " - " + exception.message) + ) + } catch (exception: URISyntaxException) { + AppLog.e( + AppLog.T.NUX, ("Google avatar download and Gravatar upload failed - " + + exception.toString() + " - " + exception.message) + ) + } + } + } + + companion object { + private const val ARG_DISPLAY_NAME = "ARG_DISPLAY_NAME" + private const val ARG_EMAIL_ADDRESS = "ARG_EMAIL_ADDRESS" + private const val ARG_IS_EMAIL_SIGNUP = "ARG_IS_EMAIL_SIGNUP" + private const val ARG_PHOTO_URL = "ARG_PHOTO_URL" + private const val ARG_USERNAME = "ARG_USERNAME" + private const val KEY_DISPLAY_NAME = "KEY_DISPLAY_NAME" + private const val KEY_EMAIL_ADDRESS = "KEY_EMAIL_ADDRESS" + private const val KEY_IS_AVATAR_ADDED = "KEY_IS_AVATAR_ADDED" + private const val KEY_PHOTO_URL = "KEY_PHOTO_URL" + private const val KEY_USERNAME = "KEY_USERNAME" + private const val KEY_IS_UPDATING_DISPLAY_NAME = "KEY_IS_UPDATING_DISPLAY_NAME" + private const val KEY_IS_UPDATING_PASSWORD = "KEY_IS_UPDATING_PASSWORD" + private const val KEY_HAS_UPDATED_PASSWORD = "KEY_HAS_UPDATED_PASSWORD" + private const val KEY_HAS_MADE_UPDATES = "KEY_HAS_MADE_UPDATES" + + private const val SOURCE = "source" + private const val SOURCE_SIGNUP_EPILOGUE = "signup_epilogue" + + const val TAG: String = "signup_epilogue_fragment_tag" + + fun newInstance( + displayName: String?, emailAddress: String?, + photoUrl: String?, username: String?, + isEmailSignup: Boolean + ): SignupEpilogueFragment { + val signupEpilogueFragment = SignupEpilogueFragment() + val args = Bundle() + args.putString(ARG_DISPLAY_NAME, displayName) + args.putString(ARG_EMAIL_ADDRESS, emailAddress) + args.putString(ARG_PHOTO_URL, photoUrl) + args.putString(ARG_USERNAME, username) + args.putBoolean(ARG_IS_EMAIL_SIGNUP, isEmailSignup) + signupEpilogueFragment.arguments = args + return signupEpilogueFragment + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt index 2129fb2e9d8e..8375e18630f7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt @@ -18,16 +18,17 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.gravatar.services.AvatarService -import com.gravatar.services.ErrorType -import com.gravatar.services.GravatarListener +import com.gravatar.services.Result import com.gravatar.types.Email import com.yalantis.ucrop.UCrop import com.yalantis.ucrop.UCrop.Options import com.yalantis.ucrop.UCropActivity import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -679,18 +680,21 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { return } binding?.showGravatarProgressBar(true) - avatarService.upload(file, Email(accountStore.account.email), accountStore.accessToken.orEmpty(), - object : GravatarListener { - override fun onSuccess(response: Unit) { - AnalyticsTracker.track(ME_GRAVATAR_UPLOADED) - EventBus.getDefault().post(GravatarUploadFinished(filePath, true)) + lifecycleScope.launch { + val result = + avatarService.upload(file, Email(accountStore.account.email), accountStore.accessToken.orEmpty()) + when (result) { + is Result.Failure -> { + AnalyticsTracker.track(ME_GRAVATAR_UPLOAD_EXCEPTION, mapOf("error_type" to result.error.name)) + EventBus.getDefault().post(GravatarUploadFinished(filePath, false)) } - override fun onError(errorType: ErrorType) { - AnalyticsTracker.track(ME_GRAVATAR_UPLOAD_EXCEPTION, mapOf("error_type" to errorType.name)) - EventBus.getDefault().post(GravatarUploadFinished(filePath, false)) + is Result.Success -> { + AnalyticsTracker.track(ME_GRAVATAR_UPLOADED) + EventBus.getDefault().post(GravatarUploadFinished(filePath, true)) } - }) + } + } } class GravatarUploadFinished internal constructor(val filePath: String, val success: Boolean) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 33ecef3a1107..9e074da25dec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -752,14 +752,17 @@ private void initViewModel() { .show(getSupportFragmentManager(), FeatureAnnouncementDialogFragment.TAG); }); - mFloatingActionButton.setOnClickListener(v -> mViewModel.onFabClicked(getSelectedSite())); + mFloatingActionButton.setOnClickListener(v -> { + PageType selectedPage = getSelectedPage(); + if (selectedPage != null) mViewModel.onFabClicked(getSelectedSite(), selectedPage); + }); mFloatingActionButton.setOnLongClickListener(v -> { if (v.isHapticFeedbackEnabled()) { v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } - int messageId = mViewModel.getCreateContentMessageId(getSelectedSite()); + int messageId = mViewModel.getCreateContentMessageId(getSelectedSite(), getSelectedPage()); Toast.makeText(v.getContext(), messageId, Toast.LENGTH_SHORT).show(); return true; @@ -831,7 +834,7 @@ private void initViewModel() { // initialized with the most restrictive rights case. This is OK and will be frequently checked // to normalize the UI state whenever mSelectedSite changes. // It also means that the ViewModel must accept a nullable SiteModel. - mViewModel.start(getSelectedSite()); + mViewModel.start(getSelectedSite(), mBottomNav.getCurrentSelectedPage()); } private void triggerCreatePageFlow(ActionType actionType) { @@ -1177,8 +1180,8 @@ protected void onResume() { mViewModel.onResume( getSelectedSite(), - mSelectedSiteRepository.hasSelectedSite() && mBottomNav != null - && mBottomNav.getCurrentSelectedPage() == PageType.MY_SITE + mSelectedSiteRepository.hasSelectedSite(), + getSelectedPage() ); if (AppReviewManager.INSTANCE.shouldShowInAppReviewsPrompt()) { @@ -1267,8 +1270,9 @@ public void onPageChanged(int position) { } mViewModel.onPageChanged( - mSiteStore.hasSite() && pageType == PageType.MY_SITE, - getSelectedSite() + getSelectedSite(), + mSiteStore.hasSite(), + pageType ); } @@ -1987,4 +1991,9 @@ private void showOpenPageMessageIfNeeded() { } } } + + @Nullable + private PageType getSelectedPage() { + return mBottomNav != null ? mBottomNav.getCurrentSelectedPage() : null; + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt new file mode 100644 index 000000000000..05d6e57d6c4c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTracker.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.ui.main.analytics + +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.main.MainActionListItem +import org.wordpress.android.ui.main.WPMainNavigationView +import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import java.util.Locale +import javax.inject.Inject + +class MainCreateSheetTracker @Inject constructor( + private val analyticsTracker: AnalyticsTrackerWrapper, +) { + fun trackActionTapped(page: WPMainNavigationView.PageType, actionType: MainActionListItem.ActionType) { + val stat = when (page) { + WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED + WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_ACTION_TAPPED + else -> return + } + val properties = mapOf("action" to actionType.name.lowercase(Locale.ROOT)) + analyticsTracker.track(stat, properties) + } + + fun trackAnswerPromptActionTapped(page: WPMainNavigationView.PageType, attribution: BloggingPromptAttribution) { + val properties = mapOf("attribution" to attribution.value).filterValues { it.isNotBlank() } + val stat = when (page) { + WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED + WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED + else -> return + } + analyticsTracker.track(stat, properties) + } + + fun trackHelpPromptActionTapped(page: WPMainNavigationView.PageType) { + val stat = when (page) { + WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED + WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_PROMPT_HELP_TAPPED + else -> return + } + analyticsTracker.track(stat) + } + + fun trackSheetShown(page: WPMainNavigationView.PageType) { + val stat = when (page) { + WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_SHOWN + WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_SHEET_SHOWN + else -> return + } + analyticsTracker.track(stat) + } + + fun trackFabShown(page: WPMainNavigationView.PageType) { + val stat = when (page) { + WPMainNavigationView.PageType.MY_SITE -> AnalyticsTracker.Stat.MY_SITE_CREATE_FAB_SHOWN + WPMainNavigationView.PageType.READER -> AnalyticsTracker.Stat.READER_CREATE_FAB_SHOWN + else -> return + } + analyticsTracker.track(stat) + } + + fun trackCreateActionsSheetCard(actions: List) { + if (actions.any { it is MainActionListItem.AnswerBloggingPromptAction }) { + analyticsTracker.track(AnalyticsTracker.Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt new file mode 100644 index 000000000000..7255a908245a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelper.kt @@ -0,0 +1,37 @@ +package org.wordpress.android.ui.main.utils + +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.main.WPMainNavigationView.PageType +import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils +import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.SiteUtilsWrapper +import org.wordpress.android.util.config.ReaderFloatingButtonFeatureConfig +import javax.inject.Inject + +class MainCreateSheetHelper @Inject constructor( + private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils, + private val readerFloatingButtonFeatureConfig: ReaderFloatingButtonFeatureConfig, + private val bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper, + private val buildConfig: BuildConfigWrapper, + private val siteUtils: SiteUtilsWrapper, +) { + fun shouldShowFabForPage(page: PageType?): Boolean { + val enabledForPage = page == PageType.MY_SITE || + (page == PageType.READER && readerFloatingButtonFeatureConfig.isEnabled()) + return buildConfig.isCreateFabEnabled && enabledForPage + } + + @Suppress("FunctionOnlyReturningConstant") + fun canCreatePost(): Boolean = true // for completeness + + fun canCreatePage(site: SiteModel?, page: PageType?): Boolean { + return siteUtils.hasFullAccessToContent(site) && page == PageType.MY_SITE + } + + fun canCreatePostFromAudio(site: SiteModel?): Boolean { + return voiceToContentFeatureUtils.isVoiceToContentEnabled() && siteUtils.hasFullAccessToContent(site) + } + + suspend fun canCreatePromptAnswer(): Boolean = bloggingPromptsSettingsHelper.shouldShowPromptsFeature() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt index f976bc882eca..54a41e988294 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt @@ -686,6 +686,9 @@ class MediaPickerFragment : Fragment(), MenuProvider { // devices lower than API 33. permissions.add(permission.READ_EXTERNAL_STORAGE) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + permissions.add(permission.ACCESS_MEDIA_LOCATION) + } requestPermissions(permissions.toTypedArray(), WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardViewModelSlice.kt index 37b39f4e2479..7e303986d344 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardViewModelSlice.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardViewModelSlice.kt @@ -153,6 +153,7 @@ class CardViewModelSlice @Inject constructor( identifier = buildConfigWrapper.getApplicationId(), marketingVersion = buildConfigWrapper.getAppVersionName(), platform = FEATURE_FLAG_PLATFORM_PARAMETER, + osVersion = buildConfigWrapper.androidVersion ) val result = cardsStore.fetchCards(payload) val error = result.error diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt index 37400630c2df..0bc6aa5e16ed 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt @@ -476,6 +476,9 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { // devices lower than API 33. permissions.add(permission.READ_EXTERNAL_STORAGE) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + permissions.add(permission.ACCESS_MEDIA_LOCATION) + } requestPermissions(permissions.toTypedArray(), WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index d2659bf9c17f..2239cc19d4ad 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -3813,7 +3813,12 @@ class EditPostActivity : LocaleAwareActivity(), EditorFragmentActivity, EditorIm @Subscribe(threadMode = ThreadMode.MAIN) fun onPostUploaded(event: OnPostUploaded) { val post: PostModel? = event.post - if (post != null && post.id == editPostRepository.id) { + + // Check if editPostRepository is initialized + val editPostRepositoryInitialized = this::editPostRepository.isInitialized + val editPostId = if (editPostRepositoryInitialized) editPostRepository.getPost()?.id else null + + if (post != null && post.id == editPostId) { if (!isRemotePreviewingFromEditor) { // We are not remote previewing a post: show snackbar and update post status if needed val snackbarAttachView = findViewById(R.id.editor_activity) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt index 16d7e3067a53..85e775d67c66 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt @@ -174,8 +174,8 @@ class PostListEventListener( @Suppress("unused", "SpreadOperator") @Subscribe(threadMode = BACKGROUND) fun onMediaChanged(event: OnMediaChanged) { + featuredMediaChanged(*event.mediaList.map { it.mediaId }.toLongArray()) if (!event.isError) { - featuredMediaChanged(*event.mediaList.map { it.mediaId }.toLongArray()) uploadStatusChanged(*event.mediaList.map { it.localPostId }.toIntArray()) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFeaturedImageTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFeaturedImageTracker.kt index d94dd6e0a582..32a9a63b2518 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFeaturedImageTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFeaturedImageTracker.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.posts import android.annotation.SuppressLint +import androidx.annotation.VisibleForTesting import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.MediaActionBuilder import org.wordpress.android.fluxc.model.MediaModel @@ -22,13 +23,27 @@ class PostListFeaturedImageTracker(private val dispatcher: Dispatcher, private v https://github.com/wordpress-mobile/WordPress-Android/issues/11487 */ @SuppressLint("UseSparseArrays") - private val featuredImageMap = HashMap() + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val featuredImageMap = HashMap() + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val ongoingRequests = HashSet() fun getFeaturedImageUrl(site: SiteModel, featuredImageId: Long): String? { if (featuredImageId == 0L) { return null } - featuredImageMap[featuredImageId]?.let { return it } + + featuredImageMap[featuredImageId]?.let { + return it + } + + // Check if a request for this image is already ongoing + if (ongoingRequests.contains(featuredImageId)) { + // If the request is ongoing, just return. The callback will be invoked upon completion. + return null + } + mediaStore.getSiteMediaWithId(site, featuredImageId)?.let { media -> // This should be a pretty rare case, but some media seems to be missing url return if (media.url.isNotBlank()) { @@ -36,7 +51,11 @@ class PostListFeaturedImageTracker(private val dispatcher: Dispatcher, private v media.url } else null } + // Media is not in the Store, we need to download it + // Mark the request as ongoing + ongoingRequests.add(featuredImageId) + val mediaToDownload = MediaModel( site.id, featuredImageId @@ -47,6 +66,9 @@ class PostListFeaturedImageTracker(private val dispatcher: Dispatcher, private v } fun invalidateFeaturedMedia(featuredImageIds: List) { - featuredImageIds.forEach { featuredImageMap.remove(it) } + featuredImageIds.forEach { + featuredImageMap.remove(it) + ongoingRequests.remove(it) + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/AddLocalMediaToPostUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/AddLocalMediaToPostUseCase.kt index f1ed81298c70..c92b5a0a25fd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/AddLocalMediaToPostUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/AddLocalMediaToPostUseCase.kt @@ -101,6 +101,7 @@ class AddLocalMediaToPostUseCase @Inject constructor( site.id, optimizeMediaResult.optimizedMediaUris ) + // here we pass a map of "old" (before optimisation) Uris to the new MediaModels which contain // both the mediaModel ids and the optimized media URLs. // this way, the listener will be able to process from other models pointing to the old URLs diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index cea12f681a01..0d8c30f3d307 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -247,6 +247,8 @@ public enum UndeletablePrefKey implements PrefKey { ASKED_PERMISSION_NOTIFICATIONS, + ASKED_PERMISSION_ACCESS_MEDIA_LOCATION, + // Updated after WP.com themes have been fetched LAST_WP_COM_THEMES_SYNC, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index d62a859bfdeb..5702987f5286 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -153,6 +153,7 @@ import javax.inject.Inject; +import static androidx.lifecycle.LifecycleOwnerKt.getLifecycleScope; import static org.wordpress.android.fluxc.generated.AccountActionBuilder.newUpdateSubscriptionNotificationPostAction; import static org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType.INTERNAL; @@ -1923,7 +1924,8 @@ private ReaderPostAdapter getPostAdapter() { mImageManager, mUiHelpers, mNetworkUtilsWrapper, - mIsTopLevel + mIsTopLevel, + getLifecycleScope(this) ); mPostAdapter.setOnFollowListener(this); mPostAdapter.setOnPostSelectedListener(this); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 54b22755cc1a..6dc4ad0d99d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -15,12 +15,13 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleCoroutineScope; import androidx.recyclerview.widget.RecyclerView; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.datasets.AsyncTaskHandler; +import org.wordpress.android.datasets.AsyncTaskExecutor; import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.datasets.ReaderTagTable; import org.wordpress.android.fluxc.store.AccountStore; @@ -82,11 +83,13 @@ import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; import kotlin.jvm.functions.Function3; +import kotlinx.coroutines.CoroutineScope; public class ReaderPostAdapter extends RecyclerView.Adapter { private final ImageManager mImageManager; private final UiHelpers mUiHelpers; private final NetworkUtilsWrapper mNetworkUtilsWrapper; + private final CoroutineScope mScope; private ReaderTag mCurrentTag; private long mCurrentBlogId; private long mCurrentFeedId; @@ -372,7 +375,8 @@ private void toggleFollowButton( return; } - AsyncTaskHandler.load( + AsyncTaskExecutor.executeIo( + mScope, () -> !ReaderTagTable.isFollowedTagName(currentTag.getTagSlug()), isAskingToFollow -> { final String slugForTracking = currentTag.getTagSlug(); @@ -688,7 +692,8 @@ public ReaderPostAdapter( ImageManager imageManager, UiHelpers uiHelpers, @NonNull final NetworkUtilsWrapper networkUtilsWrapper, - boolean isMainReader + boolean isMainReader, + LifecycleCoroutineScope scope ) { super(); ((WordPress) context.getApplicationContext()).component().inject(this); @@ -699,6 +704,7 @@ public ReaderPostAdapter( mNetworkUtilsWrapper = networkUtilsWrapper; mAvatarSzSmall = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small); mIsMainReader = isMainReader; + mScope = scope; int displayWidth = DisplayUtils.getWindowPixelWidth(context); int cardMargin = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin); diff --git a/WordPress/src/main/java/org/wordpress/android/util/BuildConfigWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/BuildConfigWrapper.kt index b7bd6a2a1c9d..5911cf39ca0b 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/BuildConfigWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/BuildConfigWrapper.kt @@ -1,5 +1,6 @@ package org.wordpress.android.util +import android.os.Build import org.wordpress.android.BuildConfig import javax.inject.Inject @@ -33,4 +34,6 @@ class BuildConfigWrapper @Inject constructor() { val isFollowedSitesSettingsEnabled = BuildConfig.ENABLE_FOLLOWED_SITES_SETTINGS val isWhatsNewFeatureEnabled = BuildConfig.ENABLE_WHATS_NEW_FEATURE + + val androidVersion: String = Build.VERSION.RELEASE } diff --git a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt index 5a4df6403e6e..a44a13192627 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt @@ -27,4 +27,5 @@ class SiteUtilsWrapper @Inject constructor(private val appContext: Context) { fun getSiteIconUrlOfResourceSize(site: SiteModel, @DimenRes sizeRes: Int): String { return SiteUtils.getSiteIconUrl(site, appContext.resources.getDimensionPixelSize(sizeRes)) } + fun hasFullAccessToContent(site: SiteModel?): Boolean = SiteUtils.hasFullAccessToContent(site) } diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java index 24e47daeb759..dedc18123b5a 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java @@ -28,6 +28,7 @@ import org.wordpress.android.fluxc.store.MediaStore.MediaError; import org.wordpress.android.fluxc.store.media.MediaErrorSubType; import org.wordpress.android.fluxc.store.media.MediaErrorSubType.MalformedMediaArgSubType; +import org.wordpress.android.fluxc.utils.ExifUtils; import org.wordpress.android.fluxc.utils.MimeTypes; import org.wordpress.android.fluxc.utils.MimeTypes.Plan; import org.wordpress.android.imageeditor.preview.PreviewImageFragment; @@ -44,6 +45,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; public class WPMediaUtils { public interface LaunchCameraCallback { @@ -66,6 +68,9 @@ public static Uri getOptimizedMedia(Context context, String path, boolean isVide return null; } + // Read EXIF data from the original image + final Map exifData = ExifUtils.readExifData(path); + int resizeDimension = AppPrefs.getImageOptimizeMaxSize() > 1 ? AppPrefs.getImageOptimizeMaxSize() : Integer.MAX_VALUE; int quality = AppPrefs.getImageOptimizeQuality(); @@ -79,6 +84,9 @@ public static Uri getOptimizedMedia(Context context, String path, boolean isVide AppLog.e(AppLog.T.EDITOR, "Optimized picture was null!"); AnalyticsTracker.track(AnalyticsTracker.Stat.MEDIA_PHOTO_OPTIMIZE_ERROR); } else { + // Write EXIF data to the new image + ExifUtils.writeExifData(exifData, optimizedPath); + AnalyticsTracker.track(AnalyticsTracker.Stat.MEDIA_PHOTO_OPTIMIZED); return Uri.parse(optimizedPath); } diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java index db541f34b812..ea92f9d930ed 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java @@ -1,6 +1,7 @@ package org.wordpress.android.util; import android.Manifest; +import android.Manifest.permission; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; @@ -192,6 +193,8 @@ private static AppPrefs.PrefKey getPermissionAskedKey(@NonNull String permission return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_CAMERA; case Manifest.permission.POST_NOTIFICATIONS: return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_NOTIFICATIONS; + case Manifest.permission.ACCESS_MEDIA_LOCATION: + return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_ACCESS_MEDIA_LOCATION; default: AppLog.w(AppLog.T.UTILS, "No key for requested permission"); return null; @@ -216,6 +219,8 @@ public static String getPermissionName(@NonNull Context context, @NonNull String return context.getString(R.string.permission_camera); case Manifest.permission.RECORD_AUDIO: return context.getString(R.string.permission_microphone); + case Manifest.permission.ACCESS_MEDIA_LOCATION: + return context.getString(R.string.permission_access_media_location); default: AppLog.w(AppLog.T.UTILS, "No name for requested permission"); return context.getString(R.string.unknown); diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/FeatureFlagConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/FeatureFlagConfig.kt index dbb843b6fb90..0d0d08723354 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/FeatureFlagConfig.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/FeatureFlagConfig.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.launch import org.wordpress.android.BuildConfig import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.fluxc.network.rest.wpcom.mobile.FeatureFlagsRestClient import org.wordpress.android.fluxc.persistence.FeatureFlagConfigDao.FeatureFlag import org.wordpress.android.fluxc.store.NotificationStore.Companion.WPCOM_PUSH_DEVICE_UUID import org.wordpress.android.fluxc.store.mobile.FeatureFlagsStore @@ -72,12 +73,15 @@ class FeatureFlagConfig private suspend fun fetchRemoteFlags() { val response = featureFlagStore.fetchFeatureFlags( - buildNumber = BuildConfig.VERSION_CODE.toString(), - deviceId = preferences.getString(WPCOM_PUSH_DEVICE_UUID, null) - ?: generateAndStoreUUID(), - identifier = BuildConfig.APPLICATION_ID, - marketingVersion = BuildConfig.VERSION_NAME, - platform = FEATURE_FLAG_PLATFORM_PARAMETER + FeatureFlagsRestClient.FeatureFlagsPayload( + buildNumber = BuildConfig.VERSION_CODE.toString(), + deviceId = preferences.getString(WPCOM_PUSH_DEVICE_UUID, null) + ?: generateAndStoreUUID(), + identifier = BuildConfig.APPLICATION_ID, + marketingVersion = BuildConfig.VERSION_NAME, + platform = FEATURE_FLAG_PLATFORM_PARAMETER, + osVersion = android.os.Build.VERSION.RELEASE + ) ) response.featureFlags?.let { configValues -> AppLog.e(UTILS, "Feature flag values synced") diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/ReaderFloatingButtonFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderFloatingButtonFeatureConfig.kt new file mode 100644 index 000000000000..c04c1ddb287e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderFloatingButtonFeatureConfig.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val READER_FLOATING_BUTTON_REMOTE_FIELD = "reader_floating_button" + +@Feature(READER_FLOATING_BUTTON_REMOTE_FIELD, false) +class ReaderFloatingButtonFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.READER_FLOATING_BUTTON, + READER_FLOATING_BUTTON_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt index 2a3c932cd678..48d78edfb669 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt @@ -18,7 +18,6 @@ import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore import org.wordpress.android.modules.UI_THREAD -import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper import org.wordpress.android.ui.debug.preferences.DebugPrefs import org.wordpress.android.ui.main.MainActionListItem import org.wordpress.android.ui.main.MainActionListItem.ActionType @@ -30,27 +29,28 @@ import org.wordpress.android.ui.main.MainActionListItem.ActionType.NO_ACTION import org.wordpress.android.ui.main.MainActionListItem.AnswerBloggingPromptAction import org.wordpress.android.ui.main.MainActionListItem.CreateAction import org.wordpress.android.ui.main.MainFabUiState +import org.wordpress.android.ui.main.WPMainNavigationView.PageType +import org.wordpress.android.ui.main.analytics.MainCreateSheetTracker +import org.wordpress.android.ui.main.utils.MainCreateSheetHelper import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent import org.wordpress.android.ui.utils.UiString.UiStringText -import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.util.FluxCUtils import org.wordpress.android.util.SiteUtils.hasFullAccessToContent import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper -import org.wordpress.android.util.mapSafe import org.wordpress.android.util.mapNullable +import org.wordpress.android.util.mapSafe import org.wordpress.android.util.merge import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent import java.io.Serializable import java.util.Date -import java.util.Locale import javax.inject.Inject import javax.inject.Named @@ -65,11 +65,11 @@ class WPMainActivityViewModel @Inject constructor( private val selectedSiteRepository: SelectedSiteRepository, private val accountStore: AccountStore, private val siteStore: SiteStore, - private val bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper, private val bloggingPromptsStore: BloggingPromptsStore, - @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, private val shouldAskPrivacyConsent: ShouldAskPrivacyConsent, - private val voiceToContentFeatureUtils: VoiceToContentFeatureUtils + private val mainCreateSheetHelper: MainCreateSheetHelper, + private val mainCreateSheetTracker: MainCreateSheetTracker, + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, ) : ScopedViewModel(mainDispatcher) { private var isStarted = false @@ -149,7 +149,7 @@ class WPMainActivityViewModel @Inject constructor( val isSignedInWPComOrHasWPOrgSite: Boolean get() = FluxCUtils.isSignedInWPComOrHasWPOrgSite(accountStore, siteStore) - fun start(site: SiteModel?) { + fun start(site: SiteModel?, page: PageType) { if (isStarted) return isStarted = true @@ -159,17 +159,17 @@ class WPMainActivityViewModel @Inject constructor( } } - setMainFabUiState(false, site) + setMainFabUiState(false, site, page) - launch { loadMainActions(site) } + launch { loadMainActions(site, page) } updateFeatureAnnouncements() } @Suppress("LongMethod") - private suspend fun loadMainActions(site: SiteModel?, onFabClicked: Boolean = false) { + private suspend fun loadMainActions(site: SiteModel?, page: PageType, onFabClicked: Boolean = false) { val actionsList = ArrayList() - if (bloggingPromptsSettingsHelper.shouldShowPromptsFeature()) { + if (mainCreateSheetHelper.canCreatePromptAnswer()) { val prompt = site?.let { bloggingPromptsStore.getPromptForDate(it, Date()).firstOrNull()?.model } @@ -182,8 +182,14 @@ class WPMainActivityViewModel @Inject constructor( isAnswered = prompt.isAnswered, promptId = prompt.id, attribution = BloggingPromptAttribution.fromPrompt(prompt), - onClickAction = ::onAnswerPromptActionClicked, - onHelpAction = ::onHelpPrompActionClicked + onClickAction = { prompt, attribution -> + onAnswerPromptActionClicked( + prompt, + attribution, + page + ) + }, + onHelpAction = { onHelpPromptActionClicked(page) } ) ) } @@ -197,42 +203,46 @@ class WPMainActivityViewModel @Inject constructor( onClickAction = null ) ) - actionsList.add( - CreateAction( - actionType = CREATE_NEW_POST, - iconRes = R.drawable.ic_posts_white_24dp, - labelRes = R.string.my_site_bottom_sheet_add_post, - onClickAction = ::onCreateActionClicked + + if (mainCreateSheetHelper.canCreatePost()) { + actionsList.add( + CreateAction( + actionType = CREATE_NEW_POST, + iconRes = R.drawable.ic_posts_white_24dp, + labelRes = R.string.my_site_bottom_sheet_add_post, + onClickAction = { onCreateActionClicked(it, page) } + ) ) - ) - if (voiceToContentFeatureUtils.isVoiceToContentEnabled() && hasFullAccessToContent(site)) { + } + + if (mainCreateSheetHelper.canCreatePostFromAudio(site)) { actionsList.add( CreateAction( actionType = ActionType.CREATE_NEW_POST_FROM_AUDIO, iconRes = R.drawable.ic_mic_white_24dp, labelRes = R.string.my_site_bottom_sheet_add_post_from_audio, - onClickAction = ::onCreateActionClicked + onClickAction = { onCreateActionClicked(it, page) } ) ) } - if (hasFullAccessToContent(site)) { + + if (mainCreateSheetHelper.canCreatePage(site, page)) { actionsList.add( CreateAction( actionType = CREATE_NEW_PAGE, iconRes = R.drawable.ic_pages_white_24dp, labelRes = R.string.my_site_bottom_sheet_add_page, - onClickAction = ::onCreateActionClicked + onClickAction = { onCreateActionClicked(it, page) } ) ) } _mainActions.postValue(actionsList) - if (onFabClicked) trackCreateActionsSheetCard(actionsList) + if (onFabClicked) mainCreateSheetTracker.trackCreateActionsSheetCard(actionsList) } - private fun onCreateActionClicked(actionType: ActionType) { - val properties = mapOf("action" to actionType.name.lowercase(Locale.ROOT)) - analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED, properties) + private fun onCreateActionClicked(actionType: ActionType, page: PageType) { + mainCreateSheetTracker.trackActionTapped(page, actionType) _isBottomSheetShowing.postValue(Event(false)) _createAction.postValue(actionType) @@ -244,29 +254,19 @@ class WPMainActivityViewModel @Inject constructor( } } - private fun onAnswerPromptActionClicked(promptId: Int, attribution: BloggingPromptAttribution) { - analyticsTracker.track( - Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED, - mapOf("attribution" to attribution.value).filterValues { !it.isNullOrBlank() } - ) + private fun onAnswerPromptActionClicked(promptId: Int, attribution: BloggingPromptAttribution, page: PageType) { + mainCreateSheetTracker.trackAnswerPromptActionTapped(page, attribution) _isBottomSheetShowing.postValue(Event(false)) _createPostWithBloggingPrompt.postValue(promptId) } - private fun onHelpPrompActionClicked() { - analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED) + private fun onHelpPromptActionClicked(page: PageType) { + mainCreateSheetTracker.trackHelpPromptActionTapped(page) _openBloggingPromptsOnboarding.call() } - private fun trackCreateActionsSheetCard(actions: List) { - if (actions.any { it is AnswerBloggingPromptAction }) { - analyticsTracker.track(Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED) - } - } - - fun onFabClicked(site: SiteModel?) { + fun onFabClicked(site: SiteModel?, page: PageType) { appPrefsWrapper.setMainFabTooltipDisabled(true) - setMainFabUiState(true, site) _showQuickStarInBottomSheet.postValue(quickStartRepository.activeTask.value == PUBLISH_POST) @@ -278,9 +278,9 @@ class WPMainActivityViewModel @Inject constructor( // Reload main actions, since the first time this is initialized the SiteModel may not contain the // latest info. - loadMainActions(site, onFabClicked = true) + loadMainActions(site, page, onFabClicked = true) - analyticsTracker.track(Stat.MY_SITE_CREATE_SHEET_SHOWN) + mainCreateSheetTracker.trackSheetShown(page) _isBottomSheetShowing.postValue(Event(true)) } } else { @@ -289,18 +289,18 @@ class WPMainActivityViewModel @Inject constructor( } } - fun onPageChanged(isOnMySitePageWithValidSite: Boolean, site: SiteModel?) { - val showFab = if (buildConfigWrapper.isCreateFabEnabled) isOnMySitePageWithValidSite else false - setMainFabUiState(showFab, site) + fun onPageChanged(site: SiteModel?, hasValidSite: Boolean, page: PageType) { + val showFab = hasValidSite && mainCreateSheetHelper.shouldShowFabForPage(page) + setMainFabUiState(showFab, site, page) } fun onOpenLoginPage() = launch { _switchToMeTab.value = Event(Unit) } - fun onResume(site: SiteModel?, isOnMySitePageWithValidSite: Boolean) { - val showFab = if (buildConfigWrapper.isCreateFabEnabled) isOnMySitePageWithValidSite else false - setMainFabUiState(showFab, site) + fun onResume(site: SiteModel?, hasValidSite: Boolean, page: PageType?) { + val showFab = hasValidSite && mainCreateSheetHelper.shouldShowFabForPage(page) + setMainFabUiState(showFab, site, page) checkAndShowFeatureAnnouncement() } @@ -327,18 +327,20 @@ class WPMainActivityViewModel @Inject constructor( } } - private fun setMainFabUiState(isFabVisible: Boolean, site: SiteModel?) { + private fun setMainFabUiState(isFabVisible: Boolean, site: SiteModel?, page: PageType?) { + if (isFabVisible && page != null) mainCreateSheetTracker.trackFabShown(page) + val newState = MainFabUiState( isFabVisible = isFabVisible, isFabTooltipVisible = if (appPrefsWrapper.isMainFabTooltipDisabled()) false else isFabVisible, - CreateContentMessageId = getCreateContentMessageId(site) + CreateContentMessageId = getCreateContentMessageId(site, page) ) _fabUiState.value = newState } - fun getCreateContentMessageId(site: SiteModel?): Int = - if (hasFullAccessToContent(site)) { + fun getCreateContentMessageId(site: SiteModel?, page: PageType?): Int = + if (mainCreateSheetHelper.canCreatePage(site, page)) { R.string.create_post_page_fab_tooltip } else { R.string.create_post_page_fab_tooltip_contributors @@ -375,7 +377,7 @@ class WPMainActivityViewModel @Inject constructor( selectedSiteRepository.removeSite() } - fun triggerCreatePageFlow(){ + fun triggerCreatePageFlow() { _createAction.postValue(CREATE_NEW_PAGE_FROM_PAGES_CARD) } diff --git a/WordPress/src/main/res/layout/filtered_list_component.xml b/WordPress/src/main/res/layout/filtered_list_component.xml index 307ea2dd3203..c5ba57c0a687 100644 --- a/WordPress/src/main/res/layout/filtered_list_component.xml +++ b/WordPress/src/main/res/layout/filtered_list_component.xml @@ -2,7 +2,7 @@ diff --git a/WordPress/src/main/res/layout/signup_epilogue.xml b/WordPress/src/main/res/layout/signup_epilogue.xml index ae150c682c7f..25d4291af80a 100644 --- a/WordPress/src/main/res/layout/signup_epilogue.xml +++ b/WordPress/src/main/res/layout/signup_epilogue.xml @@ -88,4 +88,14 @@ android:layout_marginStart="@dimen/margin_extra_large" android:layout_marginTop="@dimen/margin_medium_large" android:text="@string/login_done" /> + + + diff --git a/WordPress/src/main/res/values-ar/strings.xml b/WordPress/src/main/res/values-ar/strings.xml index b370d66b7780..005306e5e8ae 100644 --- a/WordPress/src/main/res/values-ar/strings.xml +++ b/WordPress/src/main/res/values-ar/strings.xml @@ -1,11 +1,36 @@ + إعادة البدء + تم تنزيل التحديث. أعد التشغيل للتطبيق. + تدوينة من ملف صوتي + فتح قائمة + إزالة تدوينة تنال الإعجاب + الإعجاب بتدوينة + فتح مدونة + فتح تدوينة + إعادة المحاولة + يتعذر علينا العثور على أي تدوينات موسومة بـ %s الآن + يتعذر علينا تحميل التدوينات من هذا الوسم الآن + لا توجد تدوينات لـ %s + المزيد من %s + الوسوم + اختر الألوان والخطوط الملائمة لك. عند قراءة تدوينة، اضغط على أيقونة AA في أعلى الشاشة. + تفضيلات القراءة + اضغط على القائمة المنسدلة في الأعلى وحدد الوسوم للوصول إلى التدفقات الواردة من الوسوم التي تتابعها. + تدفق الوسوم + جديد في القارئ + الوسوم الخاصة بك + تحقق من اتصالك بالشبكة وحاول مرة أخرى. + يتعذر تحميل هذا المحتوى حاليًا. + المشتركون + مشترك + نمو المشترك مشترك مشترك عبر البريد الإلكتروني لا يوجد مشتركون عبر البريد الإلكتروني حتى الآن @@ -578,14 +603,12 @@ Language: ar ستنتقل ميزة التنبيهات إلى Jetpack ستنتقل ميزة القارئ إلى تطبيق Jetpack التبديل إلى تطبيق Jetpack الجديد - يتعذر تحميل هذا المحتوى حاليًا. حدث خطأ في أثناء تحميل المطالبات. عذرًا لا توجد مطالبات بعد %d من الإجابات إجابة واحدة ستنتقل ميزة الإحصاءات لديك إلى تطبيق Jetpack - تحقق من اتصالك بالشبكة وحاول مرة أخرى. 0 من الإجابات ✓ تم الرد المطالبات diff --git a/WordPress/src/main/res/values-cs/strings.xml b/WordPress/src/main/res/values-cs/strings.xml index dc706782450c..7e069db80244 100644 --- a/WordPress/src/main/res/values-cs/strings.xml +++ b/WordPress/src/main/res/values-cs/strings.xml @@ -148,8 +148,6 @@ Language: cs_CZ Čtečka se přesouvá do aplikace Jetpack Vaše statistiky se přesouvají do aplikace Jetpack Přepněte na novou aplikaci Jetpack - Zkontrolujte připojení k síti a zkuste to znovu. - Momentálně tento obsah nelze načíst Došlo k chybě při načítání to se mi líbí Jejda Zatím žádné výzvy diff --git a/WordPress/src/main/res/values-de/strings.xml b/WordPress/src/main/res/values-de/strings.xml index d341412c14e1..6ef4fbfd9ebe 100644 --- a/WordPress/src/main/res/values-de/strings.xml +++ b/WordPress/src/main/res/values-de/strings.xml @@ -1,11 +1,33 @@ + Neu starten + Update heruntergeladen. Zum Anwenden neu starten. + Beitrag aus Audio erstellen + Menü öffnen + Like vom Beitrag entfernen + Beitrag mit einem „Like“ markieren + Blog öffnen + Beitrag öffnen + Erneut versuchen + Es können derzeit keine Beiträge mit dem Schlagwort „%s“ gefunden werden + Es können derzeit keine Beiträge unter diesem Schlagwort geladen werden + Es wurden keine Beiträge für „%s“ gefunden + Mehr von %s + Schlagwörter + Wähle Farben und Schriftarten, die zu dir passen. Wenn du einen Beitrag liest, klicke oben im Bildschirm auf das AA-Icon. + Leseeinstellungen + Tippe oben auf das Dropdown-Menü und wähle „Schlagwörter“ aus, um auf Streams deiner abonnierten Schlagwörter zuzugreifen. + Schlagwörter-Feed + Neu im Reader + Deine Schlagwörter + Prüfe deine Netzwerkverbindung und versuche es erneut. + Dieser Inhalt kann gerade nicht geladen werden Abonnenten Abonnent Abonnentenwachstum @@ -582,8 +604,6 @@ Language: de Der Reader wird in die Jetpack-App verschoben Deine Statistiken werden in die Jetpack-App verschoben Zur neuen Jetpack-App wechseln - Prüfe deine Netzwerkverbindung und versuche es erneut. - Dieser Inhalt kann gerade nicht geladen werden Beim Laden der Schreibanregungen ist ein Fehler aufgetreten. Ups Noch keine Schreibanregungen diff --git a/WordPress/src/main/res/values-en-rCA/strings.xml b/WordPress/src/main/res/values-en-rCA/strings.xml index 73c0b5802f93..68c35e862954 100644 --- a/WordPress/src/main/res/values-en-rCA/strings.xml +++ b/WordPress/src/main/res/values-en-rCA/strings.xml @@ -487,8 +487,6 @@ Language: en_CA Stats, Reader, Notifications and other features will soon move to the Jetpack mobile app. There was an error loading prompts. Oops - Check your network connection and try again. - Unable to load this content right now No prompts yet 1 answer %d answers diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml index aac96f5c19cd..afa094cc8bc3 100644 --- a/WordPress/src/main/res/values-en-rGB/strings.xml +++ b/WordPress/src/main/res/values-en-rGB/strings.xml @@ -1,11 +1,37 @@ + Media location + Audio Recording Permission Required + To record audio, this app needs permission to access your microphone. You have previously denied this permission. Please enable the microphone permission in the app settings to use this feature. + Tap to edit + Choose colours and fonts that suit you. When you’re reading a post, tap the AA icon at the top of the screen. + Tags + More from %s + No posts found for %s + We couldn\'t load posts from this tag right now + We couldn\'t find any posts tagged %s right now + Retry + open post + open blog + like post + remove post like + open menu + Post from audio + Update downloaded. Restart to apply. + Restart + Reading Preferences + Unable to load this content right now + Check your network connection and try again. + Your tags + New in Reader + Tags stream + Tap the dropdown at the top and select Tags to access streams from your followed tags. Subscribers Subscriber Subscriber growth @@ -573,16 +599,6 @@ Language: en_GB Learn more at jetpack.com Remind me later Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app on %s. - Jetpack features are moving soon. - Notifications are moving to Jetpack - Reader is moving to the Jetpack app - Your stats are moving to the Jetpack app - Switch to the new Jetpack app - Check your network connection and try again. - Unable to load this content right now - There was an error loading prompts. - Oops - No prompts yet Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app soon. Switch to the Jetpack app Switching is free and only takes a minute. @@ -590,6 +606,14 @@ Language: en_GB %d answers 0 answers 1 answer + Jetpack features are moving soon. + No prompts yet + Notifications are moving to Jetpack + Oops + Reader is moving to the Jetpack app + Switch to the new Jetpack app + There was an error loading prompts. + Your stats are moving to the Jetpack app ✓ Answered close Prompts diff --git a/WordPress/src/main/res/values-es-rCL/strings.xml b/WordPress/src/main/res/values-es-rCL/strings.xml index 155287f105f4..b2b8b402e509 100644 --- a/WordPress/src/main/res/values-es-rCL/strings.xml +++ b/WordPress/src/main/res/values-es-rCL/strings.xml @@ -1,11 +1,33 @@ + Reiniciar + Actualización descargada. Reinicia para aplicar. + Publicar desde audio + abrir menú + eliminar \"Me gusta\" de la entrada + dar \"Me gusta\" en la entrada + abrir blog + abrir entrada + Reintentar + No hemos podido encontrar ninguna entrada con la etiqueta %s en este momento + No hemos podido cargar entradas con esta etiqueta en este momento + No se encontraron entradas para %s + Más de %s + Etiquetas + Elige colores y fuentes que te gusten. Cuando estés leyendo una entrada, toca el ícono AA en la parte superior de la pantalla. + Preferencias de lectura + Toca el menú desplegable en la parte superior y selecciona \"Etiquetas\" para acceder al feed de las etiquetas que sigues. + Feed de etiquetas + Nuevo en el lector + Tus etiquetas + Comprueba tu conexión a la red e inténtalo de nuevo. + En este momento no se ha podido cargar este contenido Suscriptores Suscriptor Incremento de suscriptores @@ -579,8 +601,6 @@ Language: es_CL Cambiar a la aplicación de Jetpack Se ha producido un error al cargar las indicaciones. ¡Oh! - Comprueba tu conexión a la red e inténtalo de nuevo. - En este momento no se ha podido cargar este contenido Todavía no hay sugerencias 1 respuesta %d respuestas diff --git a/WordPress/src/main/res/values-es-rCO/strings.xml b/WordPress/src/main/res/values-es-rCO/strings.xml index 3a60faaec9f2..09bbff66965e 100644 --- a/WordPress/src/main/res/values-es-rCO/strings.xml +++ b/WordPress/src/main/res/values-es-rCO/strings.xml @@ -1,11 +1,33 @@ + Reiniciar + Actualización descargada. Reinicia para aplicar. + Publicar desde audio + abrir menú + eliminar Me gusta de la entrada + dar Me gusta en la entrada + abrir blog + abrir entrada + Reintentar + No hemos podido encontrar ninguna entrada con la etiqueta %s en este momento + No hemos podido cargar entradas con esta etiqueta en este momento + No se encontraron entradas para %s + Más de %s + Etiquetas + Elige colores y fuentes que te gusten. Cuando estés leyendo una entrada, toca el icono AA en la parte superior de la pantalla. + Preferencias de lectura + Toca el menú desplegable en la parte superior y selecciona Etiquetas para acceder al flujo de las etiquetas que sigues. + Flujo de etiquetas + Nuevo en el lector + Tus etiquetas + Comprueba tu conexión a la red e inténtalo de nuevo. + En este momento no se ha podido cargar este contenido Suscriptores Suscriptor Incremento de suscriptores @@ -582,8 +604,6 @@ Language: es_CO El lector se está trasladando a la aplicación de Jetpack La estadísticas se están trasladado a la aplicación de Jetpack Cambiar a la nueva aplicación de Jetpack - Comprueba tu conexión a la red e inténtalo de nuevo. - En este momento no se ha podido cargar este contenido Se ha producido un error al cargar las indicaciones. ¡Vaya! Todavía no hay sugerencias diff --git a/WordPress/src/main/res/values-es/strings.xml b/WordPress/src/main/res/values-es/strings.xml index a8b2c6dd8086..2448c5258ae9 100644 --- a/WordPress/src/main/res/values-es/strings.xml +++ b/WordPress/src/main/res/values-es/strings.xml @@ -1,11 +1,36 @@ + Reiniciar + Actualización descargada. Reinicia para aplicar. + Publicar desde audio + abrir menú + eliminar Me gusta de la entrada + dar Me gusta en la entrada + abrir blog + abrir entrada + Reintentar + No hemos podido encontrar ninguna entrada con la etiqueta %s en este momento + No hemos podido cargar entradas con esta etiqueta en este momento + No se encontraron entradas para %s + Más de %s + Etiquetas + Elige colores y fuentes que te gusten. Cuando estés leyendo una entrada, toca el icono AA en la parte superior de la pantalla. + Preferencias de lectura + Toca el menú desplegable en la parte superior y selecciona Etiquetas para acceder al flujo de las etiquetas que sigues. + Flujo de etiquetas + Nuevo en el lector + Tus etiquetas + Comprueba tu conexión a la red e inténtalo de nuevo. + En este momento no se ha podido cargar este contenido + Suscriptores + Suscriptor + Crecimiento de suscriptores Suscriptor Suscriptor por correo electrónico Aún no hay suscriptores por correo electrónico @@ -579,8 +604,6 @@ Language: es El lector se está trasladando a la aplicación de Jetpack La estadísticas se están trasladado a la aplicación de Jetpack Cambiar a la nueva aplicación de Jetpack - Comprueba tu conexión a la red e inténtalo de nuevo. - En este momento no se ha podido cargar este contenido Se ha producido un error al cargar las indicaciones. ¡Vaya! Todavía no hay sugerencias diff --git a/WordPress/src/main/res/values-fr-rCA/strings.xml b/WordPress/src/main/res/values-fr-rCA/strings.xml index b8d4668a80d4..7da066aa1f78 100644 --- a/WordPress/src/main/res/values-fr-rCA/strings.xml +++ b/WordPress/src/main/res/values-fr-rCA/strings.xml @@ -1,11 +1,36 @@ + Redémarrer + Mise à jour téléchargée. Redémarrez pour appliquer les mises à jour. + Publier à partir d’un contenu audio + ouvrir le menu + retirer la mention J’aime de l’article + aimer l’article + ouvrir le blog + ouvrir l’article + Réessayer + Nous n’avons pas trouvé d’articles avec l’étiquette %s dans l’immédiat + Nous n’avons pas pu charger d’articles avec cette étiquette dans l’immédiat + Aucun résultat pour %s + Plus de %s + Étiquettes + Choisissez les couleurs et polices qui vous conviennent. Lorsque vous lisez un article, appuyez sur l’icône AA en haut de l’écran. + Préférences de lecture + Appuyez sur la liste déroulante en haut et sélectionnez Étiquettes pour accéder aux flux des étiquettes que vous suivez. + Flux Étiquettes + Nouveau dans le Lecteur + Vos étiquettes + Vérifiez votre connexion réseau et réessayez. + Impossible de charger ce contenu pour le moment + Abonnés + Abonné + Évolution des abonnés Abonné Abonné par e-mail Aucun abonné par e-mail pour le moment @@ -572,8 +597,6 @@ Language: fr Le lecteur va être déplacé dans l’application Jetpack Les statistiques vont être déplacées dans l’application Jetpack Passer à la nouvelle application Jetpack - Vérifiez votre connexion réseau et réessayez. - Impossible de charger ce contenu pour le moment Un problème est survenu lors du chargement des incitations. Oups Pas encore d’incitation diff --git a/WordPress/src/main/res/values-fr/strings.xml b/WordPress/src/main/res/values-fr/strings.xml index b8d4668a80d4..7da066aa1f78 100644 --- a/WordPress/src/main/res/values-fr/strings.xml +++ b/WordPress/src/main/res/values-fr/strings.xml @@ -1,11 +1,36 @@ + Redémarrer + Mise à jour téléchargée. Redémarrez pour appliquer les mises à jour. + Publier à partir d’un contenu audio + ouvrir le menu + retirer la mention J’aime de l’article + aimer l’article + ouvrir le blog + ouvrir l’article + Réessayer + Nous n’avons pas trouvé d’articles avec l’étiquette %s dans l’immédiat + Nous n’avons pas pu charger d’articles avec cette étiquette dans l’immédiat + Aucun résultat pour %s + Plus de %s + Étiquettes + Choisissez les couleurs et polices qui vous conviennent. Lorsque vous lisez un article, appuyez sur l’icône AA en haut de l’écran. + Préférences de lecture + Appuyez sur la liste déroulante en haut et sélectionnez Étiquettes pour accéder aux flux des étiquettes que vous suivez. + Flux Étiquettes + Nouveau dans le Lecteur + Vos étiquettes + Vérifiez votre connexion réseau et réessayez. + Impossible de charger ce contenu pour le moment + Abonnés + Abonné + Évolution des abonnés Abonné Abonné par e-mail Aucun abonné par e-mail pour le moment @@ -572,8 +597,6 @@ Language: fr Le lecteur va être déplacé dans l’application Jetpack Les statistiques vont être déplacées dans l’application Jetpack Passer à la nouvelle application Jetpack - Vérifiez votre connexion réseau et réessayez. - Impossible de charger ce contenu pour le moment Un problème est survenu lors du chargement des incitations. Oups Pas encore d’incitation diff --git a/WordPress/src/main/res/values-gl/strings.xml b/WordPress/src/main/res/values-gl/strings.xml index 6429e9b386f7..42259fc46fb2 100644 --- a/WordPress/src/main/res/values-gl/strings.xml +++ b/WordPress/src/main/res/values-gl/strings.xml @@ -584,9 +584,7 @@ Language: gl_ES Os avisos estanse trasladando á aplicación de Jetpack O lector estase trasladando á aplicación de Jetpack Produciuse un erro ao cargar as indicacións. - Neste momento non se puido cargar este contido A estatísticas estanse trasladado á aplicación de Jetpack - Comproba a túa conexión á rede e inténtao de novo. Peticións ✓ Respondido pechar diff --git a/WordPress/src/main/res/values-he/strings.xml b/WordPress/src/main/res/values-he/strings.xml index 405e6d661bc9..e77afbe18914 100644 --- a/WordPress/src/main/res/values-he/strings.xml +++ b/WordPress/src/main/res/values-he/strings.xml @@ -1,11 +1,36 @@ + להפעיל מחדש + העדכון הורד. כדי להחיל עדכונים יש לבצע הפעלה מחדש. + פוסט מאודיו + לפתוח תפריט + להסיר את הלייק מהפוסט + להוסיף לייק לפוסט + לפתוח את הבלוג + לפתוח את הפוסט + לנסות שוב + לא הצלחנו למצוא פוסטים עם התגית %s כעת + לא הצלחנו לטעון את הפוסטים של התגית הזאת כעת + לא נמצאו פוסטים עבור %s + עוד בנושא %s + תגיות + לבחור צבעים וגופנים לפי טעמך. כשקוראים פוסט, אפשר ללחוץ על הסמל AA שבחלקו העליון של המסך. + העדפות קריאה + יש להקיש על התפריט הנפתח למעלה ולבחור \'תגיות\' כדי לגשת לפיד של התגיות שבמעקב שלך. + פיד תגיות + חדש ב-Reader + התגיות שלך + יש לבדוק את החיבור לרשת ולנסות שוב. + אין אפשרות לטעון את התוכן כרגע + מנויים + מנוי + צמיחת מנויים מנוי מנוי באימייל אין עדיין מנויים באימייל @@ -572,8 +597,6 @@ Language: he_IL הכלי Reader עובר לאפליקציה של Jetpack הנתונים הסטטיסטיים שלך עוברים לאפליקציה של Jetpack לעבור לאפליקציה החדשה של Jetpack - יש לבדוק את החיבור לרשת ולנסות שוב. - אין אפשרות לטעון את התוכן כרגע אירעה שגיאה בטעינת ההצעות. אופס עדיין אין הצעות diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml index c6642f12e326..0fdd92caffc3 100644 --- a/WordPress/src/main/res/values-id/strings.xml +++ b/WordPress/src/main/res/values-id/strings.xml @@ -1,11 +1,36 @@ + Mulai ulang + Pembaruan telah diunduh. Mulai ulang untuk menerapkan pembaruan. + Pos dari Audio + buka menu + hapus suka pada pos + sukai pos + buka blog + buka pos + Coba lagi + Saat ini kami tidak dapat menemukan pos dengan tag %s + Saat ini kami tidak dapat memuat pos dari tag ini + Tidak ditemukan pos untuk %s + Selengkapnya dari %s + Tag + Pilih warna dan font sesuai selera. Saat membaca pos, ketuk ikon AA di bagian atas layar. + Preferensi Membaca + Ketuk menu tarik-turun di bagian atas dan pilih Tag untuk mengakses stream dari tag yang Anda ikuti. + Tags Stream + Baru di Pembaca + Tag Anda + Periksa koneksi internet dan coba lagi. + Tidak dapat memuat konten ini sekarang + Pelanggan + Pelanggan + Perkembangan Pelanggan Pelanggan Pelanggan Email Belum ada pelanggan email @@ -579,8 +604,6 @@ Language: id Reader akan berpindah ke aplikasi Jetpack Statistik Anda akan berpindah ke aplikasi Jetpack Beralih ke aplikasi Jetpack baru - Periksa koneksi internet Anda dan coba lagi. - Tidak dapat memuat konten ini sekarang Terjadi eror saat memuat prompt. Waduh Belum ada prompt diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml index b0a85ca574a9..62f3bc57b0ca 100644 --- a/WordPress/src/main/res/values-it/strings.xml +++ b/WordPress/src/main/res/values-it/strings.xml @@ -1,11 +1,36 @@ + Riavvia + Aggiornamento scaricato. Riavvia per applicarlo. + Articolo dall\'audio + apri menu + rimuovi i Mi piace agli articoli + mi piace articolo + apri blog + apri articolo + Riprova + Non abbiamo trovato articoli con il tag %s + Non siamo riusciti a caricare gli articoli da questo tag + Nessun articolo trovato per %s + Ulteriori informazioni da %s + Tag + Scegli i colori e i caratteri che più ti si addicono. Mentre leggi un articolo, tocca l\'icona AA nella parte superiore dello schermo. + Preferenze di lettura + Tocca il menu a tendina in alto e seleziona Tag per accedere agli stream dei tag seguiti. + Stream dei tag + Novità su Reader + I tuoi tag + Controlla la connessione di rete e riprova. + Impossibile caricare questo contenuto al momento + Abbonati + Abbonato + Crescita abbonati Abbonato Abbonati e-mail Ancora nessun abbonato e-mail @@ -574,8 +599,6 @@ Language: it Le notifiche si stanno spostando sull\'app Jetpack Le tue statistiche si stanno spostando sull\'app Jetpack Passa alla nuova app Jetpack - Controlla la connessione di rete e riprova. - Impossibile caricare questo contenuto al momento Si è verificato un errore durante il caricamento delle richieste. Ops Ancora nessuna richiesta diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml index 1fe65db69d93..831f9782fc57 100644 --- a/WordPress/src/main/res/values-ja/strings.xml +++ b/WordPress/src/main/res/values-ja/strings.xml @@ -1,11 +1,36 @@ + 再開 + 更新がダウンロードされました。 再起動して適用します。 + 音声ファイルから投稿 + メニューを開く + 投稿の「いいね」を削除 + 投稿を「いいね」する + ブログを開く + 投稿を開く + 再試行 + 現在、%s がタグ付けされた投稿は見つかりませんでした + 現在、このタグから投稿を読み込むことができません + %s の投稿が見つかりませんでした + %s の続き + タグ + お好みの色とフォントを選択してください。 投稿を読んでいるときに、画面上部の AA アイコンをタップします。 + 閲覧の設定 + 上部のドロップダウンをタップし、「タグ」を選択して、フォロー中のタグからストリームにアクセスします。 + タグのストリーム + Reader の新規 + あなたのタグ + ネットワーク接続を確認して、もう一度お試しください。 + 現在このコンテンツを読み込めません + 購読者 + 購読者 + 購読者の拡大 購読者 メール購読者 まだメール購読者はいません @@ -567,8 +592,6 @@ Language: ja_JP Reader は Jetpack アプリに移動しています 統計は Jetpack アプリに移動しています 新しい Jetpack アプリに切り替える - ネットワーク接続を確認して、もう一度お試しください。 - 現在このページを読み込めません プロンプトの読み込みでエラーが発生しました。 エラーです プロンプトはまだありません diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml index fcbe1fbb5e31..824755431e89 100644 --- a/WordPress/src/main/res/values-ko/strings.xml +++ b/WordPress/src/main/res/values-ko/strings.xml @@ -1,11 +1,36 @@ + 재시작 + 업데이트가 다운로드되었습니다. 적용하려면 재시작하세요. + 오디오에서 글 작성하기 + 메뉴 열기 + 글에서 좋아요 제거하기 + 글에 좋아요 표시하기 + 블로그 열기 + 글 열기 + 다시 시도 + 현재 태그가 %s인 글을 찾을 수 없습니다. + 현재 이 태그에서 글을 로드할 수 없습니다. + %s에 대한 글을 찾을 수 없습니다. + %s에서 더 보기 + 태그 + 자신에게 맞는 색상과 글꼴을 선택하세요. 글을 읽을 때 화면 상단의 AA 아이콘을 탭하세요. + 읽기 기본 설정 + 상단의 드롭다운을 탭하고 태그를 선택하여 회원님이 팔로우하는 태그의 스트림에 접근하세요. + 태그 스트림 + 뉴스피드 새 항목 + 내 태그 + 네트워크 연결을 확인하고 다시 시도하세요. + 지금 이 콘텐츠를 로드할 수 없습니다. + 구독자 + 구독자 + 구독자 성장세 구독자 이메일 구독자 아직 이메일 구독자 없음 @@ -579,8 +604,6 @@ Language: ko_KR 젯팩 앱으로 리더 이동 중 젯팩 앱으로 통계 이동 중 새 젯팩 앱으로 전환 - 네트워크 연결을 확인하고 다시 시도하세요. - 지금 이 콘텐츠를 로드할 수 없습니다. 프롬프트 로드 중 오류가 발생했습니다. 죄송합니다. 아직 프롬프트 없음 diff --git a/WordPress/src/main/res/values-lv/strings.xml b/WordPress/src/main/res/values-lv/strings.xml index 7da9dd32f15f..b6ed46cf2cc8 100644 --- a/WordPress/src/main/res/values-lv/strings.xml +++ b/WordPress/src/main/res/values-lv/strings.xml @@ -1,6 +1,6 @@ + Herstart + Update gedownload. Herstarten om toe te passen. + Bericht van audio + open menu + verwijder bericht vind-ik-leuk + vind-ik-leuk bericht + open blog + open bericht + Opnieuw proberen + We konden op dit moment geen berichten vinden met de tag %s. + We konden berichten met deze tag op dit moment niet laden + Geen berichten gevonden voor %s + Meer van %s + Tags + Kies kleuren en lettertypen die bij je passen. Als je een bericht leest, dan tikken op het pictogram AA bovenaan het scherm. + Leesvoorkeuren + Tikken op de dropdown bovenaan en selecteer Tags om toegang te krijgen tot streams van je gevolgde tags. + Tags stream + Nieuw in Reader + Je tags + Controleer je netwerkverbinding en probeer het nogmaals. + Deze inhoud kan momenteel niet geladen worden Abonnees Abonnee Groei van abonnees @@ -582,8 +604,6 @@ Language: nl Reader zal naar de Jetpack-app worden verplaatst Je statistieken zullen naar de Jetpack-app worden verplaatst Schakel over naar de nieuwe Jetpack-app - Controleer je netwerkverbinding en probeer het nogmaals. - Deze inhoud kan momenteel niet geladen worden Er is een fout opgetreden bij het laden van opdrachten. Oeps Er zijn nog geen opdrachten diff --git a/WordPress/src/main/res/values-pl/strings.xml b/WordPress/src/main/res/values-pl/strings.xml index 7aaf403c436a..00c1c4096718 100644 --- a/WordPress/src/main/res/values-pl/strings.xml +++ b/WordPress/src/main/res/values-pl/strings.xml @@ -1,11 +1,28 @@ + Uruchom ponownie + Aktualizacja pobrana. Uruchom ponownie aby ją zastosować. + Wpis z audio. + otwórz menu + usuń polubienie wpisu + polub wpis + otwórz blog + otwórz wpis + Spróbuj ponownie + Nie znaleziono żadnego wpisu z tagiem %s + Nie można było wczytać wpisów z tym tagiem + Nie znaleziono wpisów dla %s + Więcej z %s + Tagi + Wybierz pasujące ci kolory i czcionki. Podczas czytania wpisu kliknij ikonę AA na górze ekranu. + Ustawienia czytania + Aby uzyskać dostęp do zawartości obserwowanych tagów kliknij listę rozwijaną na górze je wybierz. Ruch Rozmiar liter, %1$s Plik nie jest obsługiwany jako plik mediów. diff --git a/WordPress/src/main/res/values-pt-rBR/strings.xml b/WordPress/src/main/res/values-pt-rBR/strings.xml index b6923e50a2af..58e243932458 100644 --- a/WordPress/src/main/res/values-pt-rBR/strings.xml +++ b/WordPress/src/main/res/values-pt-rBR/strings.xml @@ -308,8 +308,6 @@ Language: pt_BR O Leitor está migrando para o aplicativo Jetpack Suas estatísticas estão migrando para o aplicativo Jetpack Mudar para o novo aplicativo do Jetpack - Verifique sua conexão de rede e tente novamente. - Não foi possível carregar esse conteúdo no momento Ocorreu um erro ao carregar as sugestões. Opa Nenhuma sugestão ainda diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml index 7fba41ca7c50..1894462b1998 100644 --- a/WordPress/src/main/res/values-ro/strings.xml +++ b/WordPress/src/main/res/values-ro/strings.xml @@ -1,11 +1,33 @@ + Repornește + Am descărcat actualizarea. Repornește pentru a o aplica. + Articol din fișier audio + deschide meniul + înlătură aprecierea articolului + apreciază articolul + deschide blogul + deschide articolul + Reîncearcă + Momentan, nu am găsit niciun articol etichetat cu %s + Momentan, nu am putut să încărcăm articole cu această etichetă + Nu am găsit niciun articol pentru %s + Mai multe pentru %s + Etichete + Alegi culorile și fonturile pe care le vrei. Când citești un articol, atinge iconul AA în partea de sus a ecranului. + Preferințe pentru citire + Atinge lista derulantă în partea de sus și selectează Etichete pentru a accesa fluxurile pentru etichetele urmărite. + Flux de etichete + Noutăți în Cititor + Etichetele tale + Verifică conexiunea la rețea și încearcă din nou. + Nu pot să încarc acest conținut chiar acum Abonați Abonat Creștere număr de abonați @@ -582,8 +604,6 @@ Language: ro Cititorul a fost mutat în aplicația Jetpack Statisticile se mută în aplicația Jetpack Comută la noua aplicație Jetpack - Verifică conexiunea rețelei și încearcă din nou. - Nu pot să încarc acest conținut chiar acum A fost o eroare la încărcarea îndemnurilor. Hopa Niciun îndemn încă @@ -2154,7 +2174,7 @@ Language: ro Dacă abia ai înregistrat un nume de domeniu, te rog așteaptă până terminăm setarea lui și încearcă din nou.\n\nDacă nu, se pare că ceva nu a mers bine și funcționalitatea modulului ar putea să nu fie disponibilă pentru acest site. Stare (indisponibilă) Prin înregistrarea acestui domeniu ești de acord cu %1$stermenii și condițiile%2$s. - Verifică conexiunea rețelei și încearcă din nou. + Verifică conexiunea la rețea și încearcă din nou. Nu pot să încarc această pagină chiar acum. Nu am putut aduce setările: unele API-uri sunt indisponibile pentru această combinație de ID aplicație OAuth + cont. Prin inițializarea Jetpack ești de acord cu %1$stermenii și condițiile%2$s. diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml index 59f5259d24d9..047c726aaa32 100644 --- a/WordPress/src/main/res/values-ru/strings.xml +++ b/WordPress/src/main/res/values-ru/strings.xml @@ -1,11 +1,33 @@ + Перезапуск + Обновите загруженное. Выполните перезагрузку, чтобы обновления вступили в силу. + Публикация из аудио + открыть меню + удалить отметку «Нравится» в публикации + поставить отметку «Нравится» + открыть блог + открыть запись + Повторить + Не удалось найти записи с метками %s + Не удалось загрузить записи с этой меткой + Записей по запросу %s не найдено + Другие записи с меткой %s + Метки + Выберите цвета и шрифты по своему вкусу. Во время чтения записи нажмите значок АА в верхней части экрана. + Настройки чтения + Нажмите раскрывающееся меню в верхней части и выберите «Метки», чтобы перейти к лентам записей с метками, на которые вы подписались. + Ленты записей с метками + Новое в «Чтиве» + Ваши метки + Проверьте подключение к сети и попробуйте снова. + Сейчас невозможно загрузить этот контент. Подписчики Подписчик Рост числа подписчиков @@ -582,8 +604,6 @@ Language: ru Чтиво переезжает в приложение Jetpack Статистика переезжает в приложение Jetpack Перейти в новое приложение Jetpack - Проверьте ваше подключение к сети и попробуйте снова. - Сейчас невозможно загрузить это содержимое При загрузке подсказок возникла ошибка. Ой! Пока нет подсказок diff --git a/WordPress/src/main/res/values-sk/strings.xml b/WordPress/src/main/res/values-sk/strings.xml index 075ab4bb3a23..7b4d57e1708d 100644 --- a/WordPress/src/main/res/values-sk/strings.xml +++ b/WordPress/src/main/res/values-sk/strings.xml @@ -1,6 +1,6 @@ + Rinise + Postim që nga audio + hapni menunë + Riprovoni + S’u gjetën postime për %s + Më tepër nga %s + Etiketa + Parapëlqime Leximi + Të reja në Lexues + Etiketat Tuaja + Kontrolloni lidhjen tuaj në rrjet dhe riprovoni. + S’arrihet të ngarkohet kjo lëndë këtë çast. + Pajtimtarë + Pajtimtar + Shtim Pajtimtarësh + Pajtimtar + Pajtimtar me Email + Ende pa pajtimtarë me email + Ende pa pajtimtarë + Pajtimtarë Me Email + Pajtimtarë + %s: I pajtuar tashmë + S’u hoq dot pajtimtari + S’arrihet të shtohet te kalendar + Pajtime te sajti + Pajtimtarë + Pajtimtarë + Ende pa pajtimtarë + Email-e + Pajtimtarë + Pajtimtarë Gjithsej + Klikime + Hapje + Pajtimtar që prej + Emër + Pajtimtarë + Pajtimtar + Po përditësohet lëndë + Pajtimtarë + Sajte Së Fundi + Krejt Sajtet + Sajte të Fiksuar + Përpunoni Fiksime + Pajisje Tjetër + Pajisja e Tanishme + Postimi u ndryshua në tjetër pajisje. Ju lutemi, përzgjidhni versionin e postimit që duhet mbajtur. + Zgjidheni Përplasjen + Ekstra të mëdha + Të mëdha + Normale + Të vogla + Ekstra të vogla + Madhësi Shkronjash + Shkronja + Skemë Ngjyrash + jepni përshtypjet tuaja + <Eksperimentale> Hiqe ngjyrën e përzgjedhur Pa etiketa të ndjekura E ndiqni tashmë këtë etiketë. Parapëlqime Leximi Etiketa të ndjekura + Kallamsheqeri h4x0r OLED Mbrëmje @@ -31,6 +89,7 @@ Language: sq_AL Kopjo hollësi gabimi Buton për “Kopjo tekst postimi” Buton për “Kopjo hollësi gabimi” + S’u arrit të përditësohet lëndë Legjendë videoje. %s Legjendë videoje. E zbrazët Përpunoni video @@ -516,8 +575,6 @@ Language: sq_AL Lexuesi po kalohet te aplikacioni Jetpack Statistikat tuaja po kalojnë te aplikacioni Jetpack Kaloni në aplikacionin e ri Jetpack - Kontrolloni lidhjen tuaj në rrjet dhe riprovoni. - S’arrihet të ngarkohet kjo lëndë këtë çast Pati një gabim në ngarkim cytjesh. Hëm Ende pa cytje @@ -642,6 +699,7 @@ Language: sq_AL Skanoni vetëm kode QR të marrë drejt e nga shfletuesi juaj. Mos skanoni kurrë një kod të dërguar për ju nga dikush tjetër. Po provoni të hyni në shfletuesin tuaj pranë %1$s? A po provoni të hyni te %1$s pranë %2$s? + 💡Komentimi në blogje të tjerë është një mënyrë e goditur për të shtuar vëmendjen dhe pajtimtarë për sajtin tuaj të ri. 💡Prekni “SHIHNI MË TEPËR”, që të shihni komentuesit tuaj kryesues. Shihni prapë, kur të keni botuar postimin tuaj të parë! Shihni ndihmëzat tona kryesuese për shtim të parjeve dhe trafikut %1$s @@ -679,6 +737,7 @@ Language: sq_AL Skanoni Kod Hyrje ⭐️ Postimi juaj më i ri %1$s ka marrë %2$s pëlqime. Pa veprimtari të mjaftueshme. Kontrolloni më vonë, kur sajti juaj të ketë më tepër vizitorë! + %1$s, %2$s%% e pajtimtarëve gjithsej %1$s (%2$s%%) Kopjoji lidhjen Përgëzime! Dini nga shkoni<br/> @@ -3033,6 +3092,7 @@ Language: sq_AL Krejt ngarkimet e mediave u anuluan për shkak të një gabimi të panjohur. Ju lutemi, riprovoni t’i ringarkoni Format i panjohur postimesh Parashtroje + Pajtimtar U pikas një sajt i përsëdytur. Ky sajt ekziston tashmë në aplikacion, s’mund ta shtoni. Jeni tashmë i futur në një llogari WordPress.com, s’mund të shtoni një sajt WordPress.com të lidhur pas një llogarie tjetër. @@ -3098,6 +3158,7 @@ Language: sq_AL Ekipi Ftoni deri në 10 adresa email dhe/ose emra përdoruesish WordPress.com. Atyre që kanë nevojë për emër përdoruesi, do t’u dërgohen udhëzime se si të krijojnë një të tillë. Nëse e hiqni këtë parës, ai ose ajo s’do të jetë në gjendje ta vizitojë këtë sajt.\n\nDo të donit ta hiqnit këtë parës, sido qoftë? + Në u heqtë, ky pajtimtar do të reshtë së marri njoftime mbi këtë sajt, veç në u ripajtofshin.\n\nDo të donit ta hiqnit këtë pajtimar? Që prej %1$s S’u hoq dot shikuesi Disa ngarkime mediash dështuan. S’mund të kaloni nën mënyrën HTML\n në këtë gjendje. Të hiqen krejt ngarkimet e dështuara dhe të vazhdohet? @@ -3431,6 +3492,7 @@ Language: sq_AL \"%s\" s’u fsheh, ngaqë është sajti i tanishëm Ndërtoni një sajt te WordPress.com Shto sajt të vetëstrehuar + Shtoni një sajt Shfaq/fshih sajte Zgjidhni një sajt Shihni Sajtin diff --git a/WordPress/src/main/res/values-sv/strings.xml b/WordPress/src/main/res/values-sv/strings.xml index a8e8ba7ff3f9..a4ba1b338461 100644 --- a/WordPress/src/main/res/values-sv/strings.xml +++ b/WordPress/src/main/res/values-sv/strings.xml @@ -1,11 +1,33 @@ + Starta om + Uppdatering nedladdad. Starta om för att tillämpa. + Inlägg via ljud + öppna meny + ta bort gillamarkering för inlägg + gilla inlägg + öppna blogg + öppna inlägg + Försök igen + Vi kunde för tillfället inte hitta några inlägg med etiketten %s + Vi kunde för tillfället inte läsa in inläggen från den här etiketten + Inga inlägg hittades för %s + Mer från %s + Etiketter + Välj färger och typsnitt som passar dig. När du läser ett inlägg, tryck på AA-ikonen längst upp på skärmen. + Läsinställningar + Tryck på rullgardinsmenyn längst upp och välj Etiketter för att komma åt strömmar från etiketter du följer. + Etikettström + Nytt i Läsaren + Dina etiketter + Kontrollera din nätverksanslutning och försök igen. + Det går inte att ladda detta innehåll för tillfället Prenumeranter Prenumerant Prenumeranttillväxt @@ -582,8 +604,6 @@ Language: sv_SE Läsaren flyttar till Jetpack-appen Din statistik flyttas till Jetpack-appen Byt till den nya Jetpack-appen - Kontrollera din nätverksanslutning och försök igen. - Det går inte att ladda detta innehåll just nu Det var ett problem att ladda in förslag. Hoppsan Inga förslag än diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml index bd08e00bf72e..1b62546dfc9d 100644 --- a/WordPress/src/main/res/values-tr/strings.xml +++ b/WordPress/src/main/res/values-tr/strings.xml @@ -1,11 +1,36 @@ + Yeniden Başla + Güncelleme indirildi. Uygulamak için yeniden başlayın. + Sesten gönderi oluştur + menüyü aç + gönderideki beğeniyi kaldır + gönderiyi beğen + blogu aç + gönderiyi aç + Tekrar dene + Şu anda %s olarak etiketlenmiş gönderi bulunamadı + Şu anda bu etiketten gönderiler yüklenemiyor + %s etiketli hiç yazı bulunamadı + %s etiketli daha fazla içerik + Etiketler + Size uygun renkler ve yazı tipleri. Bir gönderiyi okurken ekranın üst kısmındaki AA simgesine dokunun. + Okuma Tercihleri + Üstteki açılır menüye dokunun ve takip ettiğiniz etiketlerin akışına erişmek için Etiketler seçeneğini belirleyin. + Etiket akışı + Okuyucuda Yeni + Etiketleriniz + Ağ bağlantınızı kontrol edip tekrar deneyin. + Bu içerik şu an yüklenemiyor + Aboneler + Abone + Abone Artışı Abone E-posta Abonesi Henüz e-posta abonesi yok @@ -65,12 +90,15 @@ Language: tr Yazı tipi Renk Düzeni geri bildiriminizi gönderin + <Experimental> Seçilen rengi temizle Takip edilen etiket yok Bu etiketi zaten takip ediyorsunuz Okuma Tercihleri Takip edilen etiketler Şeker + h4x0r + OLED Akşam Sepya Yumuşak @@ -576,8 +604,6 @@ Language: tr Okuyucu, Jetpack uygulamasına taşınıyor İstatistikleriniz Jetpack uygulamasına taşınıyor Yeni Jetpack uygulamasına geçiş yapın - Ağ bağlantınızı denetleyip yeniden deneyin. - Bu içerik şu an yüklenemiyor İstemler yüklenirken bir sorun çıktı. Eyvah Henüz bir istem yok @@ -3102,6 +3128,7 @@ Language: tr Bilinmeyen bir sorun nedeniyle tüm ortam yüklemeleri iptal edildi. Lütfen yeniden yüklemeyi deneyin Bilinmeyen yazı biçimi Gönder + Abone Bir çift site bulundu. Bu site uygulamada zaten var. Yeniden ekleyemezsiniz. Zaten WordPress.com hesabınızda oturum açmış durumdasınız. Başka bir hesapla ilişkili WordPress.com hesabını ekleyemezsiniz. diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml index c36991cb5778..9cea2010fc4b 100644 --- a/WordPress/src/main/res/values-zh-rCN/strings.xml +++ b/WordPress/src/main/res/values-zh-rCN/strings.xml @@ -1,11 +1,36 @@ + 重新启动 + 更新内容已下载。 重新启动以应用修改。 + 从音频创建的文章 + 打开菜单 + 删除文章点赞 + 点赞文章 + 打开博客 + 打开文章 + 重试 + 我们现在无法找到任何标签为 %s 的文章 + 我们现在无法加载此标签下的文章 + 未找到关于 %s 的文章 + 更多来自 %s + 标签 + 选择适合您的颜色和字体。 阅读文章时,请点击屏幕顶部的 AA 图标。 + 阅读偏好 + 点击顶部的下拉菜单,然后选择“标签”,即可访问您所关注的标签下的数据流。 + 标签流 + 阅读器新功能 + 您的标签 + 请检查您的网络连接,然后重试。 + 现在无法加载此内容 + 订阅者 + 订阅者 + 订阅者增长情况 订阅者 电子邮件订阅者 尚无电子邮件订阅者 @@ -570,8 +595,6 @@ Language: zh_CN 阅读器即将移至 Jetpack 应用 统计信息即将移至 Jetpack 应用 切换到新版 Jetpack 应用 - 请检查您的网络连接,然后重试。 - 现在无法加载此内容 加载提示时出错。 糟糕 尚无提示 @@ -3090,6 +3113,7 @@ Language: zh_CN 由于发生未知错误,所有媒体上传均已取消。请重新尝试上传 未知文章格式 提交 + 订阅者 检测到重复的站点。 应用中已存在此站点,无法重复添加。 您已登录 WordPress.com 账户,不能添加被其他账户绑定的 WordPress.com 站点。 diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml index be345f681a93..e785f45a7d65 100644 --- a/WordPress/src/main/res/values-zh-rHK/strings.xml +++ b/WordPress/src/main/res/values-zh-rHK/strings.xml @@ -1,11 +1,36 @@ + 重新開始 + 已下載更新。 重新啟動以套用。 + 從音訊張貼 + 開啟選單 + 從文章移除「讚」 + 對文章按讚 + 開啟網誌 + 開啟文章 + 重試 + 我們目前找不到加上「%s」標籤的文章 + 我們目前無法從此標籤載入文章 + 找不到與「%s」相符的文章 + 更多來自「%s」的項目 + 標籤 + 選擇適合你的顏色和字型。 閱讀文章時,點選畫面頂端的 AA 圖示。 + 閱讀喜好 + 點選頂端的下拉式清單,然後選取標籤,存取與你關注的標籤相關的文章串。 + 標籤串 + 閱讀器新功能 + 你的標籤 + 請檢查你的網路連線並再試一次。 + 目前無法載入此內容 + 訂閱者 + 訂閱者 + 訂閱者的成長狀況 訂閱者 電子郵件訂閱者 目前沒有電子郵件訂閱者 @@ -572,8 +597,6 @@ Language: zh_TW 「閱讀器」將轉移至 Jetpack 應用程式 你的統計資料將轉移至 Jetpack 應用程式 切換至全新 Jetpack 應用程式 - 請檢查你的網路連線並再試一次。 - 目前無法載入此內容 載入提示時發生錯誤。 糟糕 尚無提示 diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml index be345f681a93..e785f45a7d65 100644 --- a/WordPress/src/main/res/values-zh-rTW/strings.xml +++ b/WordPress/src/main/res/values-zh-rTW/strings.xml @@ -1,11 +1,36 @@ + 重新開始 + 已下載更新。 重新啟動以套用。 + 從音訊張貼 + 開啟選單 + 從文章移除「讚」 + 對文章按讚 + 開啟網誌 + 開啟文章 + 重試 + 我們目前找不到加上「%s」標籤的文章 + 我們目前無法從此標籤載入文章 + 找不到與「%s」相符的文章 + 更多來自「%s」的項目 + 標籤 + 選擇適合你的顏色和字型。 閱讀文章時,點選畫面頂端的 AA 圖示。 + 閱讀喜好 + 點選頂端的下拉式清單,然後選取標籤,存取與你關注的標籤相關的文章串。 + 標籤串 + 閱讀器新功能 + 你的標籤 + 請檢查你的網路連線並再試一次。 + 目前無法載入此內容 + 訂閱者 + 訂閱者 + 訂閱者的成長狀況 訂閱者 電子郵件訂閱者 目前沒有電子郵件訂閱者 @@ -572,8 +597,6 @@ Language: zh_TW 「閱讀器」將轉移至 Jetpack 應用程式 你的統計資料將轉移至 Jetpack 應用程式 切換至全新 Jetpack 應用程式 - 請檢查你的網路連線並再試一次。 - 目前無法載入此內容 載入提示時發生錯誤。 糟糕 尚無提示 diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 2a3f1524f47d..ab87e5892b99 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -3121,6 +3121,7 @@ Photos and videos & Music and audio Camera Microphone + Media Location Audio Recording Permission Required To record audio, this app needs permission to access your microphone. You have previously denied this permission. Please enable the microphone permission in the app settings to use this feature. + Tap to edit diff --git a/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt b/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt index c54ba84bfd1e..3a22f188297f 100644 --- a/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt @@ -12,6 +12,7 @@ import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.model.AppUpdateType import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability +import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -82,6 +83,7 @@ class InAppUpdateManagerImplTest { inAppUpdateManager = InAppUpdateManagerImpl( applicationContext, + TestScope(), appUpdateManager, remoteConfigWrapper, buildConfigWrapper, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTrackerTest.kt new file mode 100644 index 000000000000..e2cec7f1df9f --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/main/analytics/MainCreateSheetTrackerTest.kt @@ -0,0 +1,276 @@ +package org.wordpress.android.ui.main.analytics + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.argThat +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.main.MainActionListItem +import org.wordpress.android.ui.main.WPMainNavigationView +import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper + +@RunWith(MockitoJUnitRunner::class) +class MainCreateSheetTrackerTest { + @Mock + private lateinit var analyticsTracker: AnalyticsTrackerWrapper + + private lateinit var tracker: MainCreateSheetTracker + + @Before + fun setUp() { + tracker = MainCreateSheetTracker(analyticsTracker) + } + + // region trackActionTapped + @Test + fun `trackActionTapped tracks action tapped for my site page`() { + // Arrange + val page = WPMainNavigationView.PageType.MY_SITE + val actionType = MainActionListItem.ActionType.CREATE_NEW_POST + val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ACTION_TAPPED + + // Act + tracker.trackActionTapped(page, actionType) + + // Assert + verify(analyticsTracker).track(eq(expectedStat), argThat> { + this["action"] == "create_new_post" + }) + } + + @Test + fun `trackActionTapped tracks action tapped for reader page`() { + // Arrange + val page = WPMainNavigationView.PageType.READER + val actionType = MainActionListItem.ActionType.CREATE_NEW_POST + val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_ACTION_TAPPED + + // Act + tracker.trackActionTapped(page, actionType) + + // Assert + verify(analyticsTracker).track(eq(expectedStat), argThat> { + this["action"] == "create_new_post" + }) + } + + @Test + fun `trackActionTapped does not track action tapped for other pages`() { + WPMainNavigationView.PageType.entries + .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER } + .forEach { page -> + // Arrange + val actionType = MainActionListItem.ActionType.CREATE_NEW_POST + + // Act + tracker.trackActionTapped(page, actionType) + + // Assert + verifyNoInteractions(analyticsTracker) + } + } + // endregion + + // region trackAnswerPromptActionTapped + @Test + fun `trackAnswerPromptActionTapped tracks answer prompt action tapped for my site page`() { + // Arrange + val page = WPMainNavigationView.PageType.MY_SITE + val attribution = BloggingPromptAttribution.DAY_ONE + val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED + + // Act + tracker.trackAnswerPromptActionTapped(page, attribution) + + // Assert + verify(analyticsTracker).track(eq(expectedStat), argThat> { + this["attribution"] == attribution.value + }) + } + + @Test + fun `trackAnswerPromptActionTapped tracks answer prompt action tapped for reader page`() { + // Arrange + val page = WPMainNavigationView.PageType.READER + val attribution = BloggingPromptAttribution.DAY_ONE + val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED + + // Act + tracker.trackAnswerPromptActionTapped(page, attribution) + + // Assert + verify(analyticsTracker).track(eq(expectedStat), argThat> { + this["attribution"] == attribution.value + }) + } + + @Test + fun `trackAnswerPromptActionTapped does not track answer prompt action tapped for other pages`() { + WPMainNavigationView.PageType.entries + .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER } + .forEach { page -> + // Arrange + val attribution = BloggingPromptAttribution.DAY_ONE + + // Act + tracker.trackAnswerPromptActionTapped(page, attribution) + + // Assert + verifyNoInteractions(analyticsTracker) + } + } + // endregion + + // region trackHelpPromptActionTapped + @Test + fun `trackHelpPromptActionTapped tracks help prompt action tapped for my site page`() { + // Arrange + val page = WPMainNavigationView.PageType.MY_SITE + val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED + + // Act + tracker.trackHelpPromptActionTapped(page) + + // Assert + verify(analyticsTracker).track(expectedStat) + } + + @Test + fun `trackHelpPromptActionTapped tracks help prompt action tapped for reader page`() { + // Arrange + val page = WPMainNavigationView.PageType.READER + val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_PROMPT_HELP_TAPPED + + // Act + tracker.trackHelpPromptActionTapped(page) + + // Assert + verify(analyticsTracker).track(expectedStat) + } + + @Test + fun `trackHelpPromptActionTapped does not track help prompt action tapped for other pages`() { + WPMainNavigationView.PageType.entries + .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER } + .forEach { page -> + // Act + tracker.trackHelpPromptActionTapped(page) + + // Assert + verifyNoInteractions(analyticsTracker) + } + } + // endregion + + // region trackSheetShown + @Test + fun `trackSheetShown tracks sheet shown for my site page`() { + // Arrange + val page = WPMainNavigationView.PageType.MY_SITE + val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_SHEET_SHOWN + + // Act + tracker.trackSheetShown(page) + + // Assert + verify(analyticsTracker).track(expectedStat) + } + + @Test + fun `trackSheetShown tracks sheet shown for reader page`() { + // Arrange + val page = WPMainNavigationView.PageType.READER + val expectedStat = AnalyticsTracker.Stat.READER_CREATE_SHEET_SHOWN + + // Act + tracker.trackSheetShown(page) + + // Assert + verify(analyticsTracker).track(expectedStat) + } + + @Test + fun `trackSheetShown does not track sheet shown for other pages`() { + WPMainNavigationView.PageType.entries + .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER } + .forEach { page -> + // Act + tracker.trackSheetShown(page) + + // Assert + verifyNoInteractions(analyticsTracker) + } + } + // endregion + + // region trackFabShown + @Test + fun `trackFabShown tracks fab shown for my site page`() { + // Arrange + val page = WPMainNavigationView.PageType.MY_SITE + val expectedStat = AnalyticsTracker.Stat.MY_SITE_CREATE_FAB_SHOWN + + // Act + tracker.trackFabShown(page) + + // Assert + verify(analyticsTracker).track(expectedStat) + } + + @Test + fun `trackFabShown tracks fab shown for reader page`() { + // Arrange + val page = WPMainNavigationView.PageType.READER + val expectedStat = AnalyticsTracker.Stat.READER_CREATE_FAB_SHOWN + + // Act + tracker.trackFabShown(page) + + // Assert + verify(analyticsTracker).track(expectedStat) + } + + @Test + fun `trackFabShown does not track fab shown for other pages`() { + WPMainNavigationView.PageType.entries + .filterNot { it == WPMainNavigationView.PageType.MY_SITE || it == WPMainNavigationView.PageType.READER } + .forEach { page -> + // Act + tracker.trackFabShown(page) + + // Assert + verifyNoInteractions(analyticsTracker) + } + } + // endregion + + // region trackCreateActionsSheetCard + @Test + fun `trackCreateActionsSheetCard tracks bottom sheet when it is in the list`() { + val actionList = listOf( + mock(), + mock(), + mock(), + ) + tracker.trackCreateActionsSheetCard(actionList) + verify(analyticsTracker).track(AnalyticsTracker.Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED) + } + + @Test + fun `trackCreateActionsSheetCard does not track bottom sheet when it is not in the list`() { + val actionList = listOf( + mock(), + mock(), + ) + tracker.trackCreateActionsSheetCard(actionList) + verifyNoInteractions(analyticsTracker) + } + // endregion +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelperTest.kt new file mode 100644 index 000000000000..fe473441fcfd --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/main/utils/MainCreateSheetHelperTest.kt @@ -0,0 +1,260 @@ +package org.wordpress.android.ui.main.utils + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.mockito.Mock +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.main.WPMainNavigationView.PageType +import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils +import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.SiteUtilsWrapper +import org.wordpress.android.util.config.ReaderFloatingButtonFeatureConfig +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class MainCreateSheetHelperTest : BaseUnitTest() { + @Mock + private lateinit var voiceToContentFeatureUtils: VoiceToContentFeatureUtils + + @Mock + private lateinit var readerFloatingButtonFeatureConfig: ReaderFloatingButtonFeatureConfig + + @Mock + private lateinit var bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper + + @Mock + private lateinit var buildConfig: BuildConfigWrapper + + @Mock + private lateinit var siteUtils: SiteUtilsWrapper + + private lateinit var helper: MainCreateSheetHelper + + @Before + fun setUp() { + helper = MainCreateSheetHelper( + voiceToContentFeatureUtils, + readerFloatingButtonFeatureConfig, + bloggingPromptsSettingsHelper, + buildConfig, + siteUtils, + ) + } + + // region shouldShowFabForPage + @Test + fun `shouldShowFabForPage returns true for my site page`() { + // Arrange + val page = PageType.MY_SITE + whenever(buildConfig.isCreateFabEnabled).thenReturn(true) + + // Act + val result = helper.shouldShowFabForPage(page) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `shouldShowFabForPage returns true for reader page when reader floating button feature is enabled`() { + // Arrange + val page = PageType.READER + whenever(buildConfig.isCreateFabEnabled).thenReturn(true) + whenever(readerFloatingButtonFeatureConfig.isEnabled()).thenReturn(true) + + // Act + val result = helper.shouldShowFabForPage(page) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `shouldShowFabForPage returns false for reader page when reader floating button feature is disabled`() { + // Arrange + val page = PageType.READER + whenever(buildConfig.isCreateFabEnabled).thenReturn(true) + whenever(readerFloatingButtonFeatureConfig.isEnabled()).thenReturn(false) + + // Act + val result = helper.shouldShowFabForPage(page) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `shouldShowFabForPage returns false for my site page when create fab is disabled`() { + // Arrange + val page = PageType.MY_SITE + whenever(buildConfig.isCreateFabEnabled).thenReturn(false) + + // Act + val result = helper.shouldShowFabForPage(page) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `shouldShowFabForPage returns false for reader page when create fab is disabled`() { + // Arrange + val page = PageType.READER + whenever(buildConfig.isCreateFabEnabled).thenReturn(false) + whenever(readerFloatingButtonFeatureConfig.isEnabled()).thenReturn(true) + + // Act + val result = helper.shouldShowFabForPage(page) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `shouldShowFabForPage returns false for other pages`() { + PageType.entries + .filterNot { it == PageType.MY_SITE || it == PageType.READER } + .forEach { page -> + // Arrange + whenever(buildConfig.isCreateFabEnabled).thenReturn(true) + + // Act + val result = helper.shouldShowFabForPage(page) + + // Assert + assertThat(result).isFalse() + } + } + // endregion + + // region canCreatePost + @Test + fun `canCreatePost returns true`() { + // Act + val result = helper.canCreatePost() + + // Assert + assertThat(result).isTrue() + } + // endregion + + // region canCreatePage + @Test + fun `canCreatePage returns true for my site page with full access to content`() { + // Arrange + val site = SiteModel() + val page = PageType.MY_SITE + whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(true) + + // Act + val result = helper.canCreatePage(site, page) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `canCreatePage returns false for my site page without full access to content`() { + // Arrange + val site = SiteModel() + val page = PageType.MY_SITE + whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(false) + + // Act + val result = helper.canCreatePage(site, page) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `canCreatePage returns false for other pages with full access to content`() { + PageType.entries + .filterNot { it == PageType.MY_SITE } + .forEach { page -> + // Arrange + val site = SiteModel() + whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(true) + + // Act + val result = helper.canCreatePage(site, page) + + // Assert + assertThat(result).isFalse() + } + } + // endregion + + // region canCreatePostFromAudio + @Test + fun `canCreatePostFromAudio returns true when voice to content is enabled and site has full access to content`() { + // Arrange + val site = SiteModel() + whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) + whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(true) + + // Act + val result = helper.canCreatePostFromAudio(site) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `canCreatePostFromAudio returns false when voice to content is disabled`() { + // Arrange + val site = SiteModel() + whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(false) + + // Act + val result = helper.canCreatePostFromAudio(site) + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `canCreatePostFromAudio returns false when site does not have full access to content`() { + // Arrange + val site = SiteModel() + whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(true) + whenever(siteUtils.hasFullAccessToContent(site)).thenReturn(false) + + // Act + val result = helper.canCreatePostFromAudio(site) + + // Assert + assertThat(result).isFalse() + } + // endregion + + // region canCreatePromptAnswer + @Test + fun `canCreatePromptAnswer returns true when prompts feature should be shown`() = test { + // Arrange + whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true) + + // Act + val result = helper.canCreatePromptAnswer() + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `canCreatePromptAnswer returns false when prompts feature should not be shown`() = test { + // Arrange + whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false) + + // Act + val result = helper.canCreatePromptAnswer() + + // Assert + assertThat(result).isFalse() + } + // endregion +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsViewModelSliceTest.kt index 90daaad5caaa..a53d61c8df06 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsViewModelSliceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsViewModelSliceTest.kt @@ -106,6 +106,7 @@ private const val DEVICE_ID_PARAM = "device_id_param" private const val IDENTIFIER_PARAM = "identifier_param" private const val MARKETING_VERSION_PARAM = "marketing_version_param" private const val PLATFORM_PARAM = "android" +private const val ANDROID_VERSION_PARAM = "14.0" /* MODEL */ @@ -296,7 +297,8 @@ class CardsViewModelSliceTest : BaseUnitTest() { DEVICE_ID_PARAM, IDENTIFIER_PARAM, MARKETING_VERSION_PARAM, - PLATFORM_PARAM + PLATFORM_PARAM, + ANDROID_VERSION_PARAM, ) viewModelSlice.initialize(testScope()) @@ -330,6 +332,7 @@ class CardsViewModelSliceTest : BaseUnitTest() { whenever(buildConfigWrapper.getAppVersionCode()).thenReturn(BUILD_NUMBER_PARAM.toInt()) whenever(buildConfigWrapper.getApplicationId()).thenReturn(IDENTIFIER_PARAM) whenever(buildConfigWrapper.getAppVersionName()).thenReturn(MARKETING_VERSION_PARAM) + whenever(buildConfigWrapper.androidVersion).thenReturn(ANDROID_VERSION_PARAM) whenever(preferenceUtilsWrapper.getFluxCPreferences()).thenReturn(sharedPreferences) whenever(sharedPreferences.getString(any(), anyOrNull())).thenReturn(DEVICE_ID_PARAM) } @@ -500,7 +503,8 @@ class CardsViewModelSliceTest : BaseUnitTest() { DEVICE_ID_PARAM, IDENTIFIER_PARAM, MARKETING_VERSION_PARAM, - PLATFORM_PARAM + PLATFORM_PARAM, + ANDROID_VERSION_PARAM, ) whenever(cardsStore.getCards(siteModel)).thenReturn(flowOf(CardsResult())) whenever(cardsStore.fetchCards(fetchCardsPayload)).thenReturn(apiError) @@ -522,7 +526,8 @@ class CardsViewModelSliceTest : BaseUnitTest() { DEVICE_ID_PARAM, IDENTIFIER_PARAM, MARKETING_VERSION_PARAM, - PLATFORM_PARAM + PLATFORM_PARAM, + ANDROID_VERSION_PARAM, ) whenever(cardsStore.getCards(siteModel)).thenReturn(flowOf(data)) whenever(cardsStore.fetchCards(fetchCardsPayload)).thenReturn(success) @@ -558,7 +563,8 @@ class CardsViewModelSliceTest : BaseUnitTest() { DEVICE_ID_PARAM, IDENTIFIER_PARAM, MARKETING_VERSION_PARAM, - PLATFORM_PARAM + PLATFORM_PARAM, + ANDROID_VERSION_PARAM, ) whenever(cardsStore.getCards(siteModel)).thenReturn(flowOf(data)) whenever(cardsStore.fetchCards(fetchCardsPayload)).thenReturn(success) @@ -581,7 +587,8 @@ class CardsViewModelSliceTest : BaseUnitTest() { DEVICE_ID_PARAM, IDENTIFIER_PARAM, MARKETING_VERSION_PARAM, - PLATFORM_PARAM + PLATFORM_PARAM, + ANDROID_VERSION_PARAM, ) whenever(cardsStore.getCards(siteModel)).thenReturn(flowOf(data)) whenever(cardsStore.fetchCards(fetchCardsPayload)).thenReturn(success) @@ -638,7 +645,8 @@ class CardsViewModelSliceTest : BaseUnitTest() { DEVICE_ID_PARAM, IDENTIFIER_PARAM, MARKETING_VERSION_PARAM, - PLATFORM_PARAM + PLATFORM_PARAM, + ANDROID_VERSION_PARAM, ) ) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListFeaturedImageTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListFeaturedImageTrackerTest.kt new file mode 100644 index 000000000000..a9616c3fd5b1 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListFeaturedImageTrackerTest.kt @@ -0,0 +1,111 @@ +package org.wordpress.android.ui.posts + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.MediaStore +import kotlin.test.Test + +@Suppress("UNCHECKED_CAST") +@ExperimentalCoroutinesApi +class PostListFeaturedImageTrackerTest : BaseUnitTest() { + private val dispatcher: Dispatcher = mock() + private val mediaStore: MediaStore = mock() + + private lateinit var tracker: PostListFeaturedImageTracker + + private val site = SiteModel().apply { id = 123 } + + @Before + fun setup() { + tracker = PostListFeaturedImageTracker(dispatcher, mediaStore) + } + + @Test + fun `given id exists in map, when getFeaturedImageUrl invoked, then return url`() { + val imageId = 123L + val imageUrl = "https://example.com/image.jpg" + tracker.featuredImageMap[imageId] = imageUrl + + val result = tracker.getFeaturedImageUrl(site, imageId) + + assertEquals(imageUrl, result) + } + + @Test + fun `given id is 0, when getFeaturedImageUrl invoked, then return null`() { + val result = tracker.getFeaturedImageUrl(site, 0L) + + assertNull(result) + } + + @Test + fun `given id not in map and exists in store, when invoked, then return url from media store`() { + val imageId = 456L + val imageUrl = "https://example.com/image.jpg" + val mediaModel = MediaModel(site.id, imageId).apply { + url = imageUrl + } + + whenever(mediaStore.getSiteMediaWithId(site, imageId)).thenReturn(mediaModel) + + val result = tracker.getFeaturedImageUrl(site, imageId) + + assertEquals(imageUrl, result) + assertEquals(imageUrl, tracker.featuredImageMap[imageId]) + } + + @Test + fun `given id not in map or store, when invoked, then return null and dispatch fetch request`() { + val imageId = 123L + + whenever(mediaStore.getSiteMediaWithId(site, imageId)).thenReturn(null) + + val result = tracker.getFeaturedImageUrl(site, imageId) + + assertNull(result) + verify(dispatcher).dispatch(any()) + assert(tracker.ongoingRequests.contains(imageId)) + } + + @Test + fun `given request ongoing for id, when invoked, should return null`() { + val imageId = 123L + + tracker.ongoingRequests.add(imageId) + + val result = tracker.getFeaturedImageUrl(site, imageId) + + assertNull(result) + verify(mediaStore, never()).getSiteMediaWithId(site, imageId) + verify(dispatcher, never()).dispatch(any()) + } + + @Test + fun `given id in map and ongoingRequests, when invalidate, then remove id from map and ongoingRequests`() { + val imageId1 = 123L + val imageId2 = 456L + + tracker.featuredImageMap[imageId1] = "https://example.com/image1.jpg" + tracker.featuredImageMap[imageId2] = "https://example.com/image2.jpg" + tracker.ongoingRequests.add(imageId1) + tracker.ongoingRequests.add(imageId2) + + tracker.invalidateFeaturedMedia(listOf(imageId1, imageId2)) + + assertNull(tracker.featuredImageMap[imageId1]) + assertNull(tracker.featuredImageMap[imageId2]) + assert(!tracker.ongoingRequests.contains(imageId1)) + assert(!tracker.ongoingRequests.contains(imageId2)) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt index 68f9e62a6174..cdfb4346c734 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt @@ -20,7 +20,6 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest -import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.analytics.AnalyticsTracker.Stat.FEATURE_ANNOUNCEMENT_SHOWN_ON_APP_UPGRADE import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.bloggingprompts.BloggingPromptModel @@ -35,21 +34,23 @@ import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore.BloggingPromptsResult -import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper import org.wordpress.android.ui.main.MainActionListItem.ActionType.ANSWER_BLOGGING_PROMPT import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_PAGE import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_POST +import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_POST_FROM_AUDIO import org.wordpress.android.ui.main.MainActionListItem.ActionType.NO_ACTION import org.wordpress.android.ui.main.MainActionListItem.AnswerBloggingPromptAction import org.wordpress.android.ui.main.MainActionListItem.CreateAction import org.wordpress.android.ui.main.MainFabUiState +import org.wordpress.android.ui.main.WPMainNavigationView.PageType +import org.wordpress.android.ui.main.analytics.MainCreateSheetTracker +import org.wordpress.android.ui.main.utils.MainCreateSheetHelper import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent import org.wordpress.android.ui.quickstart.QuickStartType -import org.wordpress.android.ui.voicetocontent.VoiceToContentFeatureUtils import org.wordpress.android.ui.whatsnew.FeatureAnnouncement import org.wordpress.android.ui.whatsnew.FeatureAnnouncementItem import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider @@ -95,9 +96,6 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Mock lateinit var siteStore: SiteStore - @Mock - lateinit var bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper - @Mock lateinit var bloggingPromptsStore: BloggingPromptsStore @@ -111,7 +109,10 @@ class WPMainActivityViewModelTest : BaseUnitTest() { private lateinit var shouldAskPrivacyConsent: ShouldAskPrivacyConsent @Mock - private lateinit var voiceToContentFeatureUtils: VoiceToContentFeatureUtils + private lateinit var mainCreateSheetHelper: MainCreateSheetHelper + + @Mock + private lateinit var mainCreateSheetTracker: MainCreateSheetTracker private val featureAnnouncement = FeatureAnnouncement( "14.7", @@ -157,10 +158,10 @@ class WPMainActivityViewModelTest : BaseUnitTest() { activeTask = MutableLiveData() externalFocusPointEvents = mutableListOf() whenever(quickStartRepository.activeTask).thenReturn(activeTask) - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false) whenever(bloggingPromptsStore.getPromptForDate(any(), any())).thenReturn(flowOf(bloggingPrompt)) whenever(shouldAskPrivacyConsent()).thenReturn(false) - whenever(voiceToContentFeatureUtils.isVoiceToContentEnabled()).thenReturn(false) + whenever(mainCreateSheetHelper.canCreatePost()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(false) viewModel = WPMainActivityViewModel( featureAnnouncementProvider, buildConfigWrapper, @@ -170,11 +171,11 @@ class WPMainActivityViewModelTest : BaseUnitTest() { selectedSiteRepository, accountStore, siteStore, - bloggingPromptsSettingsHelper, bloggingPromptsStore, - NoDelayCoroutineDispatcher(), shouldAskPrivacyConsent, - voiceToContentFeatureUtils + mainCreateSheetHelper, + mainCreateSheetTracker, + NoDelayCoroutineDispatcher(), ) viewModel.onFeatureAnnouncementRequested.observeForever( onFeatureAnnouncementRequestedObserver @@ -195,73 +196,41 @@ class WPMainActivityViewModelTest : BaseUnitTest() { /* FAB VISIBILITY */ @Test - fun `given fab enabled, when page changed to my site, then fab is visible`() { + fun `given fab enabled and page changed to supported page, then fab is visible`() { startViewModelWithDefaultParameters() + whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(true) - viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) + viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) assertThat(fabUiState?.isFabVisible).isTrue } @Test - fun `given fab enabled, when page changed away from my site, then fab is hidden`() { + fun `given fab disabled or page changed to non-supported page, then fab is hidden`() { startViewModelWithDefaultParameters() + whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(false) - viewModel.onPageChanged(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true)) + viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) - assertThat(fabUiState?.isFabVisible).isFalse + assertThat(fabUiState?.isFabVisible).isFalse() } @Test - fun `given fab enabled, when my site page is resumed, then fab is visible`() { + fun `given fab enabled and supported page is resumed, then fab is visible`() { startViewModelWithDefaultParameters() + whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(true) - viewModel.onResume(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) + viewModel.onResume(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) assertThat(fabUiState?.isFabVisible).isTrue } @Test - fun `given fab enabled, when non my site page is resumed, then fab is hidden`() { + fun `given fab disabled or non-supported page is resumed, then fab is hidden`() { startViewModelWithDefaultParameters() + whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(false) - viewModel.onResume(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true)) - - assertThat(fabUiState?.isFabVisible).isFalse - } - - @Test - fun `given fab disabled, when page changed to my site, then fab is hidden`() { - startViewModelWithDefaultParameters(isCreateFabEnabled = false) - - viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) - - assertThat(fabUiState?.isFabVisible).isFalse - } - - @Test - fun `given fab disabled, when page changed away from my site, then fab is hidden`() { - startViewModelWithDefaultParameters(isCreateFabEnabled = false) - - viewModel.onPageChanged(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true)) - - assertThat(fabUiState?.isFabVisible).isFalse - } - - @Test - fun `given fab disabled, when my site page is resumed, then fab is hidden`() { - startViewModelWithDefaultParameters(isCreateFabEnabled = false) - - viewModel.onResume(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) - - assertThat(fabUiState?.isFabVisible).isFalse - } - - @Test - fun `given fab disabled, when non my site page is resumed, then fab is hidden`() { - startViewModelWithDefaultParameters(isCreateFabEnabled = false) - - viewModel.onResume(isOnMySitePageWithValidSite = false, site = initSite(hasFullAccessToContent = true)) + viewModel.onResume(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) assertThat(fabUiState?.isFabVisible).isFalse } @@ -269,8 +238,10 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `fab focus point visible when active task is PUBLISH_POST`() { startViewModelWithDefaultParameters() + whenever(mainCreateSheetHelper.shouldShowFabForPage(any())).thenReturn(true) + activeTask.value = PUBLISH_POST - viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) + viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) assertThat(fabUiState?.isFocusPointVisible).isTrue } @@ -279,7 +250,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { fun `fab focus point gone when active task is different`() { startViewModelWithDefaultParameters() activeTask.value = UPDATE_SITE_TITLE - viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) + viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) assertThat(fabUiState?.isFocusPointVisible).isFalse } @@ -288,7 +259,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { fun `fab focus point gone when active task is null`() { startViewModelWithDefaultParameters() activeTask.value = null - viewModel.onPageChanged(isOnMySitePageWithValidSite = true, site = initSite(hasFullAccessToContent = true)) + viewModel.onPageChanged(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = mock()) assertThat(fabUiState?.isFocusPointVisible).isFalse } @@ -304,6 +275,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `bottom sheet action is new page when new page is tapped`() { + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true) startViewModelWithDefaultParameters() val action = viewModel.mainActions.value?.first { it.actionType == CREATE_NEW_PAGE } as CreateAction assertThat(action).isNotNull @@ -313,7 +285,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `bottom sheet does not show prompt card when prompts feature is not active`() = test { - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(false) startViewModelWithDefaultParameters() val hasBloggingPromptAction = viewModel.mainActions.value?.any { it.actionType == ANSWER_BLOGGING_PROMPT } assertThat(hasBloggingPromptAction).isFalse() @@ -321,7 +293,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `bottom sheet does show prompt card when prompts feature is active`() = test { - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true) startViewModelWithDefaultParameters() val hasBloggingPromptAction = viewModel.mainActions.value?.any { it.actionType == ANSWER_BLOGGING_PROMPT } assertThat(hasBloggingPromptAction).isTrue() @@ -329,7 +301,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `bottom sheet action is ANSWER_BLOGGING_PROMPT when the BP answer button is clicked`() = test { - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true) startViewModelWithDefaultParameters() val action = viewModel.mainActions.value?.firstOrNull { it.actionType == ANSWER_BLOGGING_PROMPT @@ -345,7 +317,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `bottom sheet does not show quick start focus point by default`() { startViewModelWithDefaultParameters() - viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true)) + viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE) assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue assertThat(viewModel.mainActions.value?.any { it is CreateAction && it.showQuickStartFocusPoint }).isEqualTo( false @@ -356,7 +328,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { fun `CREATE_NEW_POST action in bottom sheet with active Quick Start completes task and hides the focus point`() { startViewModelWithDefaultParameters() activeTask.value = PUBLISH_POST - viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true)) + viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE) assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue assertThat(viewModel.mainActions.value?.any { it is CreateAction && it.showQuickStartFocusPoint }).isEqualTo( true @@ -376,7 +348,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { fun `CREATE_NEW_POST action sets task as done in QuickStartRepository`() { startViewModelWithDefaultParameters() activeTask.value = PUBLISH_POST - viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true)) + viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE) val action = viewModel.mainActions.value?.first { it.actionType == CREATE_NEW_POST } as CreateAction assertThat(action).isNotNull @@ -387,9 +359,11 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `actions that are not CREATE_NEW_POST will not complete quick start task`() { + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true) startViewModelWithDefaultParameters() + activeTask.value = PUBLISH_POST - viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true)) + viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE) assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue assertThat(viewModel.mainActions.value?.any { it is CreateAction && it.showQuickStartFocusPoint }).isEqualTo( true @@ -407,15 +381,29 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `new post action is triggered from FAB when no full access to content if stories unavailable`() { startViewModelWithDefaultParameters() - viewModel.onFabClicked(site = initSite(hasFullAccessToContent = false, isWpcomOrJpSite = false)) + viewModel.onFabClicked( + site = initSite(hasFullAccessToContent = false, isWpcomOrJpSite = false), + page = PageType.MY_SITE + ) assertThat(viewModel.isBottomSheetShowing.value).isNull() assertThat(viewModel.createAction.value).isEqualTo(CREATE_NEW_POST) } @Test - fun `bottom sheet is visualized when user has full access to content and has all 3 options`() { + fun `bottom sheet is visualized when user has full access to content and has 2 options`() { + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(false) + startViewModelWithDefaultParameters() + viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE) + assertThat(viewModel.createAction.value).isNull() + assertThat(viewModel.mainActions.value?.size).isEqualTo(2) // 1 option plus NO_ACTION, first in list + assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue + } + + @Test + fun `bottom sheet is visualized when user has full access to content and has 3 options`() { + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true) startViewModelWithDefaultParameters() - viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true)) + viewModel.onFabClicked(site = initSite(hasFullAccessToContent = true), page = PageType.MY_SITE) assertThat(viewModel.createAction.value).isNull() assertThat(viewModel.mainActions.value?.size).isEqualTo(3) // 2 options plus NO_ACTION, first in list assertThat(viewModel.isBottomSheetShowing.value!!.peekContent()).isTrue @@ -606,7 +594,20 @@ class WPMainActivityViewModelTest : BaseUnitTest() { } @Test - fun `bottom sheet actions are sorted in the correct order`() { + fun `bottom sheet actions are sorted in the correct order when can create post only`() { + startViewModelWithDefaultParameters() + + val expectedOrder = listOf( + NO_ACTION, + CREATE_NEW_POST, + ) + + assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder) + } + + @Test + fun `bottom sheet actions are sorted in the correct order when can create post, and page`() { + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true) startViewModelWithDefaultParameters() val expectedOrder = listOf( @@ -618,6 +619,41 @@ class WPMainActivityViewModelTest : BaseUnitTest() { assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder) } + @Test + fun `bottom sheet actions are sorted in the correct order when can create post, prompts, and page`() = test { + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true) + startViewModelWithDefaultParameters() + + val expectedOrder = listOf( + ANSWER_BLOGGING_PROMPT, + NO_ACTION, + CREATE_NEW_POST, + CREATE_NEW_PAGE + ) + + assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder) + } + + @Test + fun `bottom sheet actions are sorted in the correct order when can create post, from audio, prompts, and page`() = + test { + whenever(mainCreateSheetHelper.canCreatePostFromAudio(any())).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePage(any(), any())).thenReturn(true) + startViewModelWithDefaultParameters() + + val expectedOrder = listOf( + ANSWER_BLOGGING_PROMPT, + NO_ACTION, + CREATE_NEW_POST, + CREATE_NEW_POST_FROM_AUDIO, + CREATE_NEW_PAGE + ) + + assertThat(viewModel.mainActions.value!!.map { it.actionType }).isEqualTo(expectedOrder) + } + @Test fun `hasMultipleSites should be true when there are more than one site`() { whenever(siteStore.sitesCount).thenReturn(2) @@ -656,18 +692,18 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `Should track analytics event when onHelpPromptActionClicked is called`() = test { - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true) startViewModelWithDefaultParameters() val action = viewModel.mainActions.value?.first { it.actionType == ANSWER_BLOGGING_PROMPT } as AnswerBloggingPromptAction action.onHelpAction?.invoke() - verify(analyticsTrackerWrapper).track(Stat.MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED) + verify(mainCreateSheetTracker).trackHelpPromptActionTapped(any()) } @Test fun `Should trigger openBloggingPromptsOnboarding when onHelpPromptActionClicked is called`() = test { - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true) + whenever(mainCreateSheetHelper.canCreatePromptAnswer()).thenReturn(true) startViewModelWithDefaultParameters() val action = viewModel.mainActions.value?.first { it.actionType == ANSWER_BLOGGING_PROMPT @@ -677,14 +713,11 @@ class WPMainActivityViewModelTest : BaseUnitTest() { } @Test - @Suppress("MaxLineLength") - fun `Should track BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED when onFabClicked is called and actions contains AnswerBloggingPromptAction`() = - test { - whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(true) - startViewModelWithDefaultParameters() - viewModel.onFabClicked(initSite()) - verify(analyticsTrackerWrapper).track(Stat.BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED) - } + fun `Should track card actions when onFabClicker is called`() { + startViewModelWithDefaultParameters() + viewModel.onFabClicked(initSite(), page = PageType.MY_SITE) + verify(mainCreateSheetTracker).trackCreateActionsSheetCard(any()) + } @Test fun `it asks for privacy consent at the start when it should`() = test { @@ -731,12 +764,14 @@ class WPMainActivityViewModelTest : BaseUnitTest() { private fun startViewModelWithDefaultParameters( isWhatsNewFeatureEnabled: Boolean = true, - isCreateFabEnabled: Boolean = true, - isWpcomOrJpSite: Boolean = true + isWpcomOrJpSite: Boolean = true, + pageType: PageType = PageType.MY_SITE, ) { whenever(buildConfigWrapper.isWhatsNewFeatureEnabled).thenReturn(isWhatsNewFeatureEnabled) - whenever(buildConfigWrapper.isCreateFabEnabled).thenReturn(isCreateFabEnabled) - viewModel.start(site = initSite(hasFullAccessToContent = true, isWpcomOrJpSite = isWpcomOrJpSite)) + viewModel.start( + site = initSite(hasFullAccessToContent = true, isWpcomOrJpSite = isWpcomOrJpSite), + page = pageType + ) } private fun setupObservers() { @@ -748,7 +783,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { } private fun resumeViewModelWithDefaultParameters() { - viewModel.onResume(site = initSite(hasFullAccessToContent = true), isOnMySitePageWithValidSite = true) + viewModel.onResume(site = initSite(hasFullAccessToContent = true), hasValidSite = true, page = PageType.MY_SITE) } private fun initSite( diff --git a/build.gradle b/build.gradle index 58a456beb870..ebbc2036d83a 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ plugins { ext { minSdkVersion = 24 compileSdkVersion = 34 - targetSdkVersion = 34 + targetSdkVersion = 33 } ext { @@ -25,12 +25,12 @@ ext { automatticTracksVersion = '5.1.0' gutenbergMobileVersion = 'v1.120.0' wordPressAztecVersion = 'v2.1.3' - wordPressFluxCVersion = '2.83.0' + wordPressFluxCVersion = 'trunk-b5d95fda4257bd1b3c94b33088f5e2a3f48ff1c2' wordPressLoginVersion = '1.15.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0' indexosMediaForMobileVersion = '43a9026f0973a2f0a74fa813132f6a16f7499c3a' - gravatarVersion = '0.3.0' + gravatarVersion = '1.0.0' // debug flipperVersion = '0.245.0' diff --git a/fastlane/jetpack_metadata/android/ar/changelogs/1433.txt b/fastlane/jetpack_metadata/android/ar/changelogs/1433.txt deleted file mode 100644 index eef3b44283d4..000000000000 --- a/fastlane/jetpack_metadata/android/ar/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- أعدنا تنظيم شاشة الإحصاءات لعرض البيانات حول حركة المرور والرؤى. -- أضفنا علامة تبويب المشتركين لعرض البيانات حول مشتركي الموقع. -- أزلنا المشتركين عبر شبكات التواصل الاجتماعي من بطاقة "إجمالي المتابعين". -- تم وضع أسماء المواقع وعناوين URL الخاصة بها بشكل صحيح بالنسبة إلى مستخدمي اللغات التي تبدأ من اليمين إلى اليسار. diff --git a/fastlane/jetpack_metadata/android/ar/changelogs/1435.txt b/fastlane/jetpack_metadata/android/ar/changelogs/1435.txt new file mode 100644 index 000000000000..b54a9a483a7a --- /dev/null +++ b/fastlane/jetpack_metadata/android/ar/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +أصبح موجز الوسوم مباشرًا! يمكنك الآن الاطلاع على المحتوى الذي يتضمن وسومًا معينة، وكل ذلك في مكان واحد. وسم، أنت هو. + +لقد أصلحنا أعطالاً متنوعة في شاشتَي تسجيل الدخول وقائمة التدوينات، إضافة إلى الإجراءات المرتبطة بتذكيرات التدوين والصور المميزة وإزالة المستخدمين. هل ترغب في أعطال أقل؟ كيفية القضاء عليها. diff --git a/fastlane/jetpack_metadata/android/de-DE/changelogs/1433.txt b/fastlane/jetpack_metadata/android/de-DE/changelogs/1433.txt deleted file mode 100644 index c4a8b0866e71..000000000000 --- a/fastlane/jetpack_metadata/android/de-DE/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- Wir haben den Stats-Bildschirm neu organisiert, um Daten zu Traffic und Einsichten anzuzeigen. -- Wir haben den Tab „Abonnenten“ hinzugefügt, um Daten zu Website-Abonnenten anzuzeigen. -- Social-Media-Abonnenten wurden aus der Karte „Followers gesamt“ entfernt. -- Websitenamen und URLs sind für Benutzer von Rechts-nach-Links-Sprachen nun richtig positioniert. diff --git a/fastlane/jetpack_metadata/android/de-DE/changelogs/1435.txt b/fastlane/jetpack_metadata/android/de-DE/changelogs/1435.txt new file mode 100644 index 000000000000..8a7e0828a8ee --- /dev/null +++ b/fastlane/jetpack_metadata/android/de-DE/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +Der Schlagwort-Feed ist live! Jetzt kannst du Inhalte mit spezifischen Schlagwörtern an einem zentralen Ort sehen. Viel Spaß damit! + +Wir haben verschiedene Abstürze auf den Anmelde- und Beitragslistenbildschirmen sowie Aktionen, die mit Blog-Erinnerungen, Beitragsbildern und Benutzerentfernung zu tun hatten, behoben. Weniger Abstürze? Klingt super, oder? diff --git a/fastlane/jetpack_metadata/android/en-US/changelogs/1433.txt b/fastlane/jetpack_metadata/android/en-US/changelogs/1433.txt deleted file mode 100644 index 1e2fd6c021ad..000000000000 --- a/fastlane/jetpack_metadata/android/en-US/changelogs/1433.txt +++ /dev/null @@ -1,4 +0,0 @@ -- We reorganized the Stats screen to show data about traffic and insights. -- We added a Subscribers tab to show data about site subscribers. -- We removed social subscribers from the Total Followers card. -- Site names and URLs are properly positioned for right-to-left language users. diff --git a/fastlane/jetpack_metadata/android/en-US/changelogs/1435.txt b/fastlane/jetpack_metadata/android/en-US/changelogs/1435.txt new file mode 100644 index 000000000000..928c57d6d98a --- /dev/null +++ b/fastlane/jetpack_metadata/android/en-US/changelogs/1435.txt @@ -0,0 +1,3 @@ +The Tags feed is live! You can now see content with specific tags, all in one place. Tag, you’re it. + +We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing. diff --git a/fastlane/jetpack_metadata/android/es-ES/changelogs/1433.txt b/fastlane/jetpack_metadata/android/es-ES/changelogs/1433.txt deleted file mode 100644 index df02a299a35e..000000000000 --- a/fastlane/jetpack_metadata/android/es-ES/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- Hemos reorganizado la pantalla de estadísticas para mostrar datos sobre el tráfico y detalles. -- Hemos añadido una pestaña Suscriptores que incluye datos sobre los suscriptores del sitio. -- Hemos eliminado los suscriptores de las redes sociales de la tarjeta Total de seguidores. -- Las URL y los nombres de los sitios se colocan correctamente para los usuarios con idiomas con escritura de derecha a izquierda. diff --git a/fastlane/jetpack_metadata/android/es-ES/changelogs/1435.txt b/fastlane/jetpack_metadata/android/es-ES/changelogs/1435.txt new file mode 100644 index 000000000000..fb470f318a44 --- /dev/null +++ b/fastlane/jetpack_metadata/android/es-ES/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +¡El feed de etiquetas ya está disponible! Ahora puedes ver el contenido con etiquetas específicas, todo en un solo lugar. Las etiquetas lo son todo. + +Hemos corregido varios fallos en las pantallas de acceso y de lista de entradas, así como en las acciones asociadas a los recordatorios de blogs, las imágenes destacadas y la eliminación de usuarios. ¿Menos caídas? Estupendo. diff --git a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1433.txt b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1433.txt deleted file mode 100644 index df4204525083..000000000000 --- a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9 : -- Nous avons réorganisé l’écran Statistiques pour afficher des données relatives au trafic ainsi que des tendances. -- Nous avons ajouté un onglet Abonnés pour afficher des données relatives aux abonnés du site. -- Nous avons retiré les abonnés via les réseaux sociaux de la carte Total des abonnés. -- Les noms de site et les URL sont correctement positionnés pour les locuteurs de langues se lisant de droite à gauche. diff --git a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1435.txt b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1435.txt new file mode 100644 index 000000000000..f4a6b682ed77 --- /dev/null +++ b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0 : +Le flux Étiquettes est lancé ! Vous pouvez désormais voir le contenu avec des étiquettes spécifiques dans un seul et même endroit. Étiqueté, c’est pesé. + +Nous avons corrigé des plantages sur les écrans de connexion et de liste d’articles, ainsi qu’avec les actions associées aux rappels de blog, les images mises en avant et la suppression d’utilisateur. Moins de plantages ? Tout à votre avantage. diff --git a/fastlane/jetpack_metadata/android/id/changelogs/1433.txt b/fastlane/jetpack_metadata/android/id/changelogs/1433.txt deleted file mode 100644 index fd2ecbb0d7f6..000000000000 --- a/fastlane/jetpack_metadata/android/id/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- Kami menata ulang layar Status agar menampilkan data kunjungan dan wawasan. -- Kami menambahkan tab Pelanggan untuk menampilkan data pelanggan situs. -- Kami menghapus pelanggan media sosial dari kartu Total Pengikut. -- Nama situs dan URL telah diposisikan dengan benar untuk pengguna bahasa yang ditulis dari kanan ke kiri. diff --git a/fastlane/jetpack_metadata/android/id/changelogs/1435.txt b/fastlane/jetpack_metadata/android/id/changelogs/1435.txt new file mode 100644 index 000000000000..704a5304d4da --- /dev/null +++ b/fastlane/jetpack_metadata/android/id/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +Feed Tag sudah aktif! Konten dengan tag tertentu sekarang terkumpul rapi di satu lokasi. Tag, giliran kamu sekarang. + +Crash pada layar login dan Daftar Pos telah diperbaiki, sama halnya dengan aksi-aksi terkait pengingat blogging, gambar andalan, dan penghapusan pengguna. Crash berkurang? Performa kian cemerlang. diff --git a/fastlane/jetpack_metadata/android/it-IT/changelogs/1433.txt b/fastlane/jetpack_metadata/android/it-IT/changelogs/1433.txt deleted file mode 100644 index 30e788f0987f..000000000000 --- a/fastlane/jetpack_metadata/android/it-IT/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24,9: -- Abbiamo riorganizzato la schermata Statistiche per mostrare dati su traffico e approfondimenti. -- Abbiamo aggiunto una scheda Abbonati per mostrare dati sugli abbonati al sito. -- Abbiamo rimosso gli abbonati social dalla scheda Totale Seguaci. -- I nomi e URL del sito sono posizionati correttamente per utenti di lingue da destra a sinistra. diff --git a/fastlane/jetpack_metadata/android/it-IT/changelogs/1435.txt b/fastlane/jetpack_metadata/android/it-IT/changelogs/1435.txt new file mode 100644 index 000000000000..5858d225c781 --- /dev/null +++ b/fastlane/jetpack_metadata/android/it-IT/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +Il feed dei tag è finalmente attivo. Ora puoi vedere tutti i contenuti con tag specifici in un unico posto. Così è tutto molto più semplice! + +Abbiamo risolto diversi arresti anomali nella schermata di accesso e in quella dell'elenco degli articoli e corretto alcune azioni associate ai promemoria relativi al blog, alle immagini in evidenza e alla rimozione di utenti. Arresti anomali? Ormai sono solo un ricordo. diff --git a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1433.txt b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1433.txt deleted file mode 100644 index a7b61a64436d..000000000000 --- a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- ארגנו את המסך 'נתונים סטטיסטיים' כדי להציג נתונים לגבי תעבורה ותובנות. -- הוספנו לשונית 'מנויים' כדי להציג נתונים לגבי מנויי האתר. -- הסרנו את המנויים של הרשתות החברתיות מהכרטיס 'סך כל העוקבים'. -- שמות אתרים וכתובות URL ממוקמים בצורה נכונה עבור משתמשים בשפות עם כיווניות מימין לשמאל. diff --git a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1435.txt b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1435.txt new file mode 100644 index 000000000000..840e9ecdc3d0 --- /dev/null +++ b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +פיד התגיות הושק! כעת אפשר לראות תוכן עם תגיות מסוימות במקום מרוכז אחד. תגיות או לא להיות. + +תיקנו כמה קריסות במסכים של ההתחברות ו'רשימת פוסטים'. תיקנו גם קריסות בפעולות שקשורות לתזכורות לכתיבה בבלוג, לתמונות ראשיות ולהסרת משתמשים. פחות קריסות, יותר טוב! diff --git a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1433.txt b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1433.txt deleted file mode 100644 index 6ccedff4fd93..000000000000 --- a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- 「統計」画面を再編成して、トラフィックと統計概要に関するデータを表示しました。 -- 「購読者」タブを追加して、サイトの購読者に関するデータを表示しました。 -- 「フォロワー総数」カードからソーシャルメディアの購読者を削除しました。 -- サイト名と URL が、右から左へ記述する言語のユーザー向けに適切に配置されます。 diff --git a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1435.txt b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1435.txt new file mode 100644 index 000000000000..fc44a8075463 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +タグフィードが利用可能になりました ! 特定のタグが付いたコンテンツをすべて1か所で表示できるようになりました。 ぜひご活用ください。 + +ログイン画面や投稿一覧画面でのさまざまなクラッシュのほか、ブログのリマインダー、アイキャッチ画像、ユーザーの削除に関連するアクションを修正しました。 クラッシュが減り、 より快適にご利用いただけます。 diff --git a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1433.txt b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1433.txt deleted file mode 100644 index 014acf5b70f1..000000000000 --- a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- 트래픽과 인사이트에 대한 데이터를 표시하도록 통계 화면이 재구성되었습니다. -- 사이트 구독자에 대한 데이터를 표시하도록 구독자 탭이 추가되었습니다. -- 총 팔로워 수 카드에서 소셜 구독자가 제거되었습니다. -- 오른쪽에서 왼쪽으로 읽는(RTL) 언어 사용자를 위해 사이트 이름과 URL의 위치가 적절히 조정되었습니다. diff --git a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1435.txt b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1435.txt new file mode 100644 index 000000000000..35fedbbce29b --- /dev/null +++ b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +태그 피드가 출시되었습니다! 이제 특정 태그가 적용된 콘텐츠를 한곳에서 볼 수 있습니다. 태그를 마음껏 활용해 보세요. + +로그인 및 글 목록 화면과 블로그 알림, 특성 이미지 및 사용자 제거와 관련된 작업에서 발생하는 갖가지 충돌이 해결되었습니다. 충돌이 줄어들다니, 정말 좋은 일 아닌가요? diff --git a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1433.txt b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1433.txt deleted file mode 100644 index e961730b4c33..000000000000 --- a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- We hebben het scherm met statistieken opnieuw georganiseerd om gegevens over verkeer en inzichten weer te geven. -- We hebben een tabblad voor abonnees toegevoegd om gegevens over abonnees van de site weer te geven. -- We hebben sociale abonnees van de kaart Aantal volgers verwijderd. -- Sitenamen en URL’s zijn goed gepositioneerd voor rechts-naar-links taalgebruikers. diff --git a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1435.txt b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1435.txt new file mode 100644 index 000000000000..cdf3d28b0a81 --- /dev/null +++ b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1435.txt @@ -0,0 +1,4 @@ ++25.0 +The Tags feed is live! You can now see content with specific tags, all in one place. Tag, you’re it. + +We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing. diff --git a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1433.txt b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1433.txt deleted file mode 100644 index f05f68914901..000000000000 --- a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- Reorganizamos a tela Estatísticas para mostrar dados sobre tráfego e insights. -- Adicionamos uma guia Assinantes para mostrar dados sobre os assinantes do site. -- Removemos assinantes de redes sociais do cartão Total de seguidores. -- Nomes e URLs dos sites posicionados corretamente para usuários de idiomas que escrevem da direita para a esquerda. diff --git a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1435.txt b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1435.txt new file mode 100644 index 000000000000..59595d25db47 --- /dev/null +++ b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +O feed de tags está no ar! Agora você pode ver o conteúdo com tags específicas, tudo em um só lugar. É a sua vez de aproveitar as tags. + +Corrigimos diferentes falhas nas telas de login e listas de posts, bem como ações associadas aos lembretes de publicação, imagens destacadas e remoção de usuário. Menos falhas? Que incrível. diff --git a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1433.txt b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1433.txt deleted file mode 100644 index 098d753ce728..000000000000 --- a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- Мы упорядочили экран статистики, чтобы отображать данные о посещаемости и аналитику. -- Мы добавили вкладку «Подписчики», чтобы отображать данные о подписчиках сайта. -- Мы удалили подписчиков в социальных сетях из карты «Все подписчики». -- Имена и URL-адреса сайтов отображаются правильно для пользователей с направлением письма справа налево. diff --git a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1435.txt b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1435.txt new file mode 100644 index 000000000000..afbe0a3a6a53 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +Представляем живую ленту тегов! Теперь вы можете просматривать контент по определённым тегам из одной точки. Все теги под рукой! + +Мы устранили целый ряд сбоев на экранах входа в систему и списка записей, а также исправили ошибки в действиях, связанных с напоминаниями для блогеров, избранными изображениями и удалением пользователей. Ничего не падает, и работа радует. diff --git a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1433.txt b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1433.txt deleted file mode 100644 index bd731c0ec97a..000000000000 --- a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -– Vi har organiserat om statistikskärmen så att den visar data om trafik och insikter. -– Vi har lagt till fliken Prenumeranter för att visa data om webbplatsens prenumeranter. -– Vi tog bort sociala prenumeranter från kortet Totalt antal följare. -– Webbplatsnamn och URL:er är korrekt positionerade för användare med språk från höger till vänster. diff --git a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1435.txt b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1435.txt new file mode 100644 index 000000000000..f2b6e94ddcac --- /dev/null +++ b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +Etikettflödet är live. Du kan nu se innehåll med specifika etiketter, allt på en och samma plats. Det är praktiskt med etiketter. + +Vi har åtgärdat diverse krascher som uppstod på inloggnings- och inläggslisteskärmarna samt i samband med bloggpåminnelser, funktionsbilder och borttagning av användare. Mindre krascher? Fantastiskt. diff --git a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1433.txt b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1433.txt deleted file mode 100644 index 2af0f030a625..000000000000 --- a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- İstatistikler ekranını trafik ve içgörüleri gösterecek şekilde yeniden organize ettik. -- Site aboneleri hakkındaki verileri göstermek için Aboneler sekmesi ekledik. -- Toplam Takipçi kartından sosyal medya abonelerini kaldırdık. -- Site adları ve URL'ler, sağdan sola yazılan dilleri kullanan kullanıcılar için düzgün şekilde yerleştirildi. diff --git a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1435.txt b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1435.txt new file mode 100644 index 000000000000..78fbd79071b8 --- /dev/null +++ b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +Etiketler şeridi yayında! Artık belirli etiketler içeren içeriklerin hepsini bir yerde görebilirsiniz. Etiketleme zamanı geldi. + +Blog hatırlatıcıları, öne çıkan görseller ve kullanıcı kaldırmayla ilişkili işlemlerin yanı sıra oturum açma ve Gönderi Listesi ekranlarındaki çeşitli çökme sorunlarını düzelttik. Daha az çökme mi? Ne kadar etkileyici. diff --git a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1433.txt b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1433.txt deleted file mode 100644 index 1420e9b9a236..000000000000 --- a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- 我们重新组织了“统计信息”屏幕,以显示相关流量数据和数据分析。 -- 我们增加了“订阅者”选项卡,以显示站点订阅者的相关数据。 -- 我们从“粉丝总数”卡片中移除了社交媒体订阅者。 -- 站点名称和 URL 在正确位置呈现,符合从右到左语言用户的习惯。 diff --git a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1435.txt b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1435.txt new file mode 100644 index 000000000000..d58f6399cc2f --- /dev/null +++ b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +标签源已上线! 现在,您可以在同一位置查看带有特定标签的所有内容。 您可以自行设置标签。 + +我们修复了登录和“文章列表”屏幕上的各种崩溃问题,以及与博客提醒、特色图片和用户删除相关的操作。 崩溃问题更少了? 多棒啊。 diff --git a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1433.txt b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1433.txt deleted file mode 100644 index cbbfa9840464..000000000000 --- a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1433.txt +++ /dev/null @@ -1,5 +0,0 @@ -24.9: -- 我們重新編排了「統計」畫面,以便顯示流量和洞察報告的相關資料。 -- 我們新增了「訂閱者」分頁,以便顯示網站訂閱者的相關資料。 -- 我們從「追蹤者總數」的卡片中移除了社群訂閱者。 -- 我們為語言閱讀方向由右向左的使用者,顯示網站名稱和 URL 的正確位置。 diff --git a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1435.txt b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1435.txt new file mode 100644 index 000000000000..03219b841daf --- /dev/null +++ b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1435.txt @@ -0,0 +1,4 @@ +25.0: +標籤摘要已上線! 你現在可在同一處查看附有特定標籤的內容。 標籤,抓到你了。 + +我們修正了登入和文章清單畫面的各類當機狀況,以及與網誌提醒、精選圖片和使用者移除相關的動作。 當機狀況變少了嗎? 太棒了。 diff --git a/fastlane/metadata/android/ar/changelogs/1433.txt b/fastlane/metadata/android/ar/changelogs/1433.txt deleted file mode 100644 index b9e35b4cc194..000000000000 --- a/fastlane/metadata/android/ar/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -تم الآن وضع أسماء المواقع وعناوين URL الخاصة بها بشكل صحيح بالنسبة إلى مستخدمي اللغات التي تبدأ من اليمين إلى اليسار. تم الأمر. diff --git a/fastlane/metadata/android/ar/changelogs/1435.txt b/fastlane/metadata/android/ar/changelogs/1435.txt new file mode 100644 index 000000000000..fea8d733c15a --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +لقد أصلحنا أعطالاً متنوعة في شاشتَي تسجيل الدخول وقائمة التدوينات، إضافة إلى الإجراءات المرتبطة بتذكيرات التدوين والصور المميزة وإزالة المستخدمين. هل ترغب في أعطال أقل؟ كيفية القضاء عليها. diff --git a/fastlane/metadata/android/de-DE/changelogs/1433.txt b/fastlane/metadata/android/de-DE/changelogs/1433.txt deleted file mode 100644 index 1ae6129aeece..000000000000 --- a/fastlane/metadata/android/de-DE/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -Websitenamen und URLs sind für Benutzer von Rechts-nach-Links-Sprachen nun richtig positioniert. Genau richtig. diff --git a/fastlane/metadata/android/de-DE/changelogs/1435.txt b/fastlane/metadata/android/de-DE/changelogs/1435.txt new file mode 100644 index 000000000000..3bb3d67886d5 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +Wir haben verschiedene Abstürze in den Ansichten für die Anmeldung und die Beitragsliste sowie Aktionen im Zusammenhang mit Blogging-Erinnerungen, Vorschaubildern und dem Entfernen von Benutzern behoben. Weniger Abstürze? Wie toll. diff --git a/fastlane/metadata/android/en-US/changelogs/1433.txt b/fastlane/metadata/android/en-US/changelogs/1433.txt deleted file mode 100644 index 040fd0b337b1..000000000000 --- a/fastlane/metadata/android/en-US/changelogs/1433.txt +++ /dev/null @@ -1 +0,0 @@ -Site names and URLs are now properly positioned for right-to-left language users. Right on. diff --git a/fastlane/metadata/android/en-US/changelogs/1435.txt b/fastlane/metadata/android/en-US/changelogs/1435.txt new file mode 100644 index 000000000000..257e18b25fcd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1435.txt @@ -0,0 +1 @@ +We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing. diff --git a/fastlane/metadata/android/es-ES/changelogs/1433.txt b/fastlane/metadata/android/es-ES/changelogs/1433.txt deleted file mode 100644 index bae881965c4f..000000000000 --- a/fastlane/metadata/android/es-ES/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -Los nombres de los sitios y las URL están ahora colocados correctamente para los usuarios de idiomas que hablan de derecha a izquierda. ¡Bravo!. diff --git a/fastlane/metadata/android/es-ES/changelogs/1435.txt b/fastlane/metadata/android/es-ES/changelogs/1435.txt new file mode 100644 index 000000000000..d98a7a32b9bc --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +Hemos corregido varios fallos en las pantallas de acceso y de lista de entradas, así como en las acciones asociadas a los recordatorios de blogs, las imágenes destacadas y la eliminación de usuarios. ¿Menos caídas? Estupendo. diff --git a/fastlane/metadata/android/fr-CA/changelogs/1433.txt b/fastlane/metadata/android/fr-CA/changelogs/1433.txt deleted file mode 100644 index a363fb9b61ce..000000000000 --- a/fastlane/metadata/android/fr-CA/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9 : -Les noms de site et les URL sont désormais correctement positionnés pour les locuteurs de langues se lisant de droite à gauche. À gauche toute ! diff --git a/fastlane/metadata/android/fr-CA/changelogs/1435.txt b/fastlane/metadata/android/fr-CA/changelogs/1435.txt new file mode 100644 index 000000000000..b4192dff08c5 --- /dev/null +++ b/fastlane/metadata/android/fr-CA/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0 : +Nous avons corrigé des plantages sur les écrans de connexion et de liste d’articles, ainsi qu’avec les actions associées aux rappels de blog, les images mises en avant et la suppression d’utilisateur. Moins de plantages ? Tout à votre avantage. diff --git a/fastlane/metadata/android/fr-FR/changelogs/1433.txt b/fastlane/metadata/android/fr-FR/changelogs/1433.txt deleted file mode 100644 index a363fb9b61ce..000000000000 --- a/fastlane/metadata/android/fr-FR/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9 : -Les noms de site et les URL sont désormais correctement positionnés pour les locuteurs de langues se lisant de droite à gauche. À gauche toute ! diff --git a/fastlane/metadata/android/fr-FR/changelogs/1435.txt b/fastlane/metadata/android/fr-FR/changelogs/1435.txt new file mode 100644 index 000000000000..b4192dff08c5 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0 : +Nous avons corrigé des plantages sur les écrans de connexion et de liste d’articles, ainsi qu’avec les actions associées aux rappels de blog, les images mises en avant et la suppression d’utilisateur. Moins de plantages ? Tout à votre avantage. diff --git a/fastlane/metadata/android/id/changelogs/1433.txt b/fastlane/metadata/android/id/changelogs/1433.txt deleted file mode 100644 index 4307aa22f816..000000000000 --- a/fastlane/metadata/android/id/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -Nama situs dan URL telah diposisikan dengan benar untuk pengguna bahasa yang ditulis dari kanan ke kiri. Beres. diff --git a/fastlane/metadata/android/id/changelogs/1435.txt b/fastlane/metadata/android/id/changelogs/1435.txt new file mode 100644 index 000000000000..06eda42f9a81 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +Crash pada layar login dan Daftar Pos telah diperbaiki, sama halnya dengan aksi-aksi terkait pengingat blogging, gambar andalan, dan penghapusan pengguna. Crash berkurang? Performa kian cemerlang. diff --git a/fastlane/metadata/android/it-IT/changelogs/1433.txt b/fastlane/metadata/android/it-IT/changelogs/1433.txt deleted file mode 100644 index 2855dc1f69fb..000000000000 --- a/fastlane/metadata/android/it-IT/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24,9: -I nomi e URL del sito ora sono posizionati correttamente per utenti di lingue da destra a sinistra. Proprio così! diff --git a/fastlane/metadata/android/it-IT/changelogs/1435.txt b/fastlane/metadata/android/it-IT/changelogs/1435.txt new file mode 100644 index 000000000000..5945c8b41a11 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +Abbiamo risolto diversi arresti anomali nella schermata di accesso e in quella dell'elenco degli articoli e corretto alcune azioni associate ai promemoria relativi al blog, alle immagini in evidenza e alla rimozione di utenti. Arresti anomali? Ormai sono solo un ricordo. diff --git a/fastlane/metadata/android/iw-IL/changelogs/1433.txt b/fastlane/metadata/android/iw-IL/changelogs/1433.txt deleted file mode 100644 index 349bf42d16ad..000000000000 --- a/fastlane/metadata/android/iw-IL/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -שמות אתרים וכתובות URL ממוקמים כעת בצורה נכונה עבור משתמשים בשפות עם כיווניות מימין לשמאל. ישר ולעניין. diff --git a/fastlane/metadata/android/iw-IL/changelogs/1435.txt b/fastlane/metadata/android/iw-IL/changelogs/1435.txt new file mode 100644 index 000000000000..f558d30e4067 --- /dev/null +++ b/fastlane/metadata/android/iw-IL/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +תיקנו כמה קריסות במסכים של ההתחברות ו'רשימת פוסטים'. תיקנו גם קריסות בפעולות שקשורות לתזכורות לכתיבה בבלוג, לתמונות ראשיות ולהסרת משתמשים. פחות קריסות, יותר טוב! diff --git a/fastlane/metadata/android/ja-JP/changelogs/1435.txt b/fastlane/metadata/android/ja-JP/changelogs/1435.txt new file mode 100644 index 000000000000..3d47cdf7f51a --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +ログイン画面や投稿一覧画面でのさまざまなクラッシュのほか、ブログのリマインダー、アイキャッチ画像、ユーザーの削除に関連するアクションを修正しました。 クラッシュが減り、 より快適にご利用いただけます。 diff --git a/fastlane/metadata/android/ko-KR/changelogs/1433.txt b/fastlane/metadata/android/ko-KR/changelogs/1433.txt deleted file mode 100644 index 2d4be1cafa2b..000000000000 --- a/fastlane/metadata/android/ko-KR/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -이제 사이트 이름과 URL이 오른쪽에서 왼쪽 언어 사용자를 위해 올바르게 배치됩니다. 바로 적용되었습니다. diff --git a/fastlane/metadata/android/ko-KR/changelogs/1435.txt b/fastlane/metadata/android/ko-KR/changelogs/1435.txt new file mode 100644 index 000000000000..e069218b033a --- /dev/null +++ b/fastlane/metadata/android/ko-KR/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +로그인 및 글 목록 화면과 블로그 알림, 특성 이미지 및 사용자 제거와 관련된 작업에서 발생하는 갖가지 충돌이 해결되었습니다. 충돌이 줄어들다니, 정말 좋은 일 아닌가요? diff --git a/fastlane/metadata/android/nl-NL/changelogs/1433.txt b/fastlane/metadata/android/nl-NL/changelogs/1433.txt deleted file mode 100644 index 9deba172b067..000000000000 --- a/fastlane/metadata/android/nl-NL/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -Site namen en URL's zijn nu correct gepositioneerd voor gebruikers van rechts-naar-links talen. Helemaal goed. diff --git a/fastlane/metadata/android/nl-NL/changelogs/1435.txt b/fastlane/metadata/android/nl-NL/changelogs/1435.txt new file mode 100644 index 000000000000..bbbfeb0ea056 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +We hebben verschillende crashes op het login en berichtenvermelding scherm opgelost, evenals acties die verband houden met blog herinneringen, uitgelichte afbeeldingen en het verwijderen van gebruikers. Minder crashes? Hoe geweldig. diff --git a/fastlane/metadata/android/ru-RU/changelogs/1433.txt b/fastlane/metadata/android/ru-RU/changelogs/1433.txt deleted file mode 100644 index f79b0053c554..000000000000 --- a/fastlane/metadata/android/ru-RU/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -Имена и URL-адреса сайтов отображаются правильно для пользователей с направлением письма справа налево. Именно там, где надо. diff --git a/fastlane/metadata/android/ru-RU/changelogs/1435.txt b/fastlane/metadata/android/ru-RU/changelogs/1435.txt new file mode 100644 index 000000000000..7f5134d02f75 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +Мы устранили целый ряд сбоев на экранах входа в систему и списка записей, а также исправили ошибки в действиях, связанных с напоминаниями для блогеров, избранными изображениями и удалением пользователей. Ничего не падает, и работа радует. diff --git a/fastlane/metadata/android/sv-SE/changelogs/1433.txt b/fastlane/metadata/android/sv-SE/changelogs/1433.txt deleted file mode 100644 index a0a3781e96cf..000000000000 --- a/fastlane/metadata/android/sv-SE/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -Webbplatsnamn och URL:er är nu korrekt positionerade för användare med språk från höger till vänster. Höger om. diff --git a/fastlane/metadata/android/sv-SE/changelogs/1435.txt b/fastlane/metadata/android/sv-SE/changelogs/1435.txt new file mode 100644 index 000000000000..dc4fbb8de362 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/1435.txt @@ -0,0 +1,2 @@ ++25.0 +We fixed assorted crashes on the login and Posts List screens, as well as actions associated with blogging reminders, feature images, and user removal. Less crashing? How smashing. diff --git a/fastlane/metadata/android/tr-TR/changelogs/1433.txt b/fastlane/metadata/android/tr-TR/changelogs/1433.txt deleted file mode 100644 index cfc6a3c7ca18..000000000000 --- a/fastlane/metadata/android/tr-TR/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -- Site adları ve URL'ler, sağdan sola yazılan dilleri kullanan kullanıcılar için düzgün şekilde yerleştirildi. Harika. diff --git a/fastlane/metadata/android/tr-TR/changelogs/1435.txt b/fastlane/metadata/android/tr-TR/changelogs/1435.txt new file mode 100644 index 000000000000..8b251de52626 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +Giriş ve Yazı Listesi ekranlarındaki çeşitli çökmelerin yanı sıra blog hatırlatıcıları, öne çıkarılmış resimler ve kullanıcı kaldırma ile ilgili eylemleri düzelttik. Daha az çökme mi? Ne kadar harika. diff --git a/fastlane/metadata/android/zh-CN/changelogs/1433.txt b/fastlane/metadata/android/zh-CN/changelogs/1433.txt deleted file mode 100644 index 09145a66a16b..000000000000 --- a/fastlane/metadata/android/zh-CN/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -现在,站点名称和 URL 在正确位置呈现,符合从右到左语言用户的习惯。 完全符合。 diff --git a/fastlane/metadata/android/zh-CN/changelogs/1435.txt b/fastlane/metadata/android/zh-CN/changelogs/1435.txt new file mode 100644 index 000000000000..5d49971888cc --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +我们修复了登录和“文章列表”屏幕上的各种崩溃问题,以及与博客提醒、特色图片和用户删除相关的操作。 崩溃问题更少了? 多棒啊。 diff --git a/fastlane/metadata/android/zh-TW/changelogs/1433.txt b/fastlane/metadata/android/zh-TW/changelogs/1433.txt deleted file mode 100644 index 8fe7c46bda84..000000000000 --- a/fastlane/metadata/android/zh-TW/changelogs/1433.txt +++ /dev/null @@ -1,2 +0,0 @@ -24.9: -我們現在為語言閱讀方向由右向左的使用者,顯示網站名稱和 URL 的正確位置。 太好了。 diff --git a/fastlane/metadata/android/zh-TW/changelogs/1435.txt b/fastlane/metadata/android/zh-TW/changelogs/1435.txt new file mode 100644 index 000000000000..b5cef29bf852 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/1435.txt @@ -0,0 +1,2 @@ +25.0: +我們修正了登入和文章清單畫面的各類當機狀況,以及與網誌提醒、精選圖片和使用者移除相關的動作。 當機狀況變少了嗎? 太棒了。 diff --git a/fastlane/resources/values/strings.xml b/fastlane/resources/values/strings.xml index fc1aad108df8..dc156115e4a0 100644 --- a/fastlane/resources/values/strings.xml +++ b/fastlane/resources/values/strings.xml @@ -3100,6 +3100,7 @@ Photos and videos & Music and audio Camera Microphone + Media Location You can copy your post text in case your content is impacted. Copy error details to debug and share with support. Clear selected color Link label + + + Audio Recording Permission Required + To record audio, this app needs permission to access your microphone. You have previously denied this permission. Please enable the microphone permission in the app settings to use this feature. + Tap to edit + diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 2a93501cf9b6..94adb328c95b 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -304,7 +304,6 @@ public enum Stat { MY_SITE_ICON_CROPPED, MY_SITE_ICON_UPLOADED, MY_SITE_ICON_UPLOAD_UNSUCCESSFUL, - MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED, NOTIFICATIONS_DISABLED, NOTIFICATIONS_ENABLED, NOTIFICATIONS_ACCESSED, @@ -831,7 +830,14 @@ public enum Stat { JETPACK_BACKUP_DOWNLOAD_SHARE_LINK_TAPPED, MY_SITE_CREATE_SHEET_SHOWN, MY_SITE_CREATE_SHEET_ACTION_TAPPED, + MY_SITE_CREATE_SHEET_ANSWER_PROMPT_TAPPED, MY_SITE_CREATE_SHEET_PROMPT_HELP_TAPPED, + MY_SITE_CREATE_FAB_SHOWN, + READER_CREATE_SHEET_SHOWN, + READER_CREATE_SHEET_ACTION_TAPPED, + READER_CREATE_SHEET_ANSWER_PROMPT_TAPPED, + READER_CREATE_SHEET_PROMPT_HELP_TAPPED, + READER_CREATE_FAB_SHOWN, BLOGGING_PROMPTS_CREATE_SHEET_CARD_VIEWED, MY_SITE_NO_SITES_VIEW_DISPLAYED, MY_SITE_NO_SITES_VIEW_ACTION_TAPPED, @@ -1134,7 +1140,7 @@ public enum Stat { IN_APP_UPDATE_SHOWN, IN_APP_UPDATE_DISMISSED, IN_APP_UPDATE_ACCEPTED, - IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART; + IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART_BY_USER; /* * Please set the event name in the enum only if the new Stat's name in lower case does not match it. diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index c8dc71940370..38504687b0d9 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -680,6 +680,13 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d String blockId = data.getStringExtra(WPGutenbergWebViewActivity.ARG_BLOCK_ID); String content = data.getStringExtra(WPGutenbergWebViewActivity.ARG_BLOCK_CONTENT); getGutenbergContainerFragment().replaceUnsupportedBlock(content, blockId); + if (mCurrentGutenbergPropsBuilder == null) { + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(getContext()); + if (db != null) { + mCurrentGutenbergPropsBuilder = db.getParcel(ARG_GUTENBERG_PROPS_BUILDER, + GutenbergPropsBuilder.CREATOR); + } + } // We need to send latest capabilities as JS side clears them getGutenbergContainerFragment().updateCapabilities(mCurrentGutenbergPropsBuilder); trackWebViewClosed("save"); diff --git a/libs/mocks/src/main/assets/mocks/mappings/wpcom/mobile/feature-flags.json b/libs/mocks/src/main/assets/mocks/mappings/wpcom/mobile/feature-flags.json index fceb98ec7b9f..10183cd805b5 100644 --- a/libs/mocks/src/main/assets/mocks/mappings/wpcom/mobile/feature-flags.json +++ b/libs/mocks/src/main/assets/mocks/mappings/wpcom/mobile/feature-flags.json @@ -15,6 +15,9 @@ "marketing_version": { "matches": "(.*)" }, + "os_version": { + "matches": "(.*)" + }, "platform": { "matches": "android" }, diff --git a/version.properties b/version.properties index aa06b747bc3b..eed4110780a6 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -versionName=25.0-rc-1 -versionCode=1434 \ No newline at end of file +versionName=25.1-rc-2 +versionCode=1437 \ No newline at end of file