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
This commit is contained in:
Silky Priya 2019-03-21 17:35:23 +05:30 committed by neslihanturan
parent a1a65d0832
commit a32ba452ec
33 changed files with 1594 additions and 33 deletions

View file

@ -131,6 +131,10 @@
android:name=".bookmarks.BookmarksActivity"
android:label="@string/title_activity_bookmarks" />
<activity
android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" />
<service android:name=".upload.UploadService" />
<service
android:name=".auth.WikiAccountAuthenticatorService"

View file

@ -26,6 +26,7 @@ import javax.xml.parsers.ParserConfigurationException;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
import timber.log.Timber;
/**
@ -78,7 +79,7 @@ public class MediaDataExtractor {
// In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(result.getWikiSource());
categories = MediaDataExtractorUtil.extractCategories(result.getWikiSource());
// Description template info is extracted from preprocessor XML
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
@ -107,7 +108,7 @@ public class MediaDataExtractor {
e.printStackTrace();
}
}
private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
Document doc;
try {

View file

@ -1,22 +1,25 @@
package fr.free.nrw.commons.delete;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder;
import android.view.Gravity;
import android.widget.Toast;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import javax.inject.Inject;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
@ -24,6 +27,7 @@ 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.review.ReviewActivity;
import timber.log.Timber;
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
@ -63,7 +67,7 @@ public class DeleteTask extends AsyncTask<Void, Integer, Boolean> {
.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<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
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<Integer> 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);
}
}

View file

@ -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();
}

View file

@ -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<Application
void inject(DeleteTask deleteTask);
void inject(CheckCategoryTask checkCategoryTask);
void inject(SendThankTask sendThankTask);
void inject(SettingsFragment fragment);
@Override

View file

@ -16,6 +16,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
@Module
@ -67,4 +68,7 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract BookmarkLocationsFragment bindBookmarkLocationListFragment();
@ContributesAndroidInjector
abstract ReviewImageFragment bindReviewOutOfContextFragment();
}

View file

@ -2,14 +2,16 @@ package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@ -17,6 +19,7 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
@ -31,6 +34,7 @@ import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Provider;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@ -414,6 +418,53 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if(isDeleted) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
//Reviewer correct me if i have misunderstood something over here
//But how does this if (delete.getVisibility() == View.VISIBLE) {
// enableDeleteButton(true); makes sense ?
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setMessage("Why should this fileckathon-2018 be deleted?");
final EditText input = new EditText(getActivity());
alert.setView(input);
input.requestFocus();
alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
String reason = input.getText().toString();
DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
deleteTask.execute();
enableDeleteButton(false);
}
});
alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int 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(false);
}
@SuppressLint("CheckResult")

View file

@ -0,0 +1,49 @@
package fr.free.nrw.commons.media;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.util.Random;
import javax.annotation.Nullable;
public class RecentChangesImageUtils {
private static final String[] imageExtensions = new String[]
{".jpg", ".jpeg", ".png"};
@Nullable
public static String findImageInRecentChanges(NodeList childNodes) {
String imageTitle;
Random r = new Random();
int count = childNodes.getLength();
// Build a range array
int[] randomIndexes = new int[count];
for (int i = 0; i < count; i++) {
randomIndexes[i] = i;
}
// Then shuffle it
for (int i = 0; i < count; i++) {
int swapIndex = r.nextInt(count);
int temp = randomIndexes[i];
randomIndexes[i] = randomIndexes[swapIndex];
randomIndexes[swapIndex] = temp;
}
for (int i = 0; i < count; i++) {
int randomIndex = randomIndexes[i];
Element e = (Element) childNodes.item(randomIndex);
if (e.getAttribute("type").equals("log") && !e.getAttribute("old_revid").equals("0")) {
// For log entries, we only want ones where old_revid is zero, indicating a new file
continue;
}
imageTitle = e.getAttribute("title");
for (String imageExtension : imageExtensions) {
if (imageTitle.toLowerCase().endsWith(imageExtension)) {
return imageTitle;
}
}
}
return null;
}
}

View file

@ -3,8 +3,6 @@ package fr.free.nrw.commons.mwapi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.gson.Gson;
@ -34,9 +32,12 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
@ -44,6 +45,7 @@ import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.RecentChangesImageUtils;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils;
import fr.free.nrw.commons.utils.ConfigUtils;
@ -61,6 +63,14 @@ import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
*/
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
// Give up if no random recent image found after 5 tries
private static final int MAX_RANDOM_TRIES = 5;
// Random image request is for some time in the past 30 days
private static final int RANDOM_SECONDS = 60 * 60 * 24 * 30;
// Assuming MW always gives me UTC
private static final SimpleDateFormat isoFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
private static final String FILE_NAMESPACE = "6";
private AbstractHttpClient httpClient;
private CustomMwApi api;
private CustomMwApi wikidataApi;
@ -256,6 +266,19 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/@_idx")) != -1;
}
@Override
public boolean thank(String editToken, String revision) throws IOException {
CustomApiResult res = api.action("thank")
.param("rev", revision)
.param("token", editToken)
.param("source", getUserAgent())
.post();
String r = res.getString("/api/result/@success");
// Does this correctly check the success/failure?
// The docs https://www.mediawiki.org/wiki/Extension:Thanks seems unclear about that.
return r.equals("success");
}
@Override
@Nullable
public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
@ -563,6 +586,24 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/revisions/rev");
}
@Override
@Nullable
public Single<Revision> 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<Notification> 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<Media> searchImages(String query, int offset) {
List<CustomApiResult> imageNodes = null;
List<CustomApiResult> 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<Media> 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<FeedbackResponse> 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<Media> 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<CampaignResponseDTO> 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;
}
}

View file

@ -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<CampaignResponseDTO> getCampaigns();
boolean thank(String editToken, String revision) throws IOException;
Single<Revision> firstRevisionOfFile(String filename);
interface ProgressListener {
void onProgress(long transferred, long total);
}
@Nullable
Media getRecentRandomImage() throws IOException;
}

View file

@ -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);
}
}

View file

@ -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<Void, Integer, Boolean> {
@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());
}
}

View file

@ -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<String> 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);
}
}

View file

@ -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<String> 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<String> 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();
}
}

View file

@ -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<String> categories) {
if (categories != null && isAdded()) {
catString = TextUtils.join(", ", categories);
if (catString != null && !catString.equals("") && textViewQuestionContext != null) {
catString = "<b>" + catString + "</b>";
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);
}
}
}

View file

@ -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];
}
}

View file

@ -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;
}
}

View file

@ -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<Void, Integer, Boolean> {
@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());
}
}

View file

@ -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;

View file

@ -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<String> extractCategories(String source) {
ArrayList<String> 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;
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="2dp"
android:useLevel="false">
<solid android:color="@android:color/darker_gray"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:innerRadius="0dp"
android:shape="ring"
android:thickness="4dp"
android:useLevel="false"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/commons_app_blue_dark"/>
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/tab_indicator_selected"
android:state_selected="true"/>
<item android:drawable="@drawable/tab_indicator_default"/>
</selector>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar"/>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:orientation="vertical"
android:weightSum="15">
<Button
android:id="@+id/skip_image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:text="SKIP THIS IMAGE"
android:textColor="@color/button_blue_dark"
android:background="@android:color/transparent"
android:layout_weight="1"
android:textStyle="bold"/>
<fr.free.nrw.commons.review.ReviewViewPager
android:id="@+id/reviewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fadingEdge="none"
android:layout_weight="13.5"/>
<com.viewpagerindicator.CirclePageIndicator
android:id="@+id/reviewPagerIndicator"
android:layout_height="0dp"
android:layout_width="match_parent"
android:layout_gravity="center"
android:foregroundGravity="center_vertical"
android:elevation="1dp"
android:background="?attr/colorPrimaryDark"
android:layout_weight="0.5"
/>
</LinearLayout>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/drawer_view" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scroll"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_above="@+id/bottomview"
android:layout_alignParentTop="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="300dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_marginTop="0dp"
android:src="@drawable/commons_logo_large" />
<RelativeLayout
android:id="@+id/uploadOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:gravity="center"
android:layout_alignParentBottom="true"
android:background="#77000000"
android:padding="@dimen/tiny_gap"
>
<TextView
android:id="@+id/imageCaption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFFFF"
style="?android:textAppearanceMedium"
/>
</RelativeLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="visible"
/>
</RelativeLayout>
<TextView
android:id="@+id/reviewQuestion"
android:layout_width="match_parent"
android:layout_height="80dp"
android:textAlignment="center"
android:textSize="32sp"
android:textColor="?attr/reviewHeading"
android:gravity="center_vertical"
android:text="testing1"
/>
<TextView
android:id="@+id/reviewQuestionContext"
android:layout_width="match_parent"
android:layout_height="100dp"
android:textAlignment="center"
android:textSize="22sp"
android:layout_marginBottom="15dp"
android:gravity="center_vertical"
android:text="testing2"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:weightSum="2"
android:orientation="horizontal">
<Button
android:id="@+id/yesButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="@dimen/activity_margin_horizontal"
android:background="@android:color/transparent"
android:text="@string/yes"
android:textSize="18sp"
android:textColor="@color/yes_button_color"
android:textAlignment="center"
/>
<Button
android:id="@+id/noButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_weight="1"
android:layout_margin="@dimen/activity_margin_horizontal"
android:text="@string/no"
android:textSize="18sp"
android:textColor="@color/no_button_color"
android:textAlignment="center"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<View
android:id="@+id/bottomview"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_alignParentBottom="true"
android:background="?attr/colorPrimaryDark"></View>
</RelativeLayout>

View file

@ -22,6 +22,11 @@
android:icon="@drawable/ic_round_star_filled_24px"
android:title="@string/navigation_item_bookmarks"/>
<item
android:id="@+id/action_review"
android:icon="@drawable/ic_check_black_24dp"
android:title="@string/navigation_item_review"/>
</group>
<group android:id="@+id/drawer_account">
<item

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_review_randomizer"
android:title="@string/refresh_button"
app:showAsAction="ifRoom|withText"
android:icon="@drawable/ic_refresh_white_24dp"
/>
</menu>

View file

@ -17,6 +17,7 @@
<attr name="textEnabled" format="reference"/>
<attr name="bookmarkButtonColor" format="reference"/>
<attr name="rowButtonColor" format="reference"/>
<attr name="reviewHeading" format="reference"/>
<attr name="contributionsListBackground" format="reference"/>
<attr name="achievementBackground" format="reference"/>

View file

@ -67,4 +67,6 @@
<color name="black">#000000</color>
<color name="swipe_red" tools:ignore="MissingDefaultResource">#FF0000</color>
<color name="yes_button_color">#B22222</color>
<color name="no_button_color">#006400</color>
</resources>

View file

@ -87,6 +87,7 @@
<string name="title_activity_signup">Sign Up</string>
<string name="title_activity_featured_images">Featured Images</string>
<string name="title_activity_category_details">Category</string>
<string name="title_activity_review">Peer Review</string>
<string name="menu_about">About</string>
<string name="about_license">The Wikimedia Commons app is an open-source app created and maintained by grantees and volunteers of the Wikimedia community. The Wikimedia Foundation is not involved in the creation, development, or maintenance of the app. </string>
<string name="trademarked_name" translatable="false">Wikimedia Commons</string>
@ -229,6 +230,7 @@
<string name="navigation_item_info">Tutorial</string>
<string name="navigation_item_notification">Notifications</string>
<string name="navigation_item_featured_images">Featured</string>
<string name="navigation_item_review">Review</string>
<string name="nearby_needs_permissions">Nearby places cannot be displayed without location permissions</string>
<string name="no_description_found">no description found</string>
<string name="nearby_info_menu_commons_article">Commons file page</string>
@ -259,6 +261,8 @@
<string name="nominate_deletion">Nominate for Deletion</string>
<string name="nominated_for_deletion">This image has been nominated for deletion.</string>
<string name="nominated_see_more"><![CDATA[<u>See webpage for details</u>]]></string>
<string name="nominating_file_for_deletion">Nominating %1$s for deletion.</string>
<string name="nominating_for_deletion_status">Nominating file for deletion: %1$s</string>
<string name="view_browser">View in Browser</string>
<string name="skip_login">Skip</string>
<string name="navigation_item_login">Log in</string>
@ -289,6 +293,8 @@
<string name="no_internet">Internet unavailable</string>
<string name="internet_established">Internet available</string>
<string name="error_notifications">Error fetching notifications</string>
<string name="error_review">Error fetching image for review. Press refresh to try again.</string>
<string name="error_review_categories">Error fetching image categories for review. Press refresh to try again.</string>
<string name="no_notifications">No notifications found</string>
<string name="about_translate"><![CDATA[<u>Translate</u>]]></string>
<string name="about_translate_title">Languages</string>
@ -456,6 +462,50 @@ Upload your first media by tapping on the add button.</string>
<string name="this_function_needs_network_connection">This function requires network connection, please check your connection settings.</string>
<string name="bad_token_error_proposed_solution">Upload failed due to issues with edit token. Please try logging out and in again. </string>
<string name="error_processing_image">Error occurred while processing the image. Please try again!</string>
<string name="getting_edit_token">Getting token for editing</string>
<string name="check_category_adding_template">Adding template for category check</string>
<string name="check_category_notification_title">Requesting category check for %1$s</string>
<string name="check_category_edit_summary">Requesting category check</string>
<string name="check_category_success_title">Requesting category check: Success</string>
<string name="check_category_failure_title">Requesting category check: Failed</string>
<string name="check_category_success_message">Successfully requested category check for %1$s</string>
<string name="check_category_failure_message">Could not request category check for %1$s</string>
<string name="check_category_toast">Requesting category check for %1$s</string>
<string name="nominate_for_deletion_edit_file_page">Adding delete message to file</string>
<string name="nominate_for_deletion_done">Done</string>
<string name="nominate_for_deletion_notify_user">Notifying User on Talk page</string>
<string name="nominate_for_deletion_edit_deletion_request_log">Adding file to Delete requests log</string>
<string name="nominate_for_deletion_create_deletion_request">Creating Delete requests subpage</string>
<string name="notsure">Not sure</string>
<string name="send_thank_success_title">Sending Thanks: Success</string>
<string name="send_thank_success_message">Successfully sent thanks to %1$s</string>
<string name="send_thank_failure_message">Failed to send thanks %1$s</string>
<string name="send_thank_failure_title">Sending Thanks: Failure</string>
<string name="send_thank_send">Sending thanks</string>
<string name="send_thank_notification_title">Sending thanks</string>
<string name="send_thank_toast">Sending Thanks for %1$s</string>
<string name="review_copyright">Does this follow the rules of copyright?</string>
<string name="review_category">Is this correctly categorized?</string>
<string name="review_spam">Is this in-scope?</string>
<string name="review_thanks">Would you like to thank the contributor?</string>
<string name="review_spam_explanation">Click NO to nominate this image for deletion if it is not useful at all.</string>
<string name="review_copyright_explanation">Logos, screenshots, movie posters are often copyright violations.\n Click NO to nominate this image for deletion</string>
<string name="review_thanks_explanation">%1$s will be encouraged by your appreciation</string>
<string name="review_no_category">Oh, this is not even categorized!</string>
<string name="review_category_explanation">This image is under %1$s categories.</string>
<string name="review_spam_report_question">It is out of scope because it is</string>
<string name="review_spam_report_problem">spam</string>
<string name="review_c_violation_report_question">It is copyright violation because it is </string>
<string name="review_c_violation_report_problem">copyRightViolation</string>
<string name="review_category_yes_button_text">NO, MIS-CATEGORIZED</string>
<string name="review_category_no_button_text">SEEMS FINE</string>
<string name="review_spam_yes_button_text">NO, OUT OF SCOPE</string>
<string name="review_spam_no_button_text">SEEMS FINE</string>
<string name="review_copyright_yes_button_text">NO, COPYRIGHT VIOLATION</string>
<string name="review_copyright_no_button_text">SEEMS FINE</string>
<string name="review_thanks_yes_button_text">YES, WHY NOT</string>
<string name="review_thanks_no_button_text">NEXT IMAGE</string>
<plurals name="receiving_shared_content">
<item quantity="one">Receiving shared content. Processing the image might take some time depending on the size of the image and your device</item>

View file

@ -17,6 +17,7 @@
<item name="colorButtonNormal">@color/primaryColor</item>
<item name="bookmarkButtonColor">@color/button_blue_dark</item>
<item name="rowButtonColor">@color/button_blue_dark</item>
<item name="reviewHeading">@color/white</item>
<item name="semitransparentText">@color/commons_app_blue_dark</item>
<item name="subBackground">@color/sub_background_dark</item>
@ -50,6 +51,7 @@
<item name="colorButtonNormal">@color/primaryColor</item>
<item name="bookmarkButtonColor">@color/button_blue</item>
<item name="rowButtonColor">@color/button_blue</item>
<item name="reviewHeading">@color/black</item>
<item name="semitransparentText">@color/commons_app_blue_light</item>
<item name="subBackground">@color/sub_background_light</item>