diff --git a/CHANGELOG.md b/CHANGELOG.md index 59134b236..e7accf82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,89 @@ # Wikimedia Commons for Android +## v5.1.2 + +### What's changed + +* Fix the broken category search in the explore screen + +## v5.1.1 + +### What's changed + +* Use Android's new EXIF interface to mitigate security issues in old + EXIF interface. +* Make the icon that helps view the upload queue always visible as it ensures + that the queue accessible at all times. + +## v5.1.0 + +### What's Changed + +* Enhanced **upload queue management** in the Commons app for smoother, sequential + processing, clearer progress tracking, prevention of stuck or duplicate + uploads. As part of this improvement, the "Limited Connection mode" has been + removed. +* Added an option in "Nearby" feature enabling users to **provide feedback on + Wikidata items**. Users can report if an item doesn’t exist, is at a different + location, or has other issues, with submissions tagged for easy tracking and + updates. +* Improved the "Nearby" feature by splitting the query into two parts for faster + loading and **better performance, especially in areas with dense amount of + places**. This update also resolves issues with pins overlapping place names. +* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to + the app such as adding **"Partial Access" support**. Also includes some minor + refactoring, and replacement of deprecated circular progress bars. +* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs + appeared blank** in the Category Details screen. Resolved by optimizing view + binding handling in the parent fragments. +* Fixed an issue where editing depictions removed all other structured data from + images. Now, **only depictions are updated, preserving other associated data**. +* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location** + from pictures and ensured "Show in map app" accurately reflects this location. +* Fixed navigation **after uploading via Nearby by directing users to the Uploads + activity** instead of returning to Nearby, preventing confusion about needing to + upload again. + +### Bug fixes and various changes + +* Improved the "Nearby" feature to fetch labels based on the user's preferred + language instead of defaulting to English. +* Added a legend to the "Nearby" feature indicating pin statuses: red for items + without pictures, green for those with pictures, and grey for items being + checked. A floating action button now allows users to toggle the legend's + visibility. +* Fixed an issue where the "Nominate for deletion" option is shown to logged out + users, preventing app errors and crashes. +* Updated the regex pattern that filters categories with an year in it to also + filter the 2020s. +* Fix an issue where past depictions were not shown as suggestions, despite + being saved correctly. +* Fixed an issue in custom image picker where exiting the media preview showed + only the first image and cleared selections. Now, previously selected images + are restored correctly after exiting the preview. This was contributed. +* Fixed an issue in custom image picker where scrolling behavior did not + maintain position after exiting fullscreen preview, ensuring users remain at + the same point in their image roll unless actioned images are filtered. This + was contributed. +* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll + threshold and adding an 800ms debounce for smoother pin updates when the map + is moved. Queued searches are now canceled on fragment destruction. +* Revised author information retrieval to emphasize the custom author name from + the metadata instead of the default registered username. +* Enhanced notification classification to properly identify "email" type + notifications and prompting users to check their e-mail inbox when such + notifications are clicked. +* Resolved a bug in the language chooser that incorrectly greyed-out previously + selected languages, ensuring only the current language is non-selectable during + image upload. +* Resolved pin color update issue in "Nearby" feature where the pin colour + failed to be updated after a successful image upload. + +What's listed here is only a subset of all the changes. Check the full-list of +the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0). +Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0) +for an exhaustive list of changes and the various contributors who contributed the same. + ## v5.0.2 - Enhanced multi-upload functionality with user prompts to clarify that all images would share the diff --git a/app/build.gradle b/app/build.gradle index f50a4e6dd..1cc594e7b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -212,8 +212,8 @@ android { defaultConfig { //applicationId 'fr.free.nrw.commons' - versionCode 1040 - versionName '5.0.2' + versionCode 1043 + versionName '5.1.2' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion 21 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e7c64f929..fb776920e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -262,4 +262,4 @@ android:required="false" /> - \ No newline at end of file + diff --git a/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt index 284c84caf..0583ae2f9 100644 --- a/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.webkit.ConsoleMessage +import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebView @@ -28,13 +29,20 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import fr.free.nrw.commons.R +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import okhttp3.HttpUrl.Companion.toHttpUrl import timber.log.Timber +import javax.inject.Inject /** * SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and * closes itself when a specified success URL is reached to success url. */ class SingleWebViewActivity : ComponentActivity() { + @Inject + lateinit var cookieJar: CommonsCookieJar + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,6 +52,11 @@ class SingleWebViewActivity : ComponentActivity() { finish() return } + ApplicationlessInjection + .getInstance(applicationContext) + .commonsApplicationComponent + .inject(this) + setCookies(url) enableEdgeToEdge() setContent { Scaffold( @@ -131,6 +144,7 @@ class SingleWebViewActivity : ComponentActivity() { override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) + setCookies(url.orEmpty()) } } @@ -152,6 +166,20 @@ class SingleWebViewActivity : ComponentActivity() { } + /** + * Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`. + * + * @param url The URL for which cookies need to be set. + */ + private fun setCookies(url: String) { + CookieManager.getInstance().let { + val cookies = cookieJar.loadForRequest(url.toHttpUrl()) + for (cookie in cookies) { + it.setCookie(url, cookie.toString()) + } + } + } + companion object { private const val VANISH_ACCOUNT_URL = "VanishAccountUrl" private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl" diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt index 7e6fee2fc..fd90be95f 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt @@ -127,30 +127,64 @@ class CategoriesModel /** * Fetches details of every category associated with selected depictions, converts them into * CategoryItem and returns them in a list. + * If a selected depiction has no categories, the categories in which its P18 belongs are + * returned in the list. * * @param selectedDepictions selected DepictItems * @return List of CategoryItem associated with selected depictions */ - private fun categoriesFromDepiction(selectedDepictions: List): Observable>? = - Observable - .fromIterable( - selectedDepictions.map { it.commonsCategories }.flatten(), - ).map { categoryItem -> - categoryClient - .getCategoriesByName( - categoryItem.name, - categoryItem.name, - SEARCH_CATS_LIMIT, - ).map { - CategoryItem( - it[0].name, - it[0].description, - it[0].thumbnail, - it[0].isSelected, - ) - }.blockingGet() - }.toList() - .toObservable() + private fun categoriesFromDepiction(selectedDepictions: List): Observable>? { + val observables = selectedDepictions.map { depictedItem -> + if (depictedItem.commonsCategories.isEmpty()) { + if (depictedItem.primaryImage == null) { + return@map Observable.just(emptyList()) + } + Observable.just( + depictedItem.primaryImage + ).map { image -> + categoryClient + .getCategoriesOfImage( + image, + SEARCH_CATS_LIMIT, + ).map { + it.map { category -> + CategoryItem( + category.name, + category.description, + category.thumbnail, + category.isSelected, + ) + } + }.blockingGet() + }.flatMapIterable { it }.toList() + .toObservable() + } else { + Observable + .fromIterable( + depictedItem.commonsCategories, + ).map { categoryItem -> + categoryClient + .getCategoriesByName( + categoryItem.name, + categoryItem.name, + SEARCH_CATS_LIMIT, + ).map { + CategoryItem( + it[0].name, + it[0].description, + it[0].thumbnail, + it[0].isSelected, + ) + }.blockingGet() + }.toList() + .toObservable() + } + } + return Observable.concat(observables) + .scan(mutableListOf()) { accumulator, currentList -> + accumulator.apply { addAll(currentList) } + } + } /** * Fetches details of every category by their name, converts them into diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 5571e0ea7..b031f12f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -78,6 +78,24 @@ class CategoryClient ), ) + /** + * Fetches categories belonging to an image (P18 of some wikidata entity). + * + * @param image P18 of some wikidata entity + * @param itemLimit How many categories to return + * @return Single Observable emitting the list of categories + */ + fun getCategoriesOfImage( + image: String, + itemLimit: Int, + ): Single> = + responseMapper( + categoryInterface.getCategoriesByTitles( + "File:${image}", + itemLimit, + ), + ) + /** * The method takes categoryName as input and returns a List of Subcategories * It uses the generator query API to get the subcategories in a category, 500 at a time. diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt index aa5ac8c3a..3888ef889 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt @@ -61,6 +61,21 @@ interface CategoryInterface { @Query("gacoffset") offset: Int, ): Single + /** + * Fetches non-hidden categories by titles. + * + * @param titles titles to fetch categories for (e.g. File:) + * @param itemLimit How many categories to return + * @return MwQueryResponse + */ + @GET( + "w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70&gclshow=!hidden", + ) + fun getCategoriesByTitles( + @Query("titles") titles: String?, + @Query("gcllimit") itemLimit: Int, + ): Single + @GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50") fun getSubCategoryList( @Query("gcmtitle") categoryName: String, diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt index 92fab56af..d623730ab 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt @@ -101,7 +101,7 @@ data class Contribution constructor( */ fun formatCaptions(uploadMediaDetails: List) = uploadMediaDetails - .associate { it.languageCode!! to it.captionText!! } + .associate { it.languageCode!! to it.captionText } .filter { it.value.isNotBlank() } /** diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 942946a6b..44cefe4d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -267,11 +267,11 @@ class DescriptionEditActivity : applicationContext, media, mediaDetail.languageCode!!, - mediaDetail.captionText!!, + mediaDetail.captionText, ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { s: Boolean? -> - updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText!! + updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText media.captions = updatedCaptions Timber.d("Caption is added.") }, diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt index b0c0c4d37..94319060b 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt @@ -6,6 +6,7 @@ import dagger.android.AndroidInjectionModule import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.activity.SingleWebViewActivity import fr.free.nrw.commons.auth.LoginActivity import fr.free.nrw.commons.contributions.ContributionsModule import fr.free.nrw.commons.explore.SearchModule @@ -51,6 +52,8 @@ interface CommonsApplicationComponent : AndroidInjector ) { - updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText!! + updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText media!!.captions = updatedCaptions } @@ -1737,10 +1754,11 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C return } ProfileActivity.startYourself( - activity, - media!!.user, - sessionManager.userName != media!!.user + requireActivity(), // Ensure this is a non-null Activity context + media?.user ?: "", // Provide a fallback value if media?.user is null + sessionManager.userName != media?.user // This can remain as is, null check will apply ) + } /** diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java deleted file mode 100644 index 4a08760af..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ /dev/null @@ -1,265 +0,0 @@ -package fr.free.nrw.commons.profile; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.databinding.ActivityProfileBinding; -import fr.free.nrw.commons.profile.achievements.AchievementsFragment; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DialogUtil; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; - -/** - * This activity will set two tabs, achievements and - * each tab will have their own fragments - */ -public class ProfileActivity extends BaseActivity { - - private FragmentManager supportFragmentManager; - - public ActivityProfileBinding binding; - - @Inject - SessionManager sessionManager; - - private ViewPagerAdapter viewPagerAdapter; - private AchievementsFragment achievementsFragment; - private LeaderboardFragment leaderboardFragment; - - public static final String KEY_USERNAME ="username"; - public static final String KEY_SHOULD_SHOW_CONTRIBUTIONS ="shouldShowContributions"; - - String userName; - private boolean shouldShowContributions; - - ContributionsFragment contributionsFragment; - - public void setScroll(boolean canScroll){ - binding.viewPager.setCanScroll(canScroll); - } - @Override - protected void onRestoreInstanceState(final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - if (savedInstanceState != null) { - userName = savedInstanceState.getString(KEY_USERNAME); - shouldShowContributions = savedInstanceState.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS); - } - } - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityProfileBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbarBinding.toolbar); - - - binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> { - onSupportNavigateUp(); - }); - - userName = getIntent().getStringExtra(KEY_USERNAME); - setTitle(userName); - shouldShowContributions = getIntent().getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false); - - supportFragmentManager = getSupportFragmentManager(); - viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.tabLayout.setupWithViewPager(binding.viewPager); - setTabs(); - } - - /** - * Navigate up event - * @return boolean - */ - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * Creates a way to change current activity to AchievementActivity - * - * @param context - */ - public static void startYourself(final Context context, final String userName, - final boolean shouldShowContributions) { - Intent intent = new Intent(context, ProfileActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(KEY_USERNAME, userName); - intent.putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions); - context.startActivity(intent); - } - - /** - * Set the tabs for the fragments - */ - private void setTabs() { - List fragmentList = new ArrayList<>(); - List titleList = new ArrayList<>(); - achievementsFragment = new AchievementsFragment(); - Bundle achievementsBundle = new Bundle(); - achievementsBundle.putString(KEY_USERNAME, userName); - achievementsFragment.setArguments(achievementsBundle); - fragmentList.add(achievementsFragment); - - titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase()); - leaderboardFragment = new LeaderboardFragment(); - Bundle leaderBoardBundle = new Bundle(); - leaderBoardBundle.putString(KEY_USERNAME, userName); - leaderboardFragment.setArguments(leaderBoardBundle); - - fragmentList.add(leaderboardFragment); - titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); - - contributionsFragment = new ContributionsFragment(); - Bundle contributionsListBundle = new Bundle(); - contributionsListBundle.putString(KEY_USERNAME, userName); - contributionsFragment.setArguments(contributionsListBundle); - fragmentList.add(contributionsFragment); - titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); - - viewPagerAdapter.setTabData(fragmentList, titleList); - viewPagerAdapter.notifyDataSetChanged(); - - } - - @Override - public void onDestroy() { - super.onDestroy(); - getCompositeDisposable().clear(); - } - - /** - * To inflate menu - * @param menu Menu - * @return boolean - */ - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - final MenuInflater menuInflater = getMenuInflater(); - menuInflater.inflate(R.menu.menu_about, menu); - return super.onCreateOptionsMenu(menu); - } - - /** - * To receive the id of selected item and handle further logic for that selected item - * @param item MenuItem - * @return boolean - */ - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - // take screenshot in form of bitmap and show it in Alert Dialog - if (item.getItemId() == R.id.share_app_icon) { - final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); - final Bitmap screenShot = Utils.getScreenShot(rootView); - showAlert(screenShot); - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * It displays the alertDialog with Image of screenshot - * @param screenshot screenshot of the present screen - */ - public void showAlert(final Bitmap screenshot) { - final LayoutInflater factory = LayoutInflater.from(this); - final View view = factory.inflate(R.layout.image_alert_layout, null); - final ImageView screenShotImage = view.findViewById(R.id.alert_image); - screenShotImage.setImageBitmap(screenshot); - final TextView shareMessage = view.findViewById(R.id.alert_text); - shareMessage.setText(R.string.achievements_share_message); - DialogUtil.showAlertDialog(this, - null, - null, - getString(R.string.about_translate_proceed), - getString(R.string.cancel), - () -> shareScreen(screenshot), - () -> {}, - view - ); - } - - /** - * To take bitmap and store it temporary storage and share it - * @param bitmap bitmap of screenshot - */ - void shareScreen(final Bitmap bitmap) { - try { - final File file = new File(getExternalCacheDir(), "screen.png"); - final FileOutputStream fileOutputStream = new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); - fileOutputStream.flush(); - fileOutputStream.close(); - file.setReadable(true, false); - - final Uri fileUri = FileProvider - .getUriForFile(getApplicationContext(), - getPackageName() + ".provider", file); - grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_STREAM, fileUri); - intent.setType("image/png"); - startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); - } catch (final IOException e) { - e.printStackTrace(); - } - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - outState.putString(KEY_USERNAME, userName); - outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions); - super.onSaveInstanceState(outState); - } - - @Override - public void onBackPressed() { - // Checking if MediaDetailPagerFragment is visible, If visible then show ContributionListFragment else close the ProfileActivity - if(contributionsFragment != null && contributionsFragment.getMediaDetailPagerFragment() != null && contributionsFragment.getMediaDetailPagerFragment().isVisible()) { - contributionsFragment.backButtonClicked(); - binding.tabLayout.setVisibility(View.VISIBLE); - }else { - super.onBackPressed(); - } - } - - /** - * To set the visibility of tab layout - * @param isVisible boolean - */ - public void setTabLayoutVisibility(boolean isVisible) { - binding.tabLayout.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt new file mode 100644 index 000000000..164842c9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt @@ -0,0 +1,229 @@ +package fr.free.nrw.commons.profile + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.* +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.databinding.ActivityProfileBinding +import fr.free.nrw.commons.profile.achievements.AchievementsFragment +import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.DialogUtil +import java.io.File +import java.io.FileOutputStream +import java.util.* +import javax.inject.Inject + +/** + * This activity will set two tabs, achievements and + * each tab will have their own fragments + */ +class ProfileActivity : BaseActivity() { + + lateinit var binding: ActivityProfileBinding + + @Inject + lateinit var sessionManager: SessionManager + + private lateinit var viewPagerAdapter: ViewPagerAdapter + private lateinit var achievementsFragment: AchievementsFragment + private lateinit var leaderboardFragment: LeaderboardFragment + private lateinit var userName: String + private var shouldShowContributions: Boolean = false + private var contributionsFragment: ContributionsFragment? = null + + fun setScroll(canScroll: Boolean) { + binding.viewPager.setCanScroll(canScroll) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + savedInstanceState.let { + userName = it.getString(KEY_USERNAME, "") + shouldShowContributions = it.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityProfileBinding.inflate(layoutInflater) + setContentView(binding.root) + setSupportActionBar(binding.toolbarBinding.toolbar) + + binding.toolbarBinding.toolbar.setNavigationOnClickListener { + onSupportNavigateUp() + } + + userName = intent.getStringExtra(KEY_USERNAME) ?: "" + title = userName + shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false) + + viewPagerAdapter = ViewPagerAdapter(supportFragmentManager) + binding.viewPager.adapter = viewPagerAdapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + setTabs() + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + private fun setTabs() { + val fragmentList = mutableListOf() + val titleList = mutableListOf() + + // Add Achievements tab + achievementsFragment = AchievementsFragment().apply { + arguments = Bundle().apply { + putString(KEY_USERNAME, userName) + } + } + fragmentList.add(achievementsFragment) + titleList.add(resources.getString(R.string.achievements_tab_title).uppercase()) + + // Add Leaderboard tab + leaderboardFragment = LeaderboardFragment().apply { + arguments = Bundle().apply { + putString(KEY_USERNAME, userName) + } + } + fragmentList.add(leaderboardFragment) + titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT)) + + // Add Contributions tab + contributionsFragment = ContributionsFragment().apply { + arguments = Bundle().apply { + putString(KEY_USERNAME, userName) + } + } + contributionsFragment?.let { + fragmentList.add(it) + titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT)) + } + + viewPagerAdapter.setTabData(fragmentList, titleList) + viewPagerAdapter.notifyDataSetChanged() + } + + public override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_about, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.share_app_icon -> { + val rootView = window.decorView.findViewById(android.R.id.content) + val screenShot = Utils.getScreenShot(rootView) + if (screenShot == null) { + Log.e("ERROR", "ScreenShot is null") + return false + } + showAlert(screenShot) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + fun showAlert(screenshot: Bitmap) { + val view = layoutInflater.inflate(R.layout.image_alert_layout, null) + val screenShotImage = view.findViewById(R.id.alert_image) + val shareMessage = view.findViewById(R.id.alert_text) + + screenShotImage.setImageBitmap(screenshot) + shareMessage.setText(R.string.achievements_share_message) + + DialogUtil.showAlertDialog( + this, + null, + null, + getString(R.string.about_translate_proceed), + getString(R.string.cancel), + { shareScreen(screenshot) }, + {}, + view + ) + } + + private fun shareScreen(bitmap: Bitmap) { + try { + val file = File(externalCacheDir, "screen.png") + FileOutputStream(file).use { out -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + out.flush() + } + file.setReadable(true, false) + + val fileUri = FileProvider.getUriForFile( + applicationContext, + "$packageName.provider", + file + ) + + grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + + val intent = Intent(Intent.ACTION_SEND).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(Intent.EXTRA_STREAM, fileUri) + type = "image/png" + } + startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))) + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(KEY_USERNAME, userName) + outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions) + } + + override fun onBackPressed() { + if (contributionsFragment?.mediaDetailPagerFragment?.isVisible == true) { + contributionsFragment?.backButtonClicked() + binding.tabLayout.visibility = View.VISIBLE + } else { + super.onBackPressed() + } + } + + fun setTabLayoutVisibility(isVisible: Boolean) { + binding.tabLayout.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + companion object { + const val KEY_USERNAME = "username" + const val KEY_SHOULD_SHOW_CONTRIBUTIONS = "shouldShowContributions" + + @JvmStatic + fun startYourself(context: Context, userName: String, shouldShowContributions: Boolean) { + val intent = Intent(context, ProfileActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + putExtra(KEY_USERNAME, userName) + putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions) + } + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt index c7bccf950..99de68f80 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt @@ -58,7 +58,7 @@ class LeaderboardListAdapter : PagedListAdapter if (view.context is ProfileActivity) { ((view.context) as Activity).finish() } - ProfileActivity.startYourself(view.context, item.username, true) + ProfileActivity.startYourself(view.context, item.username?:"", true) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt index a2b3168ec..351e53124 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt @@ -23,14 +23,14 @@ data class UploadMediaDetail( * The caption text for the item being uploaded. * @param captionText The caption text. */ - var captionText: String? = "", + var captionText: String = "", ) : Parcelable { fun javaCopy() = copy() constructor(place: Place?) : this( place?.language, place?.longDescription, - place?.name, + place?.name ?: "", ) /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 7536cad75..dbeeae6ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -140,7 +140,7 @@ class CategoriesPresenter */ private fun getImageTitleList(): List = repository.getUploads() - .map { it.uploadMediaDetails[0].captionText!! } + .map { it.uploadMediaDetails[0].captionText } .filterNot { TextUtils.isEmpty(it) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt index 0e4ed482d..4ae366535 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt @@ -68,6 +68,9 @@ data class DepictedItem constructor( entity.id(), ) + val primaryImage: String? + get() = imageUrl?.split('-')?.lastOrNull() + override fun equals(other: Any?) = when { this === other -> true diff --git a/app/src/main/res/layout/fragment_achievements.xml b/app/src/main/res/layout/fragment_achievements.xml index 00c18b323..c688f983a 100644 --- a/app/src/main/res/layout/fragment_achievements.xml +++ b/app/src/main/res/layout/fragment_achievements.xml @@ -8,7 +8,7 @@ android:fillViewport="true" tools:ignore="ContentDescription" > - + كومنز مواقع ويكي أخرى حالات استخدام الملف + نشاط عرض ويب واحد + حساب + حذف الحساب + تحذير من اختفاء الحساب + الاختفاء هو <b>الملاذ الأخير</b> ويجب <b>استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد</b> وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.<br/><br/> يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. <b>لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع</b> . الشرح تم نسخ التسمية التوضيحية إلى الحافظة diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 12eebaa7d..cd974dc30 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -708,6 +708,7 @@ 다른 위키 이 파일을 사용하는 문서 계정 + 계정 버리기 캡션 캡션이 클립보드에 복사되었습니다 diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index f43975c21..52a1243aa 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -109,7 +109,7 @@ Сиздин сүрөттөр дүйнө жүзүндөгү адамдардын билим алышына өбөлгө түзүүдө. Интернетте жарыяланган автордук укукка ээ сүрөттөрдөн, ошондой эле плакаттардан жана китептердин мукабасынан ж.б. четтеңиз. Сизге бул түшүнүктүүбү? - Ооба ! + Ооба! Категориялар Жүктөлүүдө… Тандалган жок diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 354c44b5c..1aaee6e9c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -831,4 +831,8 @@ Commons Andere wiki’s Bestandsgebruik + Activiteit enkele webraadpleging + Account + Account laten verdwijnen + Waarschuwing verwijdering account diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f016f5f39..be06b0b99 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -813,7 +813,11 @@ Commons Andra wikier Filanvändning + SingleWebViewActivity Konto + Få kontot att försvinna + Varning om försvinnande konto + Att få kontot att försvinna är en <b>sista utväg</b> och bör <b>endast användas när du vill sluta redigera för alltid</b> och även dölja så många av dina tidigare associationer som möjligt.<br/><br/>Konton raderas på Wikimedia Commons genom att ändra kontonamnet för att göra så att andra inte kan känna igen bidragen i en process som kallas kontoförsvinnande. <b>Försvinnande garanterar inte fullständig anonymitet eller att bidrag tas bort från projekten</b>. Bildtext Bildtext kopierades till urklipp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 67b5eae0f..02c314a4a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -20,6 +20,7 @@ @color/white @color/white @color/white + @color/main_background_dark @color/commons_app_blue_dark @color/sub_background_dark diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt index cc5340fbb..8c336470a 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt @@ -1,7 +1,9 @@ package fr.free.nrw.commons.category import categoryItem +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import depictedItem @@ -90,14 +92,18 @@ class CategoriesModelTest { val depictedItem = depictedItem( commonsCategories = - listOf( - CategoryItem( - "depictionCategory", - "", - "", - false, - ), + listOf( + CategoryItem( + "depictionCategory", + "", + "", + false, ), + ), + ) + val depictedItemWithoutCategories = + depictedItem( + imageUrl = "testUrl" ) whenever(gpsCategoryModel.categoriesFromLocation) @@ -159,6 +165,23 @@ class CategoriesModelTest { ), ), ) + whenever( + categoryClient.getCategoriesOfImage( + "testUrl", + 25, + ), + ).thenReturn( + Single.just( + listOf( + CategoryItem( + "categoriesOfP18", + "", + "", + false, + ), + ), + ), + ) val imageTitleList = listOf("Test") CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) .searchAll("", imageTitleList, listOf(depictedItem)) @@ -171,8 +194,21 @@ class CategoriesModelTest { categoryItem("recentCategories"), ), ) + CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) + .searchAll("", imageTitleList, listOf(depictedItemWithoutCategories)) + .test() + .assertValue( + listOf( + categoryItem("categoriesOfP18"), + categoryItem("gpsCategory"), + categoryItem("titleSearch"), + categoryItem("recentCategories"), + ), + ) imageTitleList.forEach { - verify(categoryClient).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT) + verify(categoryClient, times(2)).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT) + verify(categoryClient).getCategoriesByName(any(), any(), any(), any()) + verify(categoryClient).getCategoriesOfImage(any(), any()) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt index 5c95215a8..5edf55f6e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt @@ -132,6 +132,45 @@ class CategoryClientTest { ) } + @Test + fun getCategoriesByTitlesFound() { + val mockResponse = withMockResponse("Category:Test") + whenever( + categoryInterface.getCategoriesByTitles( + anyString(), + anyInt(), + ), + ).thenReturn(Single.just(mockResponse)) + categoryClient + .getCategoriesOfImage("tes", 10) + .test() + .assertValues( + listOf( + CategoryItem( + "Test", + "", + "", + false, + ), + ), + ) + categoryClient + .getCategoriesOfImage( + "tes", + 10, + ).test() + .assertValues( + listOf( + CategoryItem( + "Test", + "", + "", + false, + ), + ), + ) + } + @Test fun getCategoriesByNameNull() { val mockResponse = withNullPages() @@ -160,6 +199,29 @@ class CategoryClientTest { .assertValues(emptyList()) } + @Test + fun getCategoriesByTitlesNull() { + val mockResponse = withNullPages() + whenever( + categoryInterface.getCategoriesByTitles( + anyString(), + anyInt(), + ), + ).thenReturn(Single.just(mockResponse)) + categoryClient + .getCategoriesOfImage( + "tes", + 10, + ).test() + .assertValues(emptyList()) + categoryClient + .getCategoriesOfImage( + "tes", + 10, + ).test() + .assertValues(emptyList()) + } + @Test fun getParentCategoryListFound() { val mockResponse = withMockResponse("Category:Test") diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt index e0d339eee..faec52051 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt @@ -181,4 +181,20 @@ class DepictedItemTest { fun `hashCode returns different values for objects with different name`() { Assert.assertNotEquals(depictedItem(name = "a").hashCode(), depictedItem(name = "b").hashCode()) } + + @Test + fun `primaryImage is derived correctly from imageUrl`() { + Assert.assertEquals( + DepictedItem( + entity( + statements = mapOf( + WikidataProperties.IMAGE.propertyName to listOf( + statement(snak(dataValue = valueString("prefix: example_image name"))), + ), + ), + ), + ).primaryImage, + "_example_image_name", + ) + } }