From 7bf9276d1a090fb8bf72e1047f89416bd231fae0 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:17:27 +0530 Subject: [PATCH 1/5] fix: resolve IndexOutOfBounds error when removing images from top card (#6124) replace deprecated onBackPressed with onBackPressedCallback remove unit test for deprecated onBackPressed method remove if-check before deleting picture to prevent hiding top thumbnail card hide the thumbnail card on fragments other than MediaDetailFragment Co-authored-by: Nicolas Raoul --- .../free/nrw/commons/upload/UploadActivity.kt | 132 ++++++++++-------- .../nrw/commons/upload/UploadPresenter.kt | 47 +++---- .../mediaDetails/UploadMediaDetailFragment.kt | 2 +- .../mediaDetails/UploadMediaPresenter.kt | 12 +- .../commons/upload/UploadActivityUnitTests.kt | 11 -- 5 files changed, 107 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt index 020284934..ee0b21210 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt @@ -14,6 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.CheckBox +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -122,7 +123,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C /** * Set the value of the showPermissionDialog variable. * - * @param showPermissionsDialog `true` to indicate to show + * @property isShowPermissionsDialog `true` to indicate to show * Permissions Dialog if permissions are missing, `false` otherwise. */ /** @@ -166,6 +167,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C private var _binding: ActivityUploadBinding? = null private val binding: ActivityUploadBinding get() = _binding!! + private lateinit var onBackPressedCallback: OnBackPressedCallback + @SuppressLint("CheckResult") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -173,6 +176,23 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C _binding = ActivityUploadBinding.inflate(layoutInflater) setContentView(binding.root) + // Overrides the back button to make sure the user is prepared to lose their progress + onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + showAlertDialog( + this@UploadActivity, + getString(R.string.back_button_warning), + getString(R.string.back_button_warning_desc), + getString(R.string.back_button_continue), + getString(R.string.back_button_warning), + null + ) { + finish() + } + } + } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + /* If Configuration of device is changed then get the new fragments created by the system and populate the fragments ArrayList @@ -187,7 +207,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } init() - binding.rlContainerTitle.setOnClickListener { v: View? -> onRlContainerTitleClicked() } + binding.rlContainerTitle.setOnClickListener { _: View? -> onRlContainerTitleClicked() } nearbyPopupAnswers = mutableMapOf() //getting the current dpi of the device and if it is less than 320dp i.e. overlapping //threshold, thumbnails automatically minimizes @@ -201,7 +221,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } locationManager!!.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) locationManager!!.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) - store = BasicKvStore(this, storeNameForCurrentUploadImagesSize).apply { + store = BasicKvStore(this, STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE).apply { clearAll() } checkStoragePermissions() @@ -241,7 +261,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onPageSelected(position: Int) { currentSelectedPosition = position - if (position >= uploadableFiles!!.size) { + if (position >= uploadableFiles.size) { binding.cvContainerTopCard.visibility = View.GONE } else { thumbnailsAdapter!!.notifyDataSetChanged() @@ -274,7 +294,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .filter { result: Boolean? -> result!! } - .subscribe { result: Boolean? -> + .subscribe { _: Boolean? -> showAlertDialog( this, getString(R.string.block_notification_title), @@ -284,7 +304,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C }) } - fun checkStoragePermissions() { + private fun checkStoragePermissions() { // Check if all required permissions are granted val hasAllPermissions = hasPermission(this, PERMISSIONS_STORAGE) val hasPartialAccess = hasPartialAccess(this) @@ -355,7 +375,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C showLongToast(this, messageResourceId) } - override fun getUploadableFiles(): List? { + override fun getUploadableFiles(): List { return uploadableFiles } @@ -367,6 +387,14 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onUploadMediaDeleted(index: Int) { fragments!!.removeAt(index) //Remove the corresponding fragment uploadableFiles.removeAt(index) //Remove the files from the list + + val isMediaDetailFragment = fragments!!.getOrNull(currentSelectedPosition)?.let { + it is UploadMediaDetailFragment + } ?: false + if(!isMediaDetailFragment) { + // Should hide the top card current fragment is not the media detail fragment + showHideTopCard(false) + } thumbnailsAdapter!!.notifyItemRemoved(index) //Notify the thumbnails adapter uploadImagesAdapter!!.notifyDataSetChanged() //Notify the ViewPager } @@ -375,8 +403,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C binding.tvTopCardTitle.text = resources .getQuantityString( R.plurals.upload_count_title, - uploadableFiles!!.size, - uploadableFiles!!.size + uploadableFiles.size, + uploadableFiles.size ) } @@ -444,15 +472,16 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C receiveInternalSharedItems() } - if (uploadableFiles == null || uploadableFiles!!.isEmpty()) { + if (uploadableFiles.isEmpty()) { handleNullMedia() } else { //Show thumbnails - if (uploadableFiles!!.size > 1) { - if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { //If there is only file, no need to show the image thumbnails + if (uploadableFiles.size > 1) { + if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { + // If there is only file, no need to show the image thumbnails showAlertDialogForCategories() } - if (uploadableFiles!!.size > 3 && + if (uploadableFiles.size > 3 && !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload") ) { showAlertForBattery() @@ -464,8 +493,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C binding.tvTopCardTitle.text = resources .getQuantityString( R.plurals.upload_count_title, - uploadableFiles!!.size, - uploadableFiles!!.size + uploadableFiles.size, + uploadableFiles.size ) @@ -474,7 +503,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } - for (uploadableFile in uploadableFiles!!) { + for (uploadableFile in uploadableFiles) { val uploadMediaDetailFragment = UploadMediaDetailFragment() if (!uploadIsOfAPlace) { @@ -497,8 +526,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C object : UploadMediaDetailFragmentCallback { override fun deletePictureAtIndex(index: Int) { store!!.putInt( - keyForCurrentUploadImagesSize, - (store!!.getInt(keyForCurrentUploadImagesSize) - 1) + KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE, + (store!!.getInt(KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE) - 1) ) presenter!!.deletePictureAtIndex(index) } @@ -576,11 +605,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C fragments!!.add(mediaLicenseFragment!!) } else { for (i in 1 until fragments!!.size) { - fragments!![i]!!.callback = object : UploadBaseFragment.Callback { + fragments!![i].callback = object : UploadBaseFragment.Callback { override fun onNextButtonClicked(index: Int) { if (index < fragments!!.size - 1) { binding.vpUpload.setCurrentItem(index + 1, false) - fragments!![index + 1]!!.onBecameVisible() + fragments!![index + 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset( if ((index > 0)) index - 1 else 0, @@ -594,7 +623,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onPreviousButtonClicked(index: Int) { if (index != 0) { binding.vpUpload.setCurrentItem(index - 1, true) - fragments!![index - 1]!!.onBecameVisible() + fragments!![index - 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset( if ((index > 3)) index - 2 else 0, @@ -632,11 +661,12 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C binding.vpUpload.offscreenPageLimit = fragments!!.size } // Saving size of uploadableFiles - store!!.putInt(keyForCurrentUploadImagesSize, uploadableFiles!!.size) + store!!.putInt(KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE, uploadableFiles.size) } /** - * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. + * Changes current image when one image upload is cancelled, to highlight next image in the top + * thumbnail. * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) * * @param index Index of image to be removed @@ -771,7 +801,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onNextButtonClicked(index: Int) { if (index < fragments!!.size - 1) { binding.vpUpload.setCurrentItem(index + 1, false) - fragments!![index + 1]!!.onBecameVisible() + fragments!![index + 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset(if ((index > 0)) index - 1 else 0, 0) if (index < fragments!!.size - 4) { @@ -786,10 +816,10 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onPreviousButtonClicked(index: Int) { if (index != 0) { binding.vpUpload.setCurrentItem(index - 1, true) - fragments!![index - 1]!!.onBecameVisible() + fragments!![index - 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset(if ((index > 3)) index - 2 else 0, 0) - if ((index != 1) && ((index - 1) < uploadableFiles!!.size)) { + if ((index != 1) && ((index - 1) < uploadableFiles.size)) { // Shows the top card if it was hidden because of the last image being deleted and // now the user has hit previous button to go back to the media details showHideTopCard(true) @@ -797,7 +827,10 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } } - override fun onThumbnailDeleted(position: Int) = presenter!!.deletePictureAtIndex(position) + override fun onThumbnailDeleted(position: Int) { + presenter!!.deletePictureAtIndex(position) + thumbnailsAdapter?.notifyDataSetChanged() + } /** * The adapter used to show image upload intermediate fragments @@ -824,11 +857,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } - fun onRlContainerTitleClicked() { + private fun onRlContainerTitleClicked() { binding.rvThumbnails.visibility = if (isTitleExpanded) View.GONE else View.VISIBLE isTitleExpanded = !isTitleExpanded - binding.ibToggleTopCard.rotation = binding.ibToggleTopCard.rotation + 180 + binding.ibToggleTopCard.rotation += 180 } override fun onDestroy() { @@ -845,21 +878,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C if (uploadCategoriesFragment != null) { uploadCategoriesFragment!!.callback = null } - } - - /** - * Overrides the back button to make sure the user is prepared to lose their progress - */ - @SuppressLint("MissingSuperCall") - override fun onBackPressed() { - showAlertDialog( - this, - getString(R.string.back_button_warning), - getString(R.string.back_button_warning_desc), - getString(R.string.back_button_continue), - getString(R.string.back_button_warning), - null - ) { finish() } + onBackPressedCallback.remove() } /** @@ -879,7 +898,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C .setView(view) .setTitle(getString(R.string.multiple_files_depiction_header)) .setMessage(getString(R.string.multiple_files_depiction)) - .setPositiveButton("OK") { dialog: DialogInterface?, which: Int -> + .setPositiveButton("OK") { _: DialogInterface?, _: Int -> if (checkBox.isChecked) { // Save the user's choice to not show the dialog again defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true) @@ -913,14 +932,14 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C getString(R.string.cancel), { /* Since opening the right settings page might be device dependent, using - https://github.com/WaseemSabir/BatteryPermissionHelper - directly appeared like a promising idea. - However, this simply closed the popup and did not make - the settings page appear on a Pixel as well as a Xiaomi device. - Used the standard intent instead of using this library as - it shows a list of all the apps on the device and allows users to - turn battery optimisation off. - */ + https://github.com/WaseemSabir/BatteryPermissionHelper + directly appeared like a promising idea. + However, this simply closed the popup and did not make + the settings page appear on a Pixel as well as a Xiaomi device. + Used the standard intent instead of using this library as + it shows a list of all the apps on the device and allows users to + turn battery optimisation off. + */ val batteryOptimisationSettingsIntent = Intent( Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS ) @@ -958,7 +977,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C Also, location information is discarded if the difference between current location and location recorded just before capturing the image is greater than 100 meters */ - if (isLocationTagUnchecked || locationDifference > 100 || !defaultKvStore.getBoolean("inAppCameraLocationPref") + if (isLocationTagUnchecked || locationDifference > 100 + || !defaultKvStore.getBoolean("inAppCameraLocationPref") || !isInAppCameraUpload ) { currLocation = null @@ -979,8 +999,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C @JvmField var nearbyPopupAnswers: MutableMap? = null - const val keyForCurrentUploadImagesSize: String = "CurrentUploadImagesSize" - const val storeNameForCurrentUploadImagesSize: String = "CurrentUploadImageQualities" + const val KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE: String = "CurrentUploadImagesSize" + const val STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE: String = "CurrentUploadImageQualities" /** * Sets the flag indicating whether the upload is of a specific place. diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt index 9ee8fb483..5d721f408 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt @@ -146,34 +146,31 @@ class UploadPresenter @Inject internal constructor( override fun deletePictureAtIndex(index: Int) { val uploadableFiles = view.getUploadableFiles() - if (index == uploadableFiles!!.size - 1) { - // If the next fragment to be shown is not one of the MediaDetailsFragment - // lets hide the top card so that it doesn't appear on the other fragments - view.showHideTopCard(false) - } - view.setImageCancelled(true) - repository.deletePicture(uploadableFiles[index].getFilePath()) - if (uploadableFiles.size == 1) { - view.showMessage(R.string.upload_cancelled) - view.finish() - return - } - - presenter.updateImageQualitiesJSON(uploadableFiles.size, index) - view.onUploadMediaDeleted(index) - if (index != uploadableFiles.size && index != 0) { - // if the deleted image was not the last item to be uploaded, check quality of next - repository.getUploadItem(index)?.let { - presenter.checkImageQuality(it, index) + uploadableFiles?.let { + view.setImageCancelled(true) + repository.deletePicture(uploadableFiles[index].getFilePath()) + if (uploadableFiles.size == 1) { + view.showMessage(R.string.upload_cancelled) + view.finish() + return } - } - if (uploadableFiles.size < 2) { - view.showHideTopCard(false) - } + presenter.updateImageQualitiesJSON(uploadableFiles.size, index) + view.onUploadMediaDeleted(index) + if (index != uploadableFiles.size && index != 0) { + // if the deleted image was not the last item to be uploaded, check quality of next + repository.getUploadItem(index)?.let { + presenter.checkImageQuality(it, index) + } + } - //In case lets update the number of uploadable media - view.updateTopCardTitle() + if (uploadableFiles.size < 2) { + view.showHideTopCard(false) + } + + //In case lets update the number of uploadable media + view.updateTopCardTitle() + } } override fun onAttachView(view: UploadContract.View) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt index af850a7e3..4a4c13ba7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt @@ -532,7 +532,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra basicKvStore!!.putBoolean(keyForShowingAlertDialog, false) if (isInternetConnectionEstablished(requireActivity())) { val sizeOfUploads = basicKvStore!!.getInt( - UploadActivity.keyForCurrentUploadImagesSize + UploadActivity.KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE ) for (i in indexOfFragment until sizeOfUploads) { presenter.getImageQuality( diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt index 90c426091..77999cf2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt @@ -310,7 +310,7 @@ class UploadMediaPresenter @Inject constructor( private fun storeImageQuality( imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem ) { - val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize) + val store = BasicKvStore(activity, UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) val value = store.getString(UPLOAD_QUALITIES_KEY, null) try { val jsonObject = value.asJsonObject().apply { @@ -339,8 +339,10 @@ class UploadMediaPresenter @Inject constructor( */ override fun checkImageQuality(uploadItem: UploadItem, index: Int) { if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) { - val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) } + + val value = basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) } ?.getString(UPLOAD_QUALITIES_KEY, null) + try { val imageQuality = value.asJsonObject()["UploadItem$index"] as Int view.showProgress(false) @@ -363,8 +365,9 @@ class UploadMediaPresenter @Inject constructor( * @param index Index of the UploadItem which was deleted */ override fun updateImageQualitiesJSON(size: Int, index: Int) { - val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) } + val value = basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) } ?.getString(UPLOAD_QUALITIES_KEY, null) + try { val jsonObject = value.asJsonObject().apply { for (i in index until (size - 1)) { @@ -372,7 +375,8 @@ class UploadMediaPresenter @Inject constructor( } remove("UploadItem" + (size - 1)) } - basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) } + + basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) } ?.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString()) } catch (e: Exception) { Timber.e(e) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt index 1173d09b0..97fe68862 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt @@ -262,15 +262,4 @@ class UploadActivityUnitTests { method.isAccessible = true method.invoke(activity) } - - @Test - @Throws(Exception::class) - fun testOnBackPressed() { - val method: Method = - UploadActivity::class.java.getDeclaredMethod( - "onBackPressed", - ) - method.isAccessible = true - method.invoke(activity) - } } From 56fa8ceb5a8768c29de0034d2690755210c48945 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 7 Apr 2025 14:01:50 +0200 Subject: [PATCH 2/5] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ja/strings.xml | 4 ++-- app/src/main/res/values-ku/strings.xml | 2 +- app/src/main/res/values-ps/strings.xml | 9 ++++++++- app/src/main/res/values-pt-rBR/strings.xml | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8ef85d363..5219cf91e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -119,7 +119,7 @@ ログインできません - ネットワークのエラーです 失敗した回数が多すぎます。数分後にもう一度お試しください。 申し訳ありませんが、この利用者はコモンズでブロックされています。 - 2段階認証コードを入力してください。 + 二要素認証コードを入力してください。 ログイン失敗 アップロード このセットに名前をつけてください @@ -221,7 +221,7 @@ 情報なし ベータ版テスターになる Google Playのベータ版チャンネルにオプトインして、新機能やバグ修正プログラムに早期にアクセス - 2段階認証コード + 2FAコード ログアウトしてもよろしいですか? メディアイメージが失敗しました 下位カテゴリは見つかりませんでした diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 2dc2fb995..54364b533 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -179,7 +179,7 @@ Spasî Hate Wergirtin Wêneyên Bijartî Wêneyên bi riya \"Cihên Nêz\" - Derece + Derece %d Wêneyên Barkirî Wêneyê din Belê, çima na diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index eda0d954e..31e12027e 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -47,7 +47,14 @@ %d upload %d پورته کول - دا انځور به د %1$s په منښتليک سمبال وي. + + دا انځور به د منښتليک %1$s لاندې وي + دا انځورونه به د %1$s منښتليک لاندې وي + + + %1$d راپورته کول + %1$d راپورته کېدنې + سپړنه ښکارېدنه ټولګړی diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2949a707a..021a8ca3c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -696,7 +696,7 @@ WLM Essa imagem será enviada ao concurso Wiki Loves Monuments Monumentos de exibição - Estamos no mês no Wiki Loves Monuments! + Chegou o mês do Wiki Loves Monuments! SABER MAIS Wiki Loves Monuments O Wiki Loves Monuments é um concurso internacional organizado pela Wikimedia sobre fotografias de monumentos From 2eed44146275dfaa6aa78a40e49c456a2df53987 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 8 Apr 2025 07:59:28 -0400 Subject: [PATCH 3/5] Enable EmailAuth support. (#6277) --- .../fr/free/nrw/commons/auth/LoginActivity.kt | 15 ++++++++-- .../nrw/commons/auth/csrf/CsrfTokenClient.kt | 13 ++++++++- .../nrw/commons/auth/login/LoginCallback.kt | 7 +++++ .../nrw/commons/auth/login/LoginClient.kt | 28 +++++++++++++++---- .../nrw/commons/auth/login/LoginInterface.kt | 3 +- .../nrw/commons/auth/login/LoginResponse.kt | 15 ++++++---- .../nrw/commons/auth/login/LoginResult.kt | 7 +++++ app/src/main/res/layout/activity_login.xml | 3 +- app/src/main/res/values/strings.xml | 2 ++ 9 files changed, 76 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt index 75c4ac26d..840bc7ca3 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -65,6 +65,7 @@ class LoginActivity : AccountAuthenticatorActivity() { private val delegate: AppCompatDelegate by lazy { AppCompatDelegate.create(this, null) } + private var lastLoginResult: LoginResult? = null public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -271,6 +272,7 @@ class LoginActivity : AccountAuthenticatorActivity() { showLoggingProgressBar() loginClient.doLogin(username, password, + lastLoginResult, twoFactorCode, Locale.getDefault().language, object : LoginCallback { @@ -280,9 +282,17 @@ class LoginActivity : AccountAuthenticatorActivity() { onLoginSuccess(loginResult) } - override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread { + override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { Timber.d("Requesting 2FA prompt") progressDialog!!.dismiss() + lastLoginResult = loginResult + askUserForTwoFactorAuth() + } + + override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) { + Timber.d("Requesting email auth prompt") + progressDialog!!.dismiss() + lastLoginResult = loginResult askUserForTwoFactorAuth() } @@ -341,12 +351,13 @@ class LoginActivity : AccountAuthenticatorActivity() { progressDialog!!.dismiss() with(binding!!) { twoFactorContainer.visibility = View.VISIBLE + twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) loginTwoFactor.visibility = View.VISIBLE loginTwoFactor.requestFocus() } val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) - showMessageAndCancelDialog(R.string.login_failed_2fa_needed) + showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) } @VisibleForTesting diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt index f35e5f003..6353e54ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt @@ -32,7 +32,7 @@ class CsrfTokenClient( try { if (retry > 0) { // Log in explicitly - loginClient.loginBlocking(userName, password, "") + loginClient.loginBlocking(userName, password) } // Get CSRFToken response off the main thread. @@ -92,6 +92,8 @@ class CsrfTokenClient( override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } override fun twoFactorPrompt() = cb.twoFactorPrompt() + + override fun emailAuthPrompt() = cb.emailAuthPrompt() }, ) @@ -165,10 +167,17 @@ class CsrfTokenClient( } override fun twoFactorPrompt( + loginResult: LoginResult, caught: Throwable, token: String?, ) = callback.twoFactorPrompt() + override fun emailAuthPrompt( + loginResult: LoginResult, + caught: Throwable, + token: String?, + ) = callback.emailAuthPrompt() + // Should not happen here, but call the callback just in case. override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password.")) @@ -190,6 +199,8 @@ class CsrfTokenClient( fun failure(caught: Throwable?) fun twoFactorPrompt() + + fun emailAuthPrompt() } companion object { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt index 8092f73ae..8aa3d17a0 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt @@ -4,6 +4,13 @@ interface LoginCallback { fun success(loginResult: LoginResult) fun twoFactorPrompt( + loginResult: LoginResult, + caught: Throwable, + token: String?, + ) + + fun emailAuthPrompt( + loginResult: LoginResult, caught: Throwable, token: String?, ) diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt index 2a799c847..a653b8b55 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.auth.login import android.text.TextUtils +import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL @@ -51,6 +52,7 @@ class LoginClient( password, null, null, + null, response.body()!!.query()!!.loginToken(), userLanguage, cb, @@ -75,6 +77,7 @@ class LoginClient( password: String, retypedPassword: String?, twoFactorCode: String?, + emailAuthCode: String?, loginToken: String?, userLanguage: String, cb: LoginCallback, @@ -82,7 +85,7 @@ class LoginClient( this.userLanguage = userLanguage loginCall = - if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { + if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) } else { loginInterface.postLogIn( @@ -90,6 +93,7 @@ class LoginClient( password, retypedPassword, twoFactorCode, + emailAuthCode, loginToken, userLanguage, true, @@ -112,10 +116,18 @@ class LoginClient( when (loginResult) { is OAuthResult -> cb.twoFactorPrompt( + loginResult, LoginFailedException(loginResult.message), loginToken, ) + is EmailAuthResult -> + cb.emailAuthPrompt( + loginResult, + LoginFailedException(loginResult.message), + loginToken + ) + is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) is LoginResult.Result -> @@ -147,6 +159,7 @@ class LoginClient( fun doLogin( username: String, password: String, + lastLoginResult: LoginResult?, twoFactorCode: String, userLanguage: String, loginCallback: LoginCallback, @@ -159,7 +172,10 @@ class LoginClient( ) = if (response.isSuccessful) { val loginToken = response.body()?.query()?.loginToken() loginToken?.let { - login(username, password, null, twoFactorCode, it, userLanguage, loginCallback) + login(username, password, null, + if (lastLoginResult is OAuthResult) twoFactorCode else null, + if (lastLoginResult is EmailAuthResult) twoFactorCode else null, + it, userLanguage, loginCallback) } ?: run { loginCallback.error(IOException("Failed to retrieve login token")) } @@ -181,7 +197,8 @@ class LoginClient( fun loginBlocking( userName: String, password: String, - twoFactorCode: String?, + twoFactorCode: String? = null, + emailAuthCode: String? = null ) { val tokenResponse = getLoginToken().execute() if (tokenResponse @@ -195,7 +212,7 @@ class LoginClient( val loginToken = tokenResponse.body()?.query()?.loginToken() val tempLoginCall = - if (twoFactorCode.isNullOrEmpty()) { + if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) { loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) } else { loginInterface.postLogIn( @@ -203,6 +220,7 @@ class LoginClient( password, null, twoFactorCode, + emailAuthCode, loginToken, userLanguage, true, @@ -214,7 +232,7 @@ class LoginClient( val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.") if ("UI" == loginResult.status) { - if (loginResult is OAuthResult) { + if (loginResult is OAuthResult || loginResult is EmailAuthResult) { // TODO: Find a better way to boil up the warning about 2FA throw LoginFailedException(loginResult.message) } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt index 07e1cd45c..39cbf7c9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt @@ -35,7 +35,8 @@ interface LoginInterface { @Field("password") pass: String?, @Field("retype") retypedPass: String?, @Field("OATHToken") twoFactorCode: String?, - @Field("logintoken") token: String?, + @Field("token") emailAuthToken: String?, + @Field("logintoken") loginToken: String?, @Field("uselang") userLanguage: String?, @Field("logincontinue") loginContinue: Boolean, ): Call diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt index a96778e38..0fb035eea 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt @@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login import com.google.gson.annotations.SerializedName import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult +import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult import fr.free.nrw.commons.auth.login.LoginResult.Result import fr.free.nrw.commons.wikidata.mwapi.MwServiceError @@ -27,11 +28,13 @@ internal class ClientLogin { fun toLoginResult(password: String): LoginResult { var userMessage = message if ("UI" == status) { - if (requests != null) { - for (req in requests) { - if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) { + requests?.forEach { request -> + request.id()?.let { + if (it.endsWith("TOTPAuthenticationRequest")) { return OAuthResult(status, userName, password, message) - } else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) { + } else if (it.endsWith("EmailAuthAuthenticationRequest")) { + return EmailAuthResult(status, userName, password, message) + } else if (it.endsWith("PasswordAuthenticationRequest")) { return ResetPasswordResult(status, userName, password, message) } } @@ -49,7 +52,7 @@ internal class Request { private val required: String? = null private val provider: String? = null private val account: String? = null - private val fields: Map? = null + internal val fields: Map? = null fun id(): String? = id } @@ -57,5 +60,5 @@ internal class Request { internal class RequestField { private val type: String? = null private val label: String? = null - private val help: String? = null + internal val help: String? = null } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt index 6a7594ec0..99abaeeec 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt @@ -24,6 +24,13 @@ sealed class LoginResult( message: String?, ) : LoginResult(status, userName, password, message) + class EmailAuthResult( + status: String, + userName: String?, + password: String?, + message: String?, + ) : LoginResult(status, userName, password, message) + class ResetPasswordResult( status: String, userName: String?, diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 7cfd761a7..0da9f5d9f 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -146,6 +146,7 @@ android:layout_marginEnd="@dimen/standard_gap" android:layout_marginRight="@dimen/standard_gap" android:layout_marginBottom="@dimen/standard_gap" + android:hint="@string/_2fa_code" android:visibility="gone" app:passwordToggleEnabled="false" tools:visibility="visible"> @@ -154,9 +155,7 @@ android:id="@+id/login_two_factor" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/_2fa_code" android:imeOptions="flagNoExtractUi" - android:inputType="number" android:visibility="gone" tools:visibility="visible" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9b16512f..805649dff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,7 @@ Too many unsuccessful attempts. Please try again in a few minutes. Sorry, this user has been blocked on Commons You must provide your two factor authentication code. + A login verification code has been sent to your email address. Please provide the code to log in. Log-in failed Upload Name this set @@ -218,6 +219,7 @@ Opt-in to our beta channel on Google Play and get early access to new features and bug fixes https://play.google.com/apps/testing/fr.free.nrw.commons 2FA Code + Email verification code Do you really want to logout? Media Image Failed No subcategories found From 262efe4d8c1b0a5bdb6d36d68cb9be0581dd4fd1 Mon Sep 17 00:00:00 2001 From: Jason-Whitmore Date: Wed, 9 Apr 2025 22:19:31 -0700 Subject: [PATCH 4/5] ExploreMapFragment.java: fix removeMarker() to remove the correct marker (#6279) Before this change, the removeMarker() method would determine the correct overlay to remove by comparing an overlay's Place coordinates with the target BaseMarker's Place coordinates. If two different markers had the same Place coordinates, the incorrect marker would be removed, leading to more than one green label appearing on the screen. After this change, the removeMarker() method now compares an overlay's title with the BaseMarker's Place name. removeMarker() will work properly as long as all BaseMarker's Place names are unique. Also, null checks were added to removeMarker(). --- .../free/nrw/commons/explore/map/ExploreMapFragment.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java index f5657dd1b..60758ac20 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java @@ -775,7 +775,11 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be removed. */ private void removeMarker(BaseMarker nearbyBaseMarker) { - Place place = nearbyBaseMarker.getPlace(); + if (nearbyBaseMarker == null || nearbyBaseMarker.getPlace().getName() == null) { + return; + } + + String target = nearbyBaseMarker.getPlace().getName(); List overlays = binding.mapView.getOverlays(); ItemizedOverlayWithFocus item; @@ -784,8 +788,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment item = (ItemizedOverlayWithFocus) overlays.get(i); OverlayItem overlayItem = item.getItem(0); - if (place.location.getLatitude() == overlayItem.getPoint().getLatitude() - && place.location.getLongitude() == overlayItem.getPoint().getLongitude()) { + if (overlayItem.getTitle().equals(target)) { binding.mapView.getOverlays().remove(i); binding.mapView.invalidate(); break; From e3dd00bcfa908741816a240cd1c5acb3297262f6 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 10 Apr 2025 14:01:48 +0200 Subject: [PATCH 5/5] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 2 ++ app/src/main/res/values-da/strings.xml | 2 ++ app/src/main/res/values-lb/strings.xml | 2 ++ app/src/main/res/values-mk/strings.xml | 2 ++ app/src/main/res/values-pms/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ 6 files changed, 12 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 577b031cb..5c9e7bec9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -168,6 +168,7 @@ الكثير من المحاولات غير الناجحة. الرجاء المحاولة مرة أخرى في بضع دقائق. عذراً، لقد تم منع هذا المستخدم على كومنز يجب توفير رمز التحقق المزدوج. + تم إرسال رمز التحقق إلى بريدك الإلكتروني. يُرجى إدخال الرمز لتسجيل الدخول. فشل تسجيل الدخول ارفع اسم هذه المجموعة @@ -273,6 +274,7 @@ انضم لمختبري اصدارات Beta (بيتا) يمكنك الاشتراك في القناة التجريبية على جوجل بلاي والحصول على إمكانية الوصول المبكر إلى الميزات الجديدة وإصلاحات الأخطاء رمز التحقق المزدوج 2FA + رمز التحقق من البريد الإلكتروني أترغب فعلا في الخروج؟ صورة الوسائط فشلت لم يتم العثور على تصنيفات فرعية. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 232f56c49..2e535b879 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -119,6 +119,7 @@ For mange mislykkede forsøg. Prøv igen om et par minutter. Beklager, denne bruger er blevet blokeret på Commons Du skal angive din tofaktorgodkendelseskode. + En login-bekræftelseskode er blevet sendt til din e-mailadresse. Angiv koden for at logge ind. Login mislykkedes Upload Navngiv dette sæt @@ -224,6 +225,7 @@ Bliv betatester Registrer dig på vores betakanal på Google Play og få tidlig adgang til nye funktioner og fejlrettelser 2FA-kode + E-mail-bekræftelseskode Ønsker du at logge ud? Mediebillede mislykkedes Ingen underkategorier fundet diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 65574cdb9..afca8fcd4 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -99,6 +99,7 @@ Ze dacks ouni Succès probéiert. Probéiert wgl. an e puer Minutten nach eng Kéier. Pardon, dëse Benotzer ass op Commons gespaart Dir musst de Code vun Ärer Zwee-Facteur-Authentifizéierung uginn. + Dir hutt ee Login-Verifikatiounscode per E-Mail geschéckt kritt. Gitt wgl. de Code an, fir Iech anzeloggen. Aloggen huet net funktionéiert Eroplueden Gitt dëser Biller een Numm @@ -192,6 +193,7 @@ Beta-Tester ginn Schreift Iech op GooglePlay a fir eise Beta-Kanal a kritt fréi Zougang zu neie Funktiounen a Verbesserunge vu Feeler 2FA-Code + E-Mail-Verifikatiounscode Wëllt dir Iech wierklech ausloggen? Keng Ënnerkategorie fonnt Bierg Zao diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 94e8f638b..2a48ddd1e 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -113,6 +113,7 @@ Направени се премногу неуспешни обиди. Обидете се пак за некоја минута. За жал, корисникот е блокиран на Ризницата Мора да го укажете вашиот код за двочинителска заверка. + На вашата е-поштенска адреса ви испративме потврден код за најава. Внесете го кодот за да се најавите. Најавата не успеа Подигни Дајте му име на овој комплет @@ -218,6 +219,7 @@ Станете бета-испробувач Пријавете се на нашиот бета-канал на Google Play и добивајте ран пристап до нови можности и исправки на грешки 2ЧЗ-код + Испрати го потврдниот код на е-пошта Дали навистина сакате да се одјавите? Сликата не успеа Не пронајдов поткатегории diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 76a0be425..d6dd2b3ff 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -111,6 +111,7 @@ Tròpi tentativ falì. Për piasì, ch\'a preuva torna da-sì chèiche minute. An dëspias, s\'utent-sì a l\'é stàit blocà ansima a Commons A dev fornì sò còdes d\'autentificassion a doi fator. + Un còdes ëd verìfica ëd conession a l\'é stàit mandà a soa adrëssa ëd pòsta eletrònica. Për piasì, ch\'a anserissa col còdes për intré ant ël sistema. Falì a rintré ant ël sistema Carié Deje un nòm a s\'ansem @@ -216,6 +217,7 @@ Dventé në sperimentador Beta Anscriv-se a nòstr canal beta su Google Play a oten-e n\'acess antissipà a le neuve fonsionalità e coression ëd givo Còdes 2FA + Mandé un còdes ëd verifica Veul-lo për da bon seurte dal sistema? Faliment ëd la plancia dël mojen Gnun-e sot-categorìe trovà diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ca11a4ec9..b2fd85c23 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -136,6 +136,7 @@ 失敗次數過多。請於幾分鐘後重試。 很抱歉,該使用者已被維基共享資源封禁 必須提供您的雙重驗證代碼。 + 登入驗證碼已發送到您的電子郵件地址。請提供驗證碼以登入。 登入失敗 上傳 給這個集合命名 @@ -241,6 +242,7 @@ 成為 Beta 測試員 選擇加入我們在 Google Play 上的測試版頻道並儘早訪問新功能和錯誤修復 2FA 代碼 + 電子郵件驗證碼 您確定要登出嗎? 媒體圖片失敗 找不到子分類