Todo for user (#3851)

* Add a simple message

* Categories are edited

* Display categories

* read whole page

* Revert wrong changes

* Add newly added category field

* Doesnt displaey alreasy added categories

* add strings for notifications

* clean code

* Readd accidentally removed imports

* Fix edit layout style

* Fix category update messages

* Pass isWikipediaButtonDisplayed information to fragment

* Remove unused class

* Fix strings

* Fix string

* Add exeption for uncategorised images too

* Revert project.xml changes

* fix update buttonvisibility issue

* Make sure it works for auto added categories too

* make the button visible

* Make the button appear when categories are entered

* Include cancel button

* Make view updated too

* Make category view edited

* Update categories in an hacky way

* Fix unnecessary method call

* Add notes for short term fix to display added category

* Fix tests

* Fix strings

* Fix click issue
This commit is contained in:
neslihanturan 2020-08-17 14:30:46 +03:00 committed by GitHub
parent f5e28834fc
commit 1856196851
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 799 additions and 36 deletions

View file

@ -325,4 +325,4 @@
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

View file

@ -121,4 +121,14 @@ class Media constructor(
get() = captions[Locale.getDefault().language]
?: captions.values.firstOrNull()
?: displayTitle
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
*/
var addedCategories: List<String>? = null
// TODO added categories should be removed. It is added for a short fix. On category update,
// categories should be re-fetched instead
get() = field // getter
set(value) { field = value } // setter
}

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.actions;
import io.reactivex.Single;
import io.reactivex.SingleOnSubscribe;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
@ -48,12 +50,16 @@ public class PageEditClient {
* @param summary Edit summary
*/
public Observable<Boolean> appendEdit(String pageTitle, String appendText, String summary) {
try {
return pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
.map(editResponse -> editResponse.edit().editSucceeded());
} catch (Throwable throwable) {
return Observable.just(false);
}
return Single.create((SingleOnSubscribe<String>) emitter -> {
try {
emitter.onSuccess(csrfTokenClient.getTokenBlocking());
} catch (Throwable throwable) {
emitter.onError(throwable);
throwable.printStackTrace();
}
}).flatMapObservable(token -> pageEditInterface.postAppendEdit(pageTitle, summary, appendText, token)
.map(editResponse -> editResponse.edit().editSucceeded()));
}
/**

View file

@ -8,6 +8,8 @@ import javax.inject.Singleton
const val CATEGORY_PREFIX = "Category:"
const val SUB_CATEGORY_CONTINUATION_PREFIX = "sub_category_"
const val PARENT_CATEGORY_CONTINUATION_PREFIX = "parent_category_"
const val CATEGORY_UNCATEGORISED = "uncategorised"
const val CATEGORY_NEEDING_CATEGORIES = "needing categories"
/**
* Category Client to handle custom calls to Commons MediaWiki APIs

View file

@ -0,0 +1,109 @@
package fr.free.nrw.commons.category;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_CATEGORY;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.notification.NotificationHelper;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class CategoryEditHelper {
private final NotificationHelper notificationHelper;
public final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil;
private final String username;
private Callback callback;
@Inject
public CategoryEditHelper(NotificationHelper notificationHelper,
@Named("commons-page-edit") PageEditClient pageEditClient,
ViewUtilWrapper viewUtil,
@Named("username") String username) {
this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
this.viewUtil = viewUtil;
this.username = username;
}
/**
* Public interface to edit categories
* @param context
* @param media
* @param categories
* @return
*/
public Single<Boolean> makeCategoryEdit(Context context, Media media, List<String> categories, Callback callback) {
viewUtil.showShortToast(context, context.getString(R.string.category_edit_helper_make_edit_toast));
return addCategory(media, categories)
.flatMapSingle(result -> Single.just(showCategoryEditNotification(context, media, result)))
.firstOrError();
}
/**
* Appends new categories
* @param media
* @param categories to be added
* @return
*/
private Observable<Boolean> addCategory(Media media, List<String> categories) {
Timber.d("thread is category adding %s", Thread.currentThread().getName());
String summary = "Adding categories";
StringBuilder buffer = new StringBuilder();
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
}
} else {
buffer.append("{{subst:unc}}");
}
String appendText = buffer.toString();
return pageEditClient.appendEdit(media.getFilename(), appendText + "\n", summary);
}
private boolean showCategoryEditNotification(Context context, Media media, boolean result) {
String message;
String title = context.getString(R.string.category_edit_helper_show_edit_title);
if (result) {
title += ": " + context.getString(R.string.category_edit_helper_show_edit_title_success);
StringBuilder categoriesInMessage = new StringBuilder();
List<String> mediaCategoryList = media.getCategories();
for (String category : mediaCategoryList) {
categoriesInMessage.append(category);
if (category.equals(mediaCategoryList.get(mediaCategoryList.size()-1))) {
continue;
}
categoriesInMessage.append(",");
}
message = context.getResources().getQuantityString(R.plurals.category_edit_helper_show_edit_message_if, mediaCategoryList.size(), categoriesInMessage.toString());
} else {
title += ": " + context.getString(R.string.category_edit_helper_show_edit_title);
message = context.getString(R.string.category_edit_helper_edit_message_else) ;
}
String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_CATEGORY, browserIntent);
return result;
}
public interface Callback {
boolean updateCategoryDisplay(List<String> categories);
}
}

View file

@ -0,0 +1,176 @@
package fr.free.nrw.commons.category;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter.RecyclerViewHolder;
import fr.free.nrw.commons.nearby.Label;
import java.util.ArrayList;
import java.util.List;
public class CategoryEditSearchRecyclerViewAdapter
extends RecyclerView.Adapter<RecyclerViewHolder>
implements Filterable {
private List<String> displayedCategories;
private List<String> categories = new ArrayList<>();
private List<String> newCategories = new ArrayList<>();
private final LayoutInflater inflater;
private CategoryClient categoryClient;
private Context context;
private Callback callback;
public CategoryEditSearchRecyclerViewAdapter(Context context, ArrayList<Label> labels,
RecyclerView categoryRecyclerView, CategoryClient categoryClient, Callback callback) {
this.context = context;
inflater = LayoutInflater.from(context);
this.categoryClient = categoryClient;
this.callback = callback;
}
public void addToCategories(List<String> categories) {
for(String category : categories) {
if (!this.categories.contains(category)) {
this.categories.add(category);
}
}
}
public void addToCategories(String categoryToBeAdded) {
if (!categories.contains(categoryToBeAdded)) {
categories.add(categoryToBeAdded);
}
}
public void removeFromCategories(String categoryToBeRemoved) {
if (categories.contains(categoryToBeRemoved)) {
categories.remove(categoryToBeRemoved);
}
}
public void removeFromNewCategories(String categoryToBeRemoved) {
if (newCategories.contains(categoryToBeRemoved)) {
newCategories.remove(categoryToBeRemoved);
}
}
public void addToNewCategories(List<String> newCategories) {
for(String category : newCategories) {
if (!this.newCategories.contains(category)) {
this.newCategories.add(category);
}
}
}
public void addToNewCategories(String addedCategory) {
if (!newCategories.contains(addedCategory)) {
newCategories.add(addedCategory);
}
}
public List<String> getCategories() {
return categories;
}
public List<String> getNewCategories() {
return newCategories;
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
List<String> resultCategories = categoryClient.searchCategories(constraint.toString(), 10).blockingGet();
results.values = resultCategories;
results.count = resultCategories.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
List<String> resultList = (List<String>)results.values;
// Do not re-add already added categories
for (String category : categories) {
if (resultList.contains(category)) {
resultList.remove(category);
}
}
displayedCategories = resultList;
notifyDataSetChanged();
if (displayedCategories.size()==0) {
callback.noResultsFound();
} else {
callback.someResultsFound();
}
}
};
}
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
public CheckBox categoryCheckBox;
public TextView categoryTextView;
public RecyclerViewHolder(View view) {
super(view);
categoryCheckBox = view.findViewById(R.id.category_checkbox);
categoryTextView = view.findViewById(R.id.category_text);
categoryCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
addToNewCategories(categoryTextView.getText().toString());
} else {
removeFromNewCategories(categoryTextView.getText().toString());
}
List<String> allCategories = new ArrayList<>();
allCategories.addAll(categories);
allCategories.addAll(newCategories);
callback.updateSelectedCategoriesTextView(allCategories);
}
});
}
}
@NonNull
@Override
public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = inflater.inflate(R.layout.layout_edit_category_item , parent, false);
return new RecyclerViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) {
holder.categoryTextView.setText(displayedCategories.get(position));
}
@Override
public long getItemId(int position) {
return displayedCategories.get(position).hashCode();
}
@Override
public int getItemCount() {
return (displayedCategories == null) ? 0 : displayedCategories.size();
}
public interface Callback {
void updateSelectedCategoriesTextView(List<String> selectedCategories);
void noResultsFound();
void someResultsFound();
}
}

View file

@ -51,6 +51,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
private Contribution contribution;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient;
private boolean isWikipediaButtonDisplayed;
ContributionViewHolder(final View parent, final Callback callback,
final MediaClient mediaClient) {
@ -160,6 +161,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
private void displayWikipediaButton(Boolean mediaExists) {
if (!mediaExists) {
addToWikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
@ -199,7 +201,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
@OnClick(R.id.contributionImage)
public void imageClicked() {
callback.openMediaDetail(position);
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@OnClick(R.id.wikipediaButton)

View file

@ -482,12 +482,12 @@ public class ContributionsFragment
* contribution.
*/
@Override
public void showDetail(int position) {
public void showDetail(int position, boolean isWikipediaButtonDisplayed) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(position);
mediaDetailPagerFragment.showImage(position, isWikipediaButtonDisplayed);
}
@Override

View file

@ -7,7 +7,6 @@ import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.media.MediaClient;
import org.wikipedia.dataclient.WikiSite;
/**
* Represents The View Adapter for the List of Contributions
@ -75,7 +74,7 @@ public class ContributionsListAdapter extends
void deleteUpload(Contribution contribution);
void openMediaDetail(int contribution);
void openMediaDetail(int contribution, boolean isWikipediaPageExists);
void addImageToWikipedia(Contribution contribution);

View file

@ -29,9 +29,8 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import fr.free.nrw.commons.media.MediaClient;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
@ -244,9 +243,9 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
}
@Override
public void openMediaDetail(final int position) {
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.showDetail(position);
callback.showDetail(position, isWikipediaButtonDisplayed);
}
}
@ -334,8 +333,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
void retryUpload(Contribution contribution);
void pauseUpload(Contribution contribution);
void showDetail(int position, boolean isWikipediaButtonDisplayed);
void showDetail(int position);
void pauseUpload(Contribution contribution);
}
}

View file

@ -2,7 +2,9 @@ package fr.free.nrw.commons.media;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_NEEDING_CATEGORIES;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_UNCATEGORISED;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
@ -13,7 +15,9 @@ import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -23,11 +27,15 @@ import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.SearchView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@ -38,6 +46,8 @@ import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
@ -45,13 +55,18 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryClient;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryEditHelper;
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter;
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter.Callback;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.nearby.Label;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@ -59,22 +74,25 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import timber.log.Timber;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
public class MediaDetailFragment extends CommonsDaggerSupportFragment implements Callback,
CategoryEditHelper.Callback {
private boolean editable;
private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
private boolean isDeleted = false;
private boolean isWikipediaButtonDisplayed;
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage, boolean isWikipediaButtonDisplayed) {
MediaDetailFragment mf = new MediaDetailFragment();
Bundle state = new Bundle();
@ -83,6 +101,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
state.putInt("index", index);
state.putInt("listIndex", 0);
state.putInt("listTop", 0);
state.putBoolean("isWikipediaButtonDisplayed", isWikipediaButtonDisplayed);
mf.setArguments(state);
@ -96,7 +115,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Inject
DeleteHelper deleteHelper;
@Inject
CategoryEditHelper categoryEditHelper;
@Inject
ViewUtilWrapper viewUtil;
@Inject
CategoryClient categoryClient;
private int initialListTop = 0;
@ -132,6 +155,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
LinearLayout nominatedForDeletion;
@BindView(R.id.mediaDetailCategoryContainer)
LinearLayout categoryContainer;
@BindView(R.id.categoryEditButton)
Button categoryEditButton;
@BindView(R.id.media_detail_depiction_container)
LinearLayout depictionContainer;
@BindView(R.id.authorLinearLayout)
@ -140,6 +165,29 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
Button delete;
@BindView(R.id.mediaDetailScrollView)
ScrollView scrollView;
@BindView(R.id.toDoLayout)
LinearLayout toDoLayout;
@BindView(R.id.toDoReason)
TextView toDoReason;
@BindView(R.id.category_edit_layout)
LinearLayout categoryEditLayout;
@BindView(R.id.et_search)
SearchView categorySearchView;
@BindView(R.id.rv_categories)
RecyclerView categoryRecyclerView;
@BindView(R.id.update_categories_button)
Button updateCategoriesButton;
@BindView(R.id.dummy_category_edit_container)
LinearLayout dummyCategoryEditContainer;
@BindView(R.id.pb_categories)
ProgressBar progressbarCategories;
@BindView(R.id.existing_categories)
TextView existingCategories;
@BindView(R.id.no_results_found)
TextView noResultsFound;
private ArrayList<String> categoryNames = new ArrayList<>();
private String categorySearchQuery;
/**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
@ -151,6 +199,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private int newWidthOfImageView;
private boolean heightVerifyingBoolean = true; // helps in maintaining aspect ratio
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private CategoryEditSearchRecyclerViewAdapter categoryEditSearchRecyclerViewAdapter;
//Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
private Media media;
@ -163,6 +212,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
outState.putInt("index", index);
outState.putBoolean("editable", editable);
outState.putBoolean("isCategoryImage", isCategoryImage);
outState.putBoolean("isWikipediaButtonDisplayed", isWikipediaButtonDisplayed);
getScrollPosition();
outState.putInt("listTop", initialListTop);
@ -182,11 +232,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
isCategoryImage = savedInstanceState.getBoolean("isCategoryImage");
isWikipediaButtonDisplayed = savedInstanceState.getBoolean("isWikipediaButtonDisplayed");
index = savedInstanceState.getInt("index");
initialListTop = savedInstanceState.getInt("listTop");
} else {
editable = getArguments().getBoolean("editable");
isCategoryImage = getArguments().getBoolean("isCategoryImage");
isWikipediaButtonDisplayed = getArguments().getBoolean("isWikipediaButtonDisplayed");
index = getArguments().getInt("index");
initialListTop = 0;
}
@ -232,6 +284,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
.getParentFragment())).nearbyNotificationCardView
.setVisibility(View.GONE);
}
categoryEditSearchRecyclerViewAdapter =
new CategoryEditSearchRecyclerViewAdapter(getContext(), new ArrayList<>(
Label.valuesAsList()), categoryRecyclerView, categoryClient, this);
categoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
categoryRecyclerView.setAdapter(categoryEditSearchRecyclerViewAdapter);
media = detailProvider.getMediaAtPosition(index);
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@ -311,6 +369,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDepictionsLoaded, Timber::e)
);
// compositeDisposable.add(disposable);
setupToDo();
}
private void onDiscussionLoaded(String discussion) {
@ -385,6 +445,63 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
imageLandscape.setController(controllerLandscape);
}
/**
* Displays layout about missing actions to inform user
* - Images that they uploaded with no categories/descriptions, so that they can add them
* - Images that can be added to associated Wikipedia articles that have no pictures
*/
private void setupToDo() {
updateToDoWarning();
compositeDisposable.add(RxSearchView.queryTextChanges(categorySearchView)
.takeUntil(RxView.detaches(categorySearchView))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(query -> {
this.categorySearchQuery = query.toString();
//update image list
if (!TextUtils.isEmpty(query)) {
if (categoryEditLayout.getVisibility() == VISIBLE) {
((CategoryEditSearchRecyclerViewAdapter) categoryRecyclerView.getAdapter()).
getFilter().filter(query.toString());
}
}
}, Timber::e
));
}
private void updateToDoWarning() {
String toDoMessage = "";
boolean toDoNeeded = false;
boolean categoriesPresent = media.getCategories() == null ? false : (media.getCategories().size() == 0 ? false : true);
// Check if the presented category is about need of category
if (categoriesPresent) {
for (String category : media.getCategories()) {
if (category.toLowerCase().contains(CATEGORY_NEEDING_CATEGORIES) ||
category.toLowerCase().contains(CATEGORY_UNCATEGORISED)) {
categoriesPresent = false;
}
break;
}
}
if (!categoriesPresent) {
toDoNeeded = true;
toDoMessage += getString(R.string.missing_category);
}
if (isWikipediaButtonDisplayed) {
toDoNeeded = true;
toDoMessage += (toDoMessage.isEmpty()) ? "" : "\n" + getString(R.string.missing_article);
}
if (toDoNeeded) {
toDoMessage = getString(R.string.todo_improve) + "\n" + toDoMessage;
toDoLayout.setVisibility(VISIBLE);
toDoReason.setText(toDoMessage);
} else {
toDoLayout.setVisibility(GONE);
}
}
@Override
public void onDestroyView() {
if (layoutListener != null && getView() != null) {
@ -409,15 +526,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
mediaCaption.setText(prettyCaption(media));
}
final List<String> categories = media.getCategories();
if (categories.isEmpty()) {
// Stick in a filler element.
categories.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList(categories);
categoryNames.clear();
categoryNames.addAll(media.getCategories());
categoryEditSearchRecyclerViewAdapter.addToCategories(media.getCategories());
updateSelectedCategoriesTextView(categoryEditSearchRecyclerViewAdapter.getCategories());
updateCategoryList();
if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
@ -426,6 +540,49 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
}
private void updateCategoryList() {
List<String> allCategories = new ArrayList<String>( media.getCategories());
if (media.getAddedCategories() != null) {
// TODO this added categories logic should be removed.
// It is just a short term hack. Categories should be fetch everytime they are updated.
// if media.getCategories contains addedCategory, then do not re-add them
for (String addedCategory : media.getAddedCategories()) {
if (allCategories.contains(addedCategory)) {
media.setAddedCategories(null);
break;
}
}
allCategories.addAll(media.getAddedCategories());
}
if (allCategories.isEmpty()) {
// Stick in a filler element.
allCategories.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList(allCategories);
}
@Override
public void updateSelectedCategoriesTextView(List<String> selectedCategories) {
if (selectedCategories == null || selectedCategories.size() == 0) {
updateCategoriesButton.setClickable(false);
}
if (selectedCategories != null) {
existingCategories.setText(StringUtils.join(selectedCategories,", "));
updateCategoriesButton.setClickable(true);
}
}
@Override
public void noResultsFound() {
noResultsFound.setVisibility(VISIBLE);
}
@Override
public void someResultsFound() {
noResultsFound.setVisibility(GONE);
}
/**
* Populates media details fragment with depiction list
* @param idAndCaptions
@ -467,6 +624,50 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
Toast.makeText(getContext(), getString(R.string.wikicode_copied), Toast.LENGTH_SHORT).show();
}
@OnClick(R.id.dummy_category_edit_container)
public void onOutsideOfCategoryEditClicked() {
if (dummyCategoryEditContainer.getVisibility() == VISIBLE) {
dummyCategoryEditContainer.setVisibility(GONE);
}
}
@OnClick(R.id.categoryEditButton)
public void onCategoryEditButtonClicked(){
displayHideCategorySearch();
}
public void displayHideCategorySearch() {
if (dummyCategoryEditContainer.getVisibility() != VISIBLE) {
dummyCategoryEditContainer.setVisibility(VISIBLE);
} else {
dummyCategoryEditContainer.setVisibility(GONE);
}
}
@OnClick(R.id.update_categories_button)
public void onUpdateCategoriesClicked() {
updateCategories(categoryEditSearchRecyclerViewAdapter.getNewCategories());
displayHideCategorySearch();
}
@OnClick(R.id.cancel_categories_button)
public void onCancelCategoriesClicked() {
displayHideCategorySearch();
}
public void updateCategories(List<String> selectedCategories) {
compositeDisposable.add(categoryEditHelper.makeCategoryEdit(getContext(), media, selectedCategories, this)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Categories are added.");
onOutsideOfCategoryEditClicked();
media.setAddedCategories(selectedCategories);
updateCategoryList();
}));
}
@SuppressLint("StringFormatInvalid")
@OnClick(R.id.nominateDeletion)
public void onDeleteButtonClicked(){
if (AccountUtil.getUserName(getContext()) != null && AccountUtil.getUserName(getContext()).equals(media.getCreator())) {
@ -537,7 +738,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
}
@SuppressLint("CheckResult")
private void onDeleteClicked(Spinner spinner) {
String reason = spinner.getSelectedItem().toString();
@ -588,6 +788,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
private void rebuildCatList(List<String> categories) {
Log.d("deneme","rebuild cat list size:"+categories.size());
categoryContainer.removeAllViews();
for (String category : categories) {
categoryContainer.addView(buildCatLabel(sanitise(category), categoryContainer));
@ -706,4 +907,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
return media.getCoordinates().getPrettyCoordinateString();
}
@Override
public boolean updateCategoryDisplay(List<String> categories) {
if (categories == null) {
return false;
} else {
rebuildCatList(categories);
return true;
}
}
}

View file

@ -41,6 +41,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@BindView(R.id.mediaDetailsPager) ViewPager pager;
private Boolean editable;
private boolean isFeaturedImage;
private boolean isWikipediaButtonDisplayed;
MediaDetailAdapter adapter;
private Bookmark bookmark;
private MediaDetailProvider provider;
@ -249,6 +250,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
item.setIcon(icon);
}
public void showImage(int i, boolean isWikipediaButtonDisplayed) {
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed;
Handler handler = new Handler();
handler.postDelayed(() -> pager.setCurrentItem(i), 5);
}
public void showImage(int i) {
Handler handler = new Handler();
handler.postDelayed(() -> pager.setCurrentItem(i), 5);
@ -310,7 +317,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
}
pager.postDelayed(() -> getActivity().invalidateOptionsMenu(), 5);
}
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage);
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
}
@Override

View file

@ -24,6 +24,7 @@ import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
public class NotificationHelper {
public static final int NOTIFICATION_DELETE = 1;
public static final int NOTIFICATION_EDIT_CATEGORY = 2;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;

View file

@ -23,12 +23,12 @@ import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import fr.free.nrw.commons.media.MediaClient;
import javax.inject.Inject;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
@ -43,7 +43,8 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject MediaClient mediaClient;
@Inject
MediaClient mediaClient;
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View file

@ -8,6 +8,20 @@
android:background="?attr/mainBackground"
>
<LinearLayout
android:id="@+id/dummy_category_edit_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="35dp"
android:layout_gravity="bottom"
android:visibility="gone"
android:orientation="vertical"
android:weightSum="10">
<include
layout="@layout/layout_edit_categories" />
</LinearLayout>
<ImageView
android:id="@+id/mediaDetailImageFailed"
android:layout_height="wrap_content"
@ -53,9 +67,12 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_250"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/mediaDetailImageViewSpacer" />
android:id="@+id/mediaDetailImageViewSpacer"
>
</LinearLayout>
</FrameLayout>
@ -88,6 +105,30 @@
tools:text="Title of the media" />
</LinearLayout>
<LinearLayout
android:id="@+id/toDoLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:background="@color/layout_light_grey"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/MediaDetailTextLabel"
android:text="@string/warning" />
<TextView
android:id="@+id/toDoReason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin_vertical"
style="@style/MediaDetailTextBody" />
</LinearLayout>
<LinearLayout
style="@style/MediaDetailContainer"
android:layout_width="match_parent"
@ -255,9 +296,19 @@
android:layout_width="@dimen/widget_margin"
android:layout_height="match_parent"
android:layout_weight="70"
android:orientation="vertical" />
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/categoryEditButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="@dimen/standard_gap"
android:layout_gravity="end"
android:background="@drawable/ic_baseline_edit_24" />
<LinearLayout
style="@style/MediaDetailContainer"
android:layout_width="match_parent"

View file

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:id="@+id/category_edit_layout"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:orientation="vertical"
android:background="@color/white"
android:elevation="30dp">
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="@dimen/half_standard_height"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="Type categories"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<FrameLayout
android:id="@+id/category_search_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_container_search"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<SearchView
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/categories_search_text_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/pb_categories"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/existing_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<TextView
android:id="@+id/no_results_found"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="No results found"
android:textSize="@dimen/description_text_size"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="6"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_categories"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4"
android:background="@color/white"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2">
<Button
android:id="@+id/cancel_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/opak_middle_grey"
/>
<Button
android:id="@+id/update_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/category_edit_button_text"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/button_blue"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/category_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/category_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkMark="?android:attr/textCheckMark"
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/category_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Category"
app:layout_constraintLeft_toRightOf="@+id/category_checkbox"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -598,6 +598,17 @@ Upload your first media by tapping on the add button.</string>
<string name="delete_helper_ask_reason_copyright_logo">Logo</string>
<string name="delete_helper_ask_reason_copyright_other">Other</string>
<string name="delete_helper_ask_alert_set_positive_button_reason">Because it is</string>
<string name="category_edit_helper_make_edit_toast">Trying to update categories.</string>
<string name="category_edit_helper_show_edit_title">Category update</string>
<string name="category_edit_helper_show_edit_title_success">Success</string>
<plurals name="category_edit_helper_show_edit_message_if">
<item quantity="one">Category %1$s is added.</item>
<item quantity="other">Categories %1$s are added.</item>
</plurals>
<string name="category_edit_helper_edit_message_else">Could not add categories.</string>
<string name="category_edit_button_text">Update categories</string>
<string name="share_image_via">Share image via</string>
<string name="no_achievements_yet">You haven\'t made any contributions yet</string>
<string name="account_created">Account created!</string>
@ -636,6 +647,10 @@ Upload your first media by tapping on the add button.</string>
<string name="load_more">Load More</string>
<string name="nearby_no_results">No places found, try changing your search criteria.</string>
<string name="todo_improve">Suggested improvements:</string>
<string name="missing_category">- Add categories to this image to improve usability.</string>
<string name="missing_article">- Add this image to the associated Wikipedia article that has no images.</string>
<string name="edit_category">Edit categories</string>
<string name="add_picture_to_wikipedia_article_title">Add image to Wikipedia</string>
<string name="add_picture_to_wikipedia_article_desc">Do you want to add this picture to the %1$s language Wikipedia article?</string>

View file

@ -2,13 +2,16 @@ package fr.free.nrw.commons.actions
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.verify
import io.reactivex.Observable
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.wikipedia.csrf.CsrfTokenClient
import org.wikipedia.dataclient.Service
import org.wikipedia.edit.Edit
class PageEditClientTest {
@Mock
@ -20,6 +23,12 @@ class PageEditClientTest {
private lateinit var pageEditClient: PageEditClient
@Mock
lateinit var edit: Edit
@Mock
lateinit var editResult: Edit.Result
/**
* initial setup, test environment
*/
@ -46,8 +55,23 @@ class PageEditClientTest {
@Test
fun testAppendEdit() {
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
pageEditClient.appendEdit("test", "test", "test")
Mockito.`when`(
pageEditInterface.postAppendEdit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString()
)
).thenReturn(
Observable.just(edit)
)
Mockito.`when`(edit.edit()).thenReturn(editResult)
Mockito.`when`(editResult.editSucceeded()).thenReturn(true)
pageEditClient.appendEdit("test", "test", "test").test()
verify(csrfTokenClient).tokenBlocking
verify(pageEditInterface).postAppendEdit(eq("test"), eq("test"), eq("test"), eq("test"))
verify(edit).edit()
verify(editResult).editSucceeded()
}
/**