Merge pull request #1089 from maskaravivek/notifications

Integrated Notifications API
This commit is contained in:
Josephine Lim 2018-01-26 00:28:25 +10:00 committed by GitHub
commit 3697eb5b2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 669 additions and 8 deletions

View file

@ -8,6 +8,7 @@ import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.MultipleShareActivity;
import fr.free.nrw.commons.upload.ShareActivity;
@ -43,4 +44,6 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract NearbyActivity bindNearbyActivity();
@ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity();
}

View file

@ -9,10 +9,10 @@ import android.support.v4.util.LruCache;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
@ -31,7 +31,9 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
private CommonsApplication application;
private Context applicationContext;
public CommonsApplicationModule(Context applicationContext) {
@ -100,8 +102,8 @@ public class CommonsApplicationModule {
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi() {
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
public MediaWikiApi provideMediaWikiApi(Context context) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST);
}
@Provides
@ -133,4 +135,4 @@ public class CommonsApplicationModule {
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
}
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.mwapi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -21,6 +22,8 @@ import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.IOException;
import java.io.InputStream;
@ -36,11 +39,17 @@ import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.notification.Notification;
import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single;
import timber.log.Timber;
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;
/**
* @author Addshore
*/
@ -50,8 +59,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private MWApi api;
private Context context;
public ApacheHttpClientMediaWikiApi(String apiURL) {
public ApacheHttpClientMediaWikiApi(Context context, String apiURL) {
this.context = context;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@ -353,6 +364,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/revisions/rev");
}
@Override
@NonNull
public List<Notification> getNotifications() {
ApiResult notificationNode = null;
try {
notificationNode = api.action("query")
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
.param("notfilter", "!read")
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}
if (notificationNode == null) {
return new ArrayList<>();
}
List<Notification> notifications = new ArrayList<>();
NodeList childNodes = notificationNode.getDocument().getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN)) {
notifications.add(getNotificationFromApiResult(context, node));
}
}
return notifications;
}
@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")

View file

@ -5,7 +5,9 @@ import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -43,6 +45,9 @@ public interface MediaWikiApi {
@NonNull
Observable<String> allCategories(String filter, int searchCatsLimit);
@NonNull
List<Notification> getNotifications() throws IOException;
@NonNull
Observable<String> searchTitles(String title, int searchCatsLimit);
@ -51,6 +56,8 @@ public interface MediaWikiApi {
boolean existingFile(String fileSha1) throws IOException;
@NonNull
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.notification;
import android.support.annotation.Nullable;
public class MarkReadResponse {
@SuppressWarnings("unused") @Nullable
private String result;
public String result() {
return result;
}
public static class QueryMarkReadResponse {
@SuppressWarnings("unused") @Nullable private MarkReadResponse echomarkread;
}
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.notification;
/**
* Created by root on 18.12.2017.
*/
public class Notification {
public NotificationType notificationType;
public String notificationText;
public String date;
public String description;
public String link;
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
this.notificationType = notificationType;
this.notificationText = notificationText;
this.date = date;
this.description = description;
this.link = link;
}
}

View file

@ -0,0 +1,88 @@
package fr.free.nrw.commons.notification;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* Created by root on 18.12.2017.
*/
public class NotificationActivity extends NavigationBaseActivity {
NotificationAdapterFactory notificationAdapterFactory;
@BindView(R.id.listView) RecyclerView recyclerView;
@Inject NotificationController controller;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notification);
ButterKnife.bind(this);
initListView();
initDrawer();
}
private void initListView() {
recyclerView = findViewById(R.id.listView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
addNotifications();
}
@SuppressLint("CheckResult")
private void addNotifications() {
Timber.d("Add notifications");
Observable.fromCallable(() -> controller.getNotifications())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationList -> {
Timber.d("Number of notifications is %d", notificationList.size());
setAdapter(notificationList);
}, throwable -> {
Timber.e(throwable, "Error occurred while loading notifications");
});
}
private void handleUrl(String url) {
if (url == null || url.equals("")) {
return;
}
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
private void setAdapter(List<Notification> notificationList) {
notificationAdapterFactory = new NotificationAdapterFactory(notification -> {
Timber.d("Notification clicked %s", notification.link);
handleUrl(notification.link);
});
RVRendererAdapter<Notification> adapter = notificationAdapterFactory.create(notificationList);
recyclerView.setAdapter(adapter);
}
public static void startYourself(Context context) {
Intent intent = new Intent(context, NotificationActivity.class);
context.startActivity(intent);
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.notification;
import android.support.annotation.NonNull;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
/**
* Created by root on 19.12.2017.
*/
class NotificationAdapterFactory {
private NotificationRenderer.NotificationClicked listener;
NotificationAdapterFactory(@NonNull NotificationRenderer.NotificationClicked listener) {
this.listener = listener;
}
public RVRendererAdapter<Notification> create(List<Notification> notifications) {
RendererBuilder<Notification> builder = new RendererBuilder<Notification>()
.bind(Notification.class, new NotificationRenderer(listener));
ListAdapteeCollection<Notification> collection = new ListAdapteeCollection<>(
notifications != null ? notifications : Collections.<Notification>emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.notification;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
/**
* Created by root on 19.12.2017.
*/
@Singleton
public class NotificationController {
private MediaWikiApi mediaWikiApi;
private SessionManager sessionManager;
@Inject
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
this.mediaWikiApi = mediaWikiApi;
this.sessionManager = sessionManager;
}
public List<Notification> getNotifications() throws IOException {
if (mediaWikiApi.validateLogin()) {
return mediaWikiApi.getNotifications();
} else {
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
if (authTokenValidated != null && authTokenValidated) {
return mediaWikiApi.getNotifications();
}
}
return new ArrayList<>();
}
}

View file

@ -0,0 +1,68 @@
package fr.free.nrw.commons.notification;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.pedrogomez.renderers.Renderer;
import java.util.Calendar;
import java.util.Date;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.DateUtils;
/**
* Created by root on 19.12.2017.
*/
public class NotificationRenderer extends Renderer<Notification> {
@BindView(R.id.title) TextView title;
@BindView(R.id.description) TextView description;
@BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon;
private NotificationClicked listener;
NotificationRenderer(NotificationClicked listener) {
this.listener = listener;
}
@Override
protected void setUpView(View view) { }
@Override
protected void hookListeners(View rootView) {
rootView.setOnClickListener(v -> listener.notificationClicked(getContent()));
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
ButterKnife.bind(this, inflatedView);
return inflatedView;
}
@Override
public void render() {
Notification notification = getContent();
title.setText(notification.notificationText);
time.setText(notification.date);
description.setText(notification.description);
switch (notification.notificationType) {
case THANK_YOU_EDIT:
icon.setImageResource(R.drawable.ic_edit_black_24dp);
break;
default:
icon.setImageResource(R.drawable.round_icon_unknown);
}
}
public interface NotificationClicked{
void notificationClicked(Notification notification);
}
}

View file

@ -0,0 +1,27 @@
package fr.free.nrw.commons.notification;
public enum NotificationType {
THANK_YOU_EDIT("thank-you-edit"),
EDIT_USER_TALK("edit-user-talk"),
MENTION("mention"),
WELCOME("welcome"),
UNKNOWN("unknown");
private String type;
NotificationType(String type) {
this.type = type;
}
public String getType() {
return type;
}
public static NotificationType handledValueOf(String name) {
for (NotificationType e : values()) {
if (e.getType().equals(name)) {
return e;
}
}
return UNKNOWN;
}
}

View file

@ -0,0 +1,116 @@
package fr.free.nrw.commons.notification;
import android.content.Context;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
public class NotificationUtils {
private static final String COMMONS_WIKI = "commonswiki";
public static boolean isCommonsNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
}
public static NotificationType getNotificationType(Node document) {
Element element = (Element) document;
String type = element.getAttribute("type");
return NotificationType.handledValueOf(type);
}
public static Notification getNotificationFromApiResult(Context context, Node document) {
NotificationType type = getNotificationType(document);
String notificationText = "";
String link = getNotificationLink(document);
String description = getNotificationDescription(document);
switch (type) {
case THANK_YOU_EDIT:
notificationText = context.getString(R.string.notifications_thank_you_edit);
break;
case EDIT_USER_TALK:
notificationText = getUserTalkMessage(context, document);
break;
case MENTION:
notificationText = getMentionMessage(context, document);
break;
case WELCOME:
notificationText = getWelcomeMessage(context, document);
break;
}
return new Notification(type, notificationText, getTimestamp(document), description, link);
}
public static String getMentionMessage(Context context, Node document) {
String format = context.getString(R.string.notifications_mention);
return String.format(format, getAgent(document), getNotificationDescription(document));
}
public static String getUserTalkMessage(Context context, Node document) {
String format = context.getString(R.string.notifications_talk_page_message);
return String.format(format, getAgent(document));
}
public static String getWelcomeMessage(Context context, Node document) {
String welcomeMessageFormat = context.getString(R.string.notifications_welcome);
return String.format(welcomeMessageFormat, getAgent(document));
}
private static String getAgent(Node document) {
Element agentElement = (Element) getNode(document, "agent");
if (agentElement != null) {
return agentElement.getAttribute("name");
}
return "";
}
private static String getTimestamp(Node document) {
Element timestampElement = (Element) getNode(document, "timestamp");
if (timestampElement != null) {
return timestampElement.getAttribute("date");
}
return "";
}
private static String getNotificationLink(Node document) {
String format = "%s%s";
Element titleElement = (Element) getNode(document, "title");
if (titleElement != null) {
String fullName = titleElement.getAttribute("full");
return String.format(format, BuildConfig.HOME_URL, fullName);
}
return "";
}
private static String getNotificationDescription(Node document) {
Element titleElement = (Element) getNode(document, "title");
if (titleElement != null) {
return titleElement.getAttribute("text");
}
return "";
}
@Nullable
public static Node getNode(Node node, String nodeName) {
NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node nodeItem = childNodes.item(i);
Element item = (Element) nodeItem;
if (item.getTagName().equals(nodeName)) {
return nodeItem;
}
}
return null;
}
}

View file

@ -27,6 +27,7 @@ import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import timber.log.Timber;
@ -143,6 +144,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
return true;
case R.id.action_notifications:
drawerLayout.closeDrawer(navigationView);
NotificationActivity.startYourself(this);
return true;
default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
return false;

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.utils;
import java.util.Calendar;
import java.util.Date;
public class DateUtils {
public static String getTimeAgo(Date currDate, Date itemDate) {
Calendar c = Calendar.getInstance();
c.setTime(currDate);
int yearNow = c.get(Calendar.YEAR);
int monthNow = c.get(Calendar.MONTH);
int dayNow = c.get(Calendar.DAY_OF_MONTH);
int hourNow = c.get(Calendar.HOUR_OF_DAY);
int minuteNow = c.get(Calendar.MINUTE);
c.setTime(itemDate);
int videoYear = c.get(Calendar.YEAR);
int videoMonth = c.get(Calendar.MONTH);
int videoDays = c.get(Calendar.DAY_OF_MONTH);
int videoHour = c.get(Calendar.HOUR_OF_DAY);
int videoMinute = c.get(Calendar.MINUTE);
if (yearNow != videoYear) {
return (String.valueOf(yearNow - videoYear) + "-" + "years");
} else if (monthNow != videoMonth) {
return (String.valueOf(monthNow - videoMonth) + "-" + "months");
} else if (dayNow != videoDays) {
return (String.valueOf(dayNow - videoDays) + "-" + "days");
} else if (hourNow != videoHour) {
return (String.valueOf(hourNow - videoHour) + "-" + "hours");
} else if (minuteNow != videoMinute) {
return (String.valueOf(minuteNow - videoMinute) + "-" + "minutes");
} else {
return "0-seconds";
}
}
}