From a32ba452ec471ad7d4a62728fb77d298143bc70a Mon Sep 17 00:00:00 2001 From: Silky Priya Date: Thu, 21 Mar 2019 17:35:23 +0530 Subject: [PATCH] Include previous Wikimedia hackathon (2018) task, peer review, to codebase (#2602) * Add new activity to manifest * Create review activity layout base * Add a new menu item to drawer for peer review * Add a top menu with randomizer icon to review activity * Add strings for review button * Add activity to ActivityBuilderModule for injection * Add a new drawer item to start review acitivty * Create base of the Review Activity * Add fragment pager * Add new fragment for injection * Create a fragment pager layout * Wikimedia hackathon 2018 (#1533) * First draft of fn to get random recent image * Use log entries for requests to beta, try to connect refresh button FIXME: runs http request on main thread, breaks * Tweak button connection * Add ReviewController class * Fix fragments * Wmhack2018 (#1534) * tiny fixes * Load pictures into activities * Re-use same class for all review fragments (#1537) And try to add pager indicator * [WIP] category check * [WIP] add on-click actions to ReviewActivity * [WIP] add SendThankTask * Make it beautiful * Add some category stuff back in to review (#1538) * Use standalone category extraction code in MediaDataExtractor * Add categories to category review page * Change category question text sizes * Call randomizer whenever the activity is ready * Add progressbar * [WIP] add DeleteTask.askReasonAndExecute * Fix refresh button string * Typo: "nominate *for* deletion" * Add formatting to categories and put them in the same textView * Pass context and adapters as parameters to controller * Add actions to controller * Make everyting work * Add another fragment to thank * Fix npe * Add missing execute method * Some codes * Add a funy text * More random recent image selection (#1542) time-based randomness is biased - if someone uploaded 100 images in hour, one week ago, and I select a random point in time, their last image is way more likely to come up than anything else. With this, there is still bias towards choosing one of the last N in any burst of uploads (where N is the number of recent changes fetched) but it's a bit better than before. * Create Revision class * Add meaningluf strings * Error handling for review image/category fetch (#1543) * Add information layout for username and filename * Use Single to get firstRevision * try to add username and filename * Ensure caption is shown on every review fragment * Fix build * Fixes missing import * Change button text,show current category, add skip image button * Modify texts, fix night mode issues * Positive Wording * fix landscape issue * Add checkbox popup,rewording * Spelling Correction * Fix merge * Remove commented out code, use lambda * Simplify toolbar include --- app/src/main/AndroidManifest.xml | 4 + .../free/nrw/commons/MediaDataExtractor.java | 5 +- .../free/nrw/commons/delete/DeleteTask.java | 159 ++++++++++--- .../nrw/commons/di/ActivityBuilderModule.java | 4 + .../di/CommonsApplicationComponent.java | 7 + .../nrw/commons/di/FragmentBuilderModule.java | 4 + .../commons/media/MediaDetailFragment.java | 53 ++++- .../media/RecentChangesImageUtils.java | 49 ++++ .../mwapi/ApacheHttpClientMediaWikiApi.java | 225 +++++++++++++++++- .../free/nrw/commons/mwapi/MediaWikiApi.java | 10 + .../fr/free/nrw/commons/mwapi/Revision.java | 15 ++ .../nrw/commons/review/CheckCategoryTask.java | 129 ++++++++++ .../nrw/commons/review/ReviewActivity.java | 166 +++++++++++++ .../nrw/commons/review/ReviewController.java | 71 ++++++ .../commons/review/ReviewImageFragment.java | 164 +++++++++++++ .../commons/review/ReviewPagerAdapter.java | 49 ++++ .../nrw/commons/review/ReviewViewPager.java | 30 +++ .../nrw/commons/review/SendThankTask.java | 138 +++++++++++ .../commons/theme/NavigationBaseActivity.java | 7 + .../commons/utils/MediaDataExtractorUtil.java | 28 +++ .../main/res/drawable/ic_check_black_24dp.xml | 9 + .../res/drawable/ic_refresh_black_24dp.xml | 9 + .../res/drawable/tab_indicator_default.xml | 12 + .../res/drawable/tab_indicator_selected.xml | 8 + app/src/main/res/drawable/tab_selector.xml | 8 + app/src/main/res/layout/activity_review.xml | 63 +++++ .../main/res/layout/fragment_review_image.xml | 131 ++++++++++ app/src/main/res/menu/drawer.xml | 5 + .../main/res/menu/review_randomizer_menu.xml | 10 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 50 ++++ app/src/main/res/values/styles.xml | 2 + 33 files changed, 1594 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/media/RecentChangesImageUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/Revision.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java create mode 100644 app/src/main/res/drawable/ic_check_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_refresh_black_24dp.xml create mode 100644 app/src/main/res/drawable/tab_indicator_default.xml create mode 100644 app/src/main/res/drawable/tab_indicator_selected.xml create mode 100644 app/src/main/res/drawable/tab_selector.xml create mode 100644 app/src/main/res/layout/activity_review.xml create mode 100644 app/src/main/res/layout/fragment_review_image.xml create mode 100644 app/src/main/res/menu/review_randomizer_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 74c80c25d..dffea644c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -131,6 +131,10 @@ android:name=".bookmarks.BookmarksActivity" android:label="@string/title_activity_bookmarks" /> + + { .setOnlyAlertOnce(true); Toast toast = new Toast(context); toast.setGravity(Gravity.CENTER,0,0); - toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion",Toast.LENGTH_SHORT); + toast = Toast.makeText(context,"Trying to nominate "+media.getDisplayTitle()+ " for deletion", Toast.LENGTH_SHORT); toast.show(); } @@ -73,7 +77,7 @@ public class DeleteTask extends AsyncTask { String editToken; String authCookie; - String summary = "Nominating " + media.getFilename() +" for deletion."; + String summary = context.getString(R.string.nominating_file_for_deletion, media.getFilename()); authCookie = sessionManager.getAuthCookie(); mwApi.setAuthCookie(authCookie); @@ -106,15 +110,15 @@ public class DeleteTask extends AsyncTask { publishProgress(1); mwApi.prependEdit(editToken,fileDeleteString+"\n", - media.getFilename(),summary); + media.getFilename(), summary); publishProgress(2); mwApi.edit(editToken,subpageString+"\n", - "Commons:Deletion_requests/"+media.getFilename(),summary); + "Commons:Deletion_requests/"+media.getFilename(), summary); publishProgress(3); mwApi.appendEdit(editToken,logPageString+"\n", - "Commons:Deletion_requests/"+date,summary); + "Commons:Deletion_requests/"+date, summary); publishProgress(4); mwApi.appendEdit(editToken,userPageString+"\n", @@ -132,29 +136,21 @@ public class DeleteTask extends AsyncTask { protected void onProgressUpdate (Integer... values){ super.onProgressUpdate(values); + int[] messages = new int[]{ + R.string.getting_edit_token, + R.string.nominate_for_deletion_edit_file_page, + R.string.nominate_for_deletion_create_deletion_request, + R.string.nominate_for_deletion_edit_deletion_request_log, + R.string.nominate_for_deletion_notify_user, + R.string.nominate_for_deletion_done + }; + String message = ""; - switch (values[0]){ - case 0: - message = "Getting token"; - break; - case 1: - message = "Adding delete message to file"; - break; - case 2: - message = "Creating Delete requests sub-page"; - break; - case 3: - message = "Adding file to Delete requests log"; - break; - case 4: - message = "Notifying User on Talk page"; - break; - case 5: - message = "Done"; - break; + if (0 < values[0] && values[0] < messages.length) { + message = context.getString(messages[values[0]]); } - notificationBuilder.setContentTitle("Nominating "+media.getDisplayTitle()+" for deletion") + notificationBuilder.setContentTitle(context.getString(R.string.nominating_file_for_deletion, media.getFilename())) .setStyle(new NotificationCompat.BigTextStyle() .bigText(message)) .setSmallIcon(R.drawable.ic_launcher) @@ -170,7 +166,7 @@ public class DeleteTask extends AsyncTask { if (result){ title += ": Success"; - message = "Successfully nominated " + media.getDisplayTitle() + " deletion."; + message = "Successfully nominated " + media.getDisplayTitle() + " for deletion."; } else { title += ": Failed"; @@ -191,4 +187,111 @@ public class DeleteTask extends AsyncTask { notificationBuilder.setContentIntent(pendingIntent); notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build()); } + + // TODO: refactor; see MediaDetailsFragment.onDeleteButtonClicked + // ReviewActivity will use this + public static void askReasonAndExecute(Media media, Context context, String question, String problem) { + AlertDialog.Builder alert = new AlertDialog.Builder(context); + alert.setTitle(question); + + boolean[] checkedItems = {false , false, false, false}; + ArrayList mUserReason = new ArrayList<>(); + + String[] reasonList= {"Reason 1","Reason 2","Reason 3","Reason 4"}; + + + if(problem.equals("spam")){ + reasonList[0] = "A selfie"; + reasonList[1] = "Blurry"; + reasonList[2] = "Nonsense"; + reasonList[3] = "Other"; + } + else if(problem.equals("copyRightViolation")){ + reasonList[0] = "Press photo"; + reasonList[1] = "Random photo from internet"; + reasonList[2] = "Logo"; + reasonList[3] = "Other"; + } + + alert.setMultiChoiceItems(reasonList, checkedItems, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int position, boolean isChecked) { + if(isChecked){ + mUserReason.add(position); + }else{ + mUserReason.remove((Integer.valueOf(position))); + } + } + }); + + alert.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + + String reason = "Because it is "; + for (int j = 0; j < mUserReason.size(); j++) { + reason = reason + reasonList[mUserReason.get(j)]; + if (j != mUserReason.size() - 1) { + reason = reason + ", "; + } + } + + ((ReviewActivity)context).reviewController.swipeToNext(); + ((ReviewActivity)context).runRandomizer(); + + DeleteTask deleteTask = new DeleteTask(context, media, reason); + deleteTask.execute(); + } + }); + alert.setNegativeButton("Cancel" , null); + + + AlertDialog d = alert.create(); + d.show(); + +// AlertDialog.Builder alert = new AlertDialog.Builder(context); +// alert.setMessage(question); +// final EditText input = ne +// w EditText(context); +// input.setText(defaultValue); +// alert.setView(input); +// input.requestFocus(); +// alert.setPositiveButton(R.string.ok, (dialog, whichButton) -> { +// String reason = input.getText().toString(); +// +// ((ReviewActivity)context).reviewController.swipeToNext(); +// ((ReviewActivity)context).runRandomizer(); +// +// DeleteTask deleteTask = new DeleteTask(context, media, reason); +// deleteTask.execute(); +// }); +// alert.setNegativeButton(R.string.cancel, (dialog, whichButton) -> { +// }); +// AlertDialog d = alert.create(); +// input.addTextChangedListener(new TextWatcher() { +// private void handleText() { +// final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE); +// if (input.getText().length() == 0) { +// okButton.setEnabled(false); +// } else { +// okButton.setEnabled(true); +// } +// } +// +// @Override +// public void afterTextChanged(Editable arg0) { +// handleText(); +// } +// +// @Override +// public void beforeTextChanged(CharSequence s, int start, int count, int after) { +// } +// +// @Override +// public void onTextChanged(CharSequence s, int start, int before, int count) { +// } +// }); +// d.show(); +// d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(defaultValue.length() > 0); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index d4f0fe7a6..a94911b9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -15,6 +15,7 @@ import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.explore.categories.ExploreActivity; import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.upload.UploadActivity; @@ -64,4 +65,7 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract BookmarksActivity bindBookmarksActivity(); + + @ContributesAndroidInjector + abstract ReviewActivity bindReviewActivity(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 7e182e717..ec060611c 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -12,6 +12,9 @@ import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; +import fr.free.nrw.commons.review.CheckCategoryTask; +import fr.free.nrw.commons.review.SendThankTask; +import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.nearby.PlaceRenderer; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.upload.FileProcessor; @@ -42,6 +45,10 @@ public interface CommonsApplicationComponent extends AndroidInjector firstRevisionOfFile(String filename) { + return Single.fromCallable(() -> { + CustomApiResult res = api.action("query") + .param("prop", "revisions") + .param("rvprop", "timestamp|ids|user") + .param("titles", filename) + .param("rvdir", "newer") + .param("rvlimit", "1") + .get(); + return new Revision( + res.getString("/api/query/pages/page/revisions/rev/@revid"), + res.getString("/api/query/pages/page/revisions/rev/@user"), + filename); + }); + } + @Override @NonNull public List getNotifications(boolean archived) { @@ -758,6 +799,50 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return CategoryImageUtils.getMediaList(childNodes); } + /** + * This method takes search keyword as input and returns a list of Media objects filtered using search query + * It uses the generator query API to get the images searched using a query, 25 at a time. + * @param query keyword to search images on commons + * @return + */ +// @Override + @NonNull + public List searchImages(String query, int offset) { + List imageNodes = null; + List authorNodes = null; + CustomApiResult customApiResult; + try { + customApiResult= api.action("query") + .param("format", "xml") + .param("generator", "search") + .param("gsrwhat", "text") + .param("gsrnamespace", "6") + .param("gsrlimit", "25") + .param("gsroffset",offset) + .param("gsrsearch", query) + .param("prop", "imageinfo") + .get(); + imageNodes= customApiResult.getNodes("/api/query/pages/page/@title"); + authorNodes= customApiResult.getNodes("/api/query/pages/page/imageinfo/ii/@user"); + } catch (IOException e) { + Timber.e(e, "Failed to obtain searchImages"); + } + + if (imageNodes == null) { + return new ArrayList<>(); + } + + List images = new ArrayList<>(); + + for (int i=0; i< imageNodes.size();i++){ + String imgName = imageNodes.get(i).getDocument().getTextContent(); + Media media = new Media(imgName); + media.setCreator(authorNodes.get(i).getDocument().getTextContent()); + images.add(media); + } + return images; + } + /** * This method takes search keyword as input and returns a list of categories objects filtered using search query * It uses the generator query API to get the categories searched using a query, 25 at a time. @@ -947,6 +1032,78 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return userBlocked; } +// /** +// * This takes userName as input, which is then used to fetch the feedback/achievements +// * statistics using OkHttp and JavaRx. This function return JSONObject +// * @param userName MediaWiki user name +// * @return +// */ +// @Override +// public Single getAchievements(String userName) { +// final String fetchAchievementUrlTemplate = +// wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; +// return Single.fromCallable(() -> { +// String url = String.format( +// Locale.ENGLISH, +// fetchAchievementUrlTemplate, +// new PageTitle(userName).getText()); +// HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); +// urlBuilder.addQueryParameter("user", userName); +// Timber.i("Url %s", urlBuilder.toString()); +// Request request = new Request.Builder() +// .url(urlBuilder.toString()) +// .build(); +// Response response = okHttpClient.newCall(request).execute(); +// if (response != null && response.body() != null && response.isSuccessful()) { +// String json = response.body().string(); +// if (json == null) { +// return null; +// } +// return gson.fromJson(json, FeedbackResponse.class); +// } +// return null; +// }); +// +// } + +// /** +// * The method returns the picture of the day +// * +// * @return Media object corresponding to the picture of the day +// */ +// @Override +// @Nullable +// public Single getPictureOfTheDay() { +// return Single.fromCallable(() -> { +// CustomApiResult apiResult = null; +// try { +// String template = "Template:Potd/" + DateUtils.getCurrentDate(); +// CustomMwApi.RequestBuilder requestBuilder = api.action("query") +// .param("generator", "images") +// .param("format", "xml") +// .param("titles", template) +// .param("prop", "imageinfo") +// .param("iiprop", "url|extmetadata"); +// +// apiResult = requestBuilder.get(); +// } catch (IOException e) { +// Timber.e(e, "Failed to obtain searchCategories"); +// } +// +// if (apiResult == null) { +// return null; +// } +// +// CustomApiResult imageNode = apiResult.getNode("/api/query/pages/page"); +// if (imageNode == null +// || imageNode.getDocument() == null) { +// return null; +// } +// +// return CategoryImageUtils.getMediaFromPage(imageNode.getDocument()); +// }); +// } + private Date parseMWDate(String mwDate) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC isoFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -967,4 +1124,68 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { Timber.e(e, "Error occurred while logging out"); } } + +// @Override public Single getCampaigns() { +// return Single.fromCallable(() -> { +// Request request = new Request.Builder().url(WIKIMEDIA_CAMPAIGNS_BASE_URL).build(); +// Response response = okHttpClient.newCall(request).execute(); +// if (response != null && response.body() != null && response.isSuccessful()) { +// String json = response.body().string(); +// if (json == null) { +// return null; +// } +// return gson.fromJson(json, CampaignResponseDTO.class); +// } +// return null; +// }); +// } + + private String formatMWDate(Date date) { + return isoFormat.format(date); + } + + public Media getRecentRandomImage() throws IOException { + Media media = null; + int tries = 0; + Random r = new Random(); + + while (media == null && tries < MAX_RANDOM_TRIES) { + Date now = new Date(); + Date startDate = new Date(now.getTime() - r.nextInt(RANDOM_SECONDS) * 1000L); + CustomApiResult apiResult = null; + try { + CustomMwApi.RequestBuilder requestBuilder = api.action("query") + .param("list", "recentchanges") + .param("rcstart", formatMWDate(startDate)) + .param("rcnamespace", FILE_NAMESPACE) + .param("rcprop", "title|ids") + .param("rctype", "new|log") + .param("rctoponly", "1"); + + apiResult = requestBuilder.get(); + } catch (IOException e) { + Timber.e(e, "Failed to obtain recent random"); + } + if (apiResult != null) { + CustomApiResult recentChangesNode = apiResult.getNode("/api/query/recentchanges"); + if (recentChangesNode != null + && recentChangesNode.getDocument() != null + && recentChangesNode.getDocument().getChildNodes() != null + && recentChangesNode.getDocument().getChildNodes().getLength() > 0) { + NodeList childNodes = recentChangesNode.getDocument().getChildNodes(); + String imageTitle = RecentChangesImageUtils.findImageInRecentChanges(childNodes); + if (imageTitle != null) { + boolean deletionStatus = pageExists("Commons:Deletion_requests/" + imageTitle); + if (!deletionStatus) { + // strip File: prefix + imageTitle = imageTitle.replace("File:", ""); + media = new Media(imageTitle); + } + } + } + } + tries++; + } + return media; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 3ec017bd4..11dc042b4 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.util.List; import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.campaigns.CampaignResponseDTO; import fr.free.nrw.commons.notification.Notification; import io.reactivex.Observable; import io.reactivex.Single; @@ -104,7 +105,16 @@ public interface MediaWikiApi { void logout(); +// Single getCampaigns(); + + boolean thank(String editToken, String revision) throws IOException; + + Single firstRevisionOfFile(String filename); + interface ProgressListener { void onProgress(long transferred, long total); } + + @Nullable + Media getRecentRandomImage() throws IOException; } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/Revision.java b/app/src/main/java/fr/free/nrw/commons/mwapi/Revision.java new file mode 100644 index 000000000..60b264539 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/Revision.java @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.mwapi; + +import fr.free.nrw.commons.PageTitle; + +public class Revision { + public final String revisionId; + public final String username; + public final PageTitle pageTitle; + + public Revision(String revisionId, String username, String pageTitle) { + this.revisionId = revisionId; + this.username = username; + this.pageTitle = new PageTitle(pageTitle); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java b/app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java new file mode 100644 index 000000000..8775e2c17 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/CheckCategoryTask.java @@ -0,0 +1,129 @@ +package fr.free.nrw.commons.review; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.AsyncTask; +import android.view.Gravity; +import android.widget.Toast; + +import javax.inject.Inject; + +import androidx.core.app.NotificationCompat; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import timber.log.Timber; + + +// Example code: +// CheckCategoryTask deleteTask = new CheckCategoryTask(getActivity(), media); + +// TODO: refactor; see DeleteTask and SendThankTask +public class CheckCategoryTask extends AsyncTask { + + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + + public static final int NOTIFICATION_CHECK_CATEGORY = 0x101; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; + private Context context; + private Media media; + + public CheckCategoryTask(Context context, Media media){ + this.context = context; + this.media = media; + } + + @Override + protected void onPreExecute(){ + ApplicationlessInjection + .getInstance(context.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context); + Toast toast = new Toast(context); + toast.setGravity(Gravity.CENTER,0,0); + toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + protected Boolean doInBackground(Void ...voids) { + publishProgress(0); + + String editToken; + String authCookie; + String summary = context.getString(R.string.check_category_edit_summary); + + authCookie = sessionManager.getAuthCookie(); + mwApi.setAuthCookie(authCookie); + + try { + editToken = mwApi.getEditToken(); + if (editToken.equals("+\\")) { + return false; + } + publishProgress(1); + + mwApi.appendEdit(editToken, "\n{{subst:chc}}\n", media.getFilename(), summary); + publishProgress(2); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return false; + } + return true; + } + + @Override + protected void onProgressUpdate (Integer... values){ + super.onProgressUpdate(values); + + int[] messages = new int[]{R.string.getting_edit_token, R.string.check_category_adding_template}; + String message = ""; + if (0 < values[0] && values[0] < messages.length) { + message = context.getString(messages[values[0]]); + } + + notificationBuilder.setContentTitle(context.getString(R.string.check_category_notification_title, media.getDisplayTitle())) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(messages.length, values[0], false) + .setOngoing(true); + notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); + } + + @Override + protected void onPostExecute(Boolean result) { + String message = ""; + String title = ""; + + if (result){ + title = context.getString(R.string.check_category_success_title); + message = context.getString(R.string.check_category_success_message, media.getDisplayTitle()); + } + else { + title = context.getString(R.string.check_category_failure_title); + message = context.getString(R.string.check_category_failure_message, media.getDisplayTitle()); + } + + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0,0,false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java new file mode 100644 index 000000000..f8e8985b4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -0,0 +1,166 @@ +package fr.free.nrw.commons.review; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; + +import com.google.android.material.navigation.NavigationView; +import com.viewpagerindicator.CirclePageIndicator; + +import java.io.IOException; +import java.util.ArrayList; + +import javax.inject.Inject; + +import androidx.appcompat.widget.Toolbar; +import androidx.drawerlayout.widget.DrawerLayout; +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.mwapi.MediaResult; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.MediaDataExtractorUtil; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +public class ReviewActivity extends AuthenticatedActivity { + + @BindView(R.id.toolbar) + Toolbar toolbar; + @BindView(R.id.navigation_view) + NavigationView navigationView; + @BindView(R.id.drawer_layout) + DrawerLayout drawerLayout; + + @BindView(R.id.reviewPager) + ReviewViewPager reviewPager; + + @BindView(R.id.skip_image) + Button skip_image_button; + + @Inject MediaWikiApi mwApi; + + public ReviewPagerAdapter reviewPagerAdapter; + + public ReviewController reviewController; + + @BindView(R.id.reviewPagerIndicator) + public CirclePageIndicator pagerIndicator; + + + @Override + protected void onAuthCookieAcquired(String authCookie) { + + } + + @Override + protected void onAuthFailure() { + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_review); + ButterKnife.bind(this); + initDrawer(); + + reviewController = new ReviewController(this); + + reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager()); + reviewPager.setAdapter(reviewPagerAdapter); + reviewPagerAdapter.getItem(0); + pagerIndicator.setViewPager(reviewPager); + + runRandomizer(); //Run randomizer whenever everything is ready so that a first random image will be added + + skip_image_button.setOnClickListener(view -> runRandomizer()); + } + + + + public boolean runRandomizer() { + ProgressBar progressBar = reviewPagerAdapter.reviewImageFragments[reviewPager.getCurrentItem()].progressBar; + if (progressBar != null) { + progressBar.setVisibility(View.VISIBLE); + } + + reviewPager.setCurrentItem(0); + Observable.fromCallable(() -> { + String result = ""; + try { + Media media = mwApi.getRecentRandomImage(); + if (media != null) { + result = media.getFilename(); + } + } catch (IOException e) { + Timber.e("Error fetching recent random image: " + e.toString()); + } + return result; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateImage); + return true; + } + + private void updateImage(String fileName) { + if (fileName.length() == 0) { + ViewUtil.showShortSnackbar(drawerLayout, R.string.error_review); + return; + } + reviewController.onImageRefreshed(fileName); //file name is updated + mwApi.firstRevisionOfFile("File:" + fileName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(revision -> { + reviewController.firstRevision = revision; + reviewPagerAdapter.updateFileInformation(fileName, revision); + }); + reviewPager.setCurrentItem(0); + Observable.fromCallable(() -> { + MediaResult media = mwApi.fetchMediaByFilename("File:" + fileName); + return MediaDataExtractorUtil.extractCategories(media.getWikiSource()); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateCategories, this::categoryFetchError); + + + } + + private void categoryFetchError(Throwable throwable) { + Timber.e(throwable, "Error fetching categories"); + ViewUtil.showShortSnackbar(drawerLayout, R.string.error_review_categories); + } + + private void updateCategories(ArrayList categories) { + reviewController.onCategoriesRefreshed(categories); + reviewPagerAdapter.updateCategories(); + } + + /** + * References ReviewPagerAdapter to null before the activity is destroyed + */ + @Override + public void onDestroy() { + super.onDestroy(); + } + + /** + * Consumers should be simply using this method to use this activity. + * @param context + * @param title Page title + */ + public static void startYourself(Context context, String title) { + Intent reviewActivity = new Intent(context, ReviewActivity.class); + context.startActivity(reviewActivity); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java new file mode 100644 index 000000000..a95cc522d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.review; + +import android.content.Context; + +import java.util.ArrayList; + +import androidx.annotation.Nullable; +import androidx.viewpager.widget.ViewPager; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.delete.DeleteTask; +import fr.free.nrw.commons.mwapi.Revision; + +public class ReviewController { + private String fileName; + @Nullable + public Revision firstRevision; // TODO: maybe we can expand this class to include fileName + protected static ArrayList categories; + + private ReviewPagerAdapter reviewPagerAdapter; + private ViewPager viewPager; + private ReviewActivity reviewActivity; + + ReviewController(Context context) { + reviewActivity = (ReviewActivity)context; + reviewPagerAdapter = reviewActivity.reviewPagerAdapter; + viewPager = ((ReviewActivity)context).reviewPager; + } + + public void onImageRefreshed(String fileName) { + this.fileName = fileName; + ReviewController.categories = new ArrayList<>(); + } + + public void onCategoriesRefreshed(ArrayList categories) { + ReviewController.categories = categories; + } + + public void swipeToNext() { + int nextPos = viewPager.getCurrentItem()+1; + if (nextPos <= 3) { + viewPager.setCurrentItem(nextPos); + } else { + reviewActivity.runRandomizer(); + } + } + + public void reportSpam() { + DeleteTask.askReasonAndExecute(new Media("File:"+fileName), + reviewActivity, + reviewActivity.getResources().getString(R.string.review_spam_report_question), + reviewActivity.getResources().getString(R.string.review_spam_report_problem)); + } + + public void reportPossibleCopyRightViolation() { + DeleteTask.askReasonAndExecute(new Media("File:"+fileName), + reviewActivity, + reviewActivity.getResources().getString(R.string.review_c_violation_report_question), + reviewActivity.getResources().getString(R.string.review_c_violation_report_problem)); + } + + public void reportWrongCategory() { + new CheckCategoryTask(reviewActivity, new Media("File:"+fileName)).execute(); + swipeToNext(); + } + + public void sendThanks() { + new SendThankTask(reviewActivity, new Media("File:"+fileName), firstRevision).execute(); + swipeToNext(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java new file mode 100644 index 000000000..d230e5c84 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java @@ -0,0 +1,164 @@ +package fr.free.nrw.commons.review; + +import android.graphics.Color; +import android.os.Bundle; +import android.text.Html; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.facebook.drawee.view.SimpleDraweeView; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.mwapi.Revision; + +public class ReviewImageFragment extends CommonsDaggerSupportFragment { + + public static final int SPAM = 0; + public static final int COPYRIGHT = 1; + public static final int CATEGORY = 2; + public static final int THANKS = 3; + + private int position; + private String fileName; + private String catString; + + private View textViewQuestionContext; + private View imageCaption; + private View textViewQuestion; + private SimpleDraweeView simpleDraweeView; + + private Button yesButton; + private Button noButton; + + public ProgressBar progressBar; + private Revision revision; + + + public void update(int position, String fileName, Revision revision) { + this.position = position; + this.fileName = fileName; + this.revision = revision; + + fillImageCaption(); + + if (simpleDraweeView != null) { + simpleDraweeView.setImageURI(Utils.makeThumbBaseUrl(fileName)); + progressBar.setVisibility(View.GONE); + } + } + + public void updateCategories(Iterable categories) { + if (categories != null && isAdded()) { + catString = TextUtils.join(", ", categories); + if (catString != null && !catString.equals("") && textViewQuestionContext != null) { + catString = "" + catString + ""; + String stringToConvertHtml = String.format(getResources().getString(R.string.review_category_explanation), catString); + ((TextView) textViewQuestionContext).setText(Html.fromHtml(stringToConvertHtml)); + } else if (textViewQuestionContext != null) { + ((TextView) textViewQuestionContext).setText(getResources().getString(R.string.review_no_category)); + } + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + position = getArguments().getInt("position"); + View layoutView = inflater.inflate(R.layout.fragment_review_image, container, + false); + progressBar = layoutView.findViewById(R.id.progressBar); + textViewQuestion = layoutView.findViewById(R.id.reviewQuestion); + textViewQuestionContext = layoutView.findViewById(R.id.reviewQuestionContext); + imageCaption = layoutView.findViewById(R.id.imageCaption); + yesButton = layoutView.findViewById(R.id.yesButton); + noButton = layoutView.findViewById(R.id.noButton); + + fillImageCaption(); + + String question, explanation, yesButtonText, noButtonText; + switch (position) { + case COPYRIGHT: + question = getString(R.string.review_copyright); + explanation = getString(R.string.review_copyright_explanation); + yesButtonText = getString(R.string.review_copyright_yes_button_text); + noButtonText = getString(R.string.review_copyright_no_button_text); + yesButton.setOnClickListener(view -> { + ((ReviewActivity) getActivity()).reviewController.reportPossibleCopyRightViolation(); + }); + break; + case CATEGORY: + question = getString(R.string.review_category); + explanation = getString(R.string.review_no_category); + yesButtonText = getString(R.string.review_category_yes_button_text); + noButtonText = getString(R.string.review_category_no_button_text); + yesButton.setOnClickListener(view -> { + ((ReviewActivity) getActivity()).reviewController.reportWrongCategory(); + }); + break; + case SPAM: + question = getString(R.string.review_spam); + explanation = getString(R.string.review_spam_explanation); + yesButtonText = getString(R.string.review_spam_yes_button_text); + noButtonText = getString(R.string.review_spam_no_button_text); + yesButton.setOnClickListener(view -> { + ((ReviewActivity) getActivity()).reviewController.reportSpam(); + }); + break; + case THANKS: + question = getString(R.string.review_thanks); + explanation = getString(R.string.review_thanks_explanation, ((ReviewActivity) getActivity()).reviewController.firstRevision.username); + yesButtonText = getString(R.string.review_thanks_yes_button_text); + noButtonText = getString(R.string.review_thanks_no_button_text); + yesButton.setTextColor(Color.parseColor("#228b22")); + noButton.setTextColor(Color.parseColor("#116aaa")); + yesButton.setOnClickListener(view -> { + ((ReviewActivity) getActivity()).reviewController.sendThanks(); + }); + break; + default : + question = "How did we get here?"; + explanation = "No idea."; + yesButtonText = "yes"; + noButtonText = "no"; + } + + noButton.setOnClickListener(view -> { + ((ReviewActivity) getActivity()).reviewController.swipeToNext(); + }); + + ((TextView) textViewQuestion).setText(question); + ((TextView) textViewQuestionContext).setText(explanation); + yesButton.setText(yesButtonText); + noButton.setText(noButtonText); + + if(position==CATEGORY){ + updateCategories(ReviewController.categories); + } + + simpleDraweeView = layoutView.findViewById(R.id.imageView); + + if (fileName != null) { + simpleDraweeView.setImageURI(Utils.makeThumbBaseUrl(fileName)); + progressBar.setVisibility(View.GONE); + } + return layoutView; + } + + private void fillImageCaption() { + if (imageCaption != null && fileName != null && revision != null) { + ((TextView) imageCaption).setText(fileName + " is uploaded by: " + revision.username); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java new file mode 100644 index 000000000..660a0a0b0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java @@ -0,0 +1,49 @@ +package fr.free.nrw.commons.review; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import fr.free.nrw.commons.mwapi.Revision; + +public class ReviewPagerAdapter extends FragmentStatePagerAdapter { + ReviewImageFragment[] reviewImageFragments; + + + public ReviewPagerAdapter(FragmentManager fm) { + super(fm); + reviewImageFragments = new ReviewImageFragment[] { + new ReviewImageFragment(), + new ReviewImageFragment(), + new ReviewImageFragment(), + new ReviewImageFragment() + }; + } + + @Override + public int getCount() { + return reviewImageFragments.length; + } + + public void updateFileInformation(String fileName, Revision revision) { + for (int i = 0; i < getCount(); i++) { + ReviewImageFragment fragment = reviewImageFragments[i]; + fragment.update(i, fileName, revision); + } + } + + public void updateCategories() { + ReviewImageFragment categoryFragment = reviewImageFragments[ReviewImageFragment.CATEGORY]; + categoryFragment.updateCategories(ReviewController.categories); + } + + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + bundle.putInt("position", position); + reviewImageFragments[position].setArguments(bundle); + return reviewImageFragments[position]; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java new file mode 100644 index 000000000..95740aac0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.review; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.viewpager.widget.ViewPager; + +public class ReviewViewPager extends ViewPager { + + public ReviewViewPager(Context context) { + super(context); + } + + public ReviewViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + // Never allow swiping to switch between pages + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Never allow swiping to switch between pages + return false; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java b/app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java new file mode 100644 index 000000000..51e21b2a9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/SendThankTask.java @@ -0,0 +1,138 @@ +package fr.free.nrw.commons.review; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.AsyncTask; +import android.view.Gravity; +import android.widget.Toast; + +import javax.inject.Inject; + +import androidx.core.app.NotificationCompat; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.mwapi.Revision; +import timber.log.Timber; + +// example code: +// +// media = new Media("File:Iru.png"); +// Observable.fromCallable(() -> mwApi.firstRevisionOfFile(media.getFilename())) +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe(revision -> { +// SendThankTask task = new SendThankTask(getActivity(), media, revision); +// task.execute(); +// }); + +public class SendThankTask extends AsyncTask { + + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + + public static final int NOTIFICATION_SEND_THANK = 0x102; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notificationBuilder; + private Context context; + private Media media; + private Revision revision; + + public SendThankTask(Context context, Media media, Revision revision){ + this.context = context; + this.media = media; + this.revision = revision; + } + + @Override + protected void onPreExecute(){ + ApplicationlessInjection + .getInstance(context.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); + + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(context); + Toast toast = new Toast(context); + toast.setGravity(Gravity.CENTER,0,0); + toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + protected Boolean doInBackground(Void ...voids) { + publishProgress(0); + + String editToken; + String authCookie; + + authCookie = sessionManager.getAuthCookie(); + mwApi.setAuthCookie(authCookie); + + try { + editToken = mwApi.getEditToken(); + if (editToken.equals("+\\")) { + return false; + } + publishProgress(1); + + mwApi.thank(editToken, revision.revisionId); + + publishProgress(2); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return false; + } + return true; + } + + @Override + protected void onProgressUpdate (Integer... values){ + super.onProgressUpdate(values); + + int[] messages = new int[]{R.string.getting_edit_token, R.string.send_thank_send}; + String message = ""; + if (0 < values[0] && values[0] < messages.length) { + message = context.getString(messages[values[0]]); + } + + notificationBuilder.setContentTitle(context.getString(R.string.send_thank_notification_title)) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(messages.length, values[0], false) + .setOngoing(true); + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + } + + @Override + protected void onPostExecute(Boolean result) { + String message = ""; + String title = ""; + + if (result){ + title = context.getString(R.string.send_thank_success_title); + message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); + } + else { + title = context.getString(R.string.send_thank_failure_title); + message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle()); + } + + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0,0,false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 923115475..23263e1a2 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -39,6 +39,8 @@ import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.explore.categories.ExploreActivity; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.logging.CommonsLogSender; +import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.settings.SettingsActivity; import timber.log.Timber; @@ -227,6 +229,11 @@ public abstract class NavigationBaseActivity extends BaseActivity drawerLayout.closeDrawer(navigationView); BookmarksActivity.startYourself(this); return true; + + case R.id.action_review: + drawerLayout.closeDrawer(navigationView); + ReviewActivity.startYourself(this, getString(R.string.title_activity_review)); + return true; default: Timber.e("Unknown option [%s] selected from the navigation menu", itemId); return false; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java new file mode 100644 index 000000000..63421a8e4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.utils; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MediaDataExtractorUtil { + + /** + * We could fetch all category links from API, but we actually only want the ones + * directly in the page source so they're editable. In the future this may change. + * + * @param source wikitext source code + */ + public static ArrayList extractCategories(String source) { + ArrayList categories = new ArrayList<>(); + Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE); + Matcher matcher = regex.matcher(source); + while (matcher.find()) { + String cat = matcher.group(1).trim(); + categories.add(cat); + } + + return categories; + } + + +} diff --git a/app/src/main/res/drawable/ic_check_black_24dp.xml b/app/src/main/res/drawable/ic_check_black_24dp.xml new file mode 100644 index 000000000..3c728c59f --- /dev/null +++ b/app/src/main/res/drawable/ic_check_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 000000000..8229a9a64 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/tab_indicator_default.xml b/app/src/main/res/drawable/tab_indicator_default.xml new file mode 100644 index 000000000..341f4d706 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator_default.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_indicator_selected.xml b/app/src/main/res/drawable/tab_indicator_selected.xml new file mode 100644 index 000000000..41c1bcf73 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator_selected.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selector.xml b/app/src/main/res/drawable/tab_selector.xml new file mode 100644 index 000000000..001747c31 --- /dev/null +++ b/app/src/main/res/drawable/tab_selector.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_review.xml b/app/src/main/res/layout/activity_review.xml new file mode 100644 index 000000000..4fc647f6c --- /dev/null +++ b/app/src/main/res/layout/activity_review.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + +