Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grouped notifications into one and show the notifications as a list #734

Merged
merged 2 commits into from
Jun 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ buildscript {
}
//apply plugin: 'io.fabric'
repositories {
mavenCentral()
maven { url 'https://maven.fabric.io/public' }
}

Expand All @@ -162,6 +163,7 @@ dependencies {
compile project(':react-native-vector-icons')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "me.leolin:ShortcutBadger:1.1.16@aar"
compile "com.facebook.react:react-native:+" // From node_modules
compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') {
transitive = true;
Expand Down
25 changes: 21 additions & 4 deletions android/app/src/main/java/com/zulipmobile/MainApplication.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.zulipmobile;

import android.app.Application;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;

import com.facebook.react.ReactApplication;
import com.wix.reactnativenotifications.RNNotificationsPackage;
Expand All @@ -16,23 +18,28 @@
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.smixx.fabric.FabricPackage;
import com.crashlytics.android.Crashlytics;
import io.fabric.sdk.android.Fabric;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.zmxv.RNSound.RNSoundPackage;
import com.zulipmobile.notifications.GCMPushNotifications;
import com.zulipmobile.notifications.PushNotificationsProp;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;

import com.learnium.RNDeviceInfo.RNDeviceInfo;
import static com.zulipmobile.notifications.GCMPushNotifications.ACTION_NOTIFICATIONS_DISMISS;
import static com.zulipmobile.notifications.NotificationHelper.addConversationToMap;
import static com.zulipmobile.notifications.NotificationHelper.clearConversations;

public class MainApplication extends Application implements ReactApplication, INotificationsApplication {
private LinkedHashMap<String, Pair<String, Integer>> conversations;

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
Expand Down Expand Up @@ -65,10 +72,20 @@ public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
//Fabric.with(this, new Crashlytics());
conversations = new LinkedHashMap<>();
}

@Override
public IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper) {
return new GCMPushNotifications(context, bundle, defaultFacade, defaultAppLaunchHelper, new JsIOHelper());
if (ACTION_NOTIFICATIONS_DISMISS.equals(bundle.getString(ACTION_NOTIFICATIONS_DISMISS))) {
clearConversations(conversations);
NotificationManager nMgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nMgr.cancelAll();
return null;
} else {
PushNotificationsProp prop = new PushNotificationsProp(bundle);
addConversationToMap(prop, conversations);
return new GCMPushNotifications(context, bundle, defaultFacade, defaultAppLaunchHelper, new JsIOHelper(), conversations);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,47 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.core.ProxyService;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.zulipmobile.R;

import java.io.IOException;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Locale;

import me.leolin.shortcutbadger.ShortcutBadger;

import static com.zulipmobile.notifications.NotificationHelper.buildNotificationContent;
import static com.zulipmobile.notifications.NotificationHelper.clearConversations;
import static com.zulipmobile.notifications.NotificationHelper.extractNames;
import static com.zulipmobile.notifications.NotificationHelper.extractTotalMessagesCount;

public class GCMPushNotifications extends PushNotification {

private static final int NOTIFICATION_ID = 1;
private static final int NOTIFICATION_ID = 435;
public static final String ACTION_NOTIFICATIONS_DISMISS = "ACTION_NOTIFICATIONS_DISMISS";
private LinkedHashMap<String, Pair<String, Integer>> conversations;

/**
* Same as {@link com.wix.reactnativenotifications.core.NotificationIntentAdapter#PUSH_NOTIFICATION_EXTRA_NAME}
*/
private static final String PUSH_NOTIFICATION_EXTRA_NAME = "pushNotification";

public GCMPushNotifications(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
public GCMPushNotifications(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper, LinkedHashMap<String, Pair<String, Integer>> conversations) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
this.conversations = conversations;
}

@Override
Expand All @@ -33,6 +55,19 @@ protected PushNotificationsProp getProps() {
return (PushNotificationsProp) mNotificationProps;
}


@Override
public void onOpened() {
super.onOpened();
clearConversations(conversations);
try {
ShortcutBadger.removeCount(mContext);
} catch (Exception e) {
Log.e("BADGE ERROR", e.toString());
}
}


@Override
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
// First, get a builder initialized with defaults from the core class.
Expand All @@ -46,33 +81,65 @@ protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
String stream = getProps().getStream();
String topic = getProps().getTopic();
String baseURL = getProps().getBaseURL();
int totalMessagesCount = extractTotalMessagesCount(conversations);

builder.setSmallIcon(R.drawable.zulip_notification);
builder.setContentTitle(title);
builder.setAutoCancel(true);
builder.setContentText(content);

if (type.equals("stream")) {
if (android.os.Build.VERSION.SDK_INT >= 16) {
String displayTopic = stream + " > "
+ topic;
builder.setSubText("Mention on " + displayTopic);
if (conversations.size() == 1) {
//Only one 1 notification therefore no using of big view styles
builder.setContentTitle(title);
if (type.equals("stream")) {
if (Build.VERSION.SDK_INT >= 16) {
String displayTopic = stream + " > "
+ topic;
builder.setSubText("Mention on " + displayTopic);
}
}
}
if (avatarURL != null && avatarURL.startsWith("http")) {
Bitmap avatar = fetchAvatar(NotificationHelper.sizedURL(mContext,
avatarURL, 64, baseURL));
if (avatar != null) {
builder.setLargeIcon(avatar);
if (avatarURL != null && avatarURL.startsWith("http")) {
Bitmap avatar = fetchAvatar(NotificationHelper.sizedURL(mContext,
avatarURL, 64, baseURL));
if (avatar != null) {
builder.setLargeIcon(avatar);
}
}
} else {
builder.setContentTitle(String.format(Locale.ENGLISH, "%d messages in %d conversations", totalMessagesCount, conversations.size()));
builder.setStyle(new Notification.BigTextStyle().bigText(buildNotificationContent(conversations)));
builder.setContentText("Messages from " + TextUtils.join(",", extractNames(conversations)));
}

try {
ShortcutBadger.applyCount(mContext, totalMessagesCount);
} catch (Exception e) {
Log.e("BADGE ERROR", e.toString());
}

if (time != null) {
long timStamp = Long.parseLong(getProps().getTime()) * 1000;
builder.setWhen(timStamp);
}

long[] vPattern = {0, 100, 200, 100};
builder.setVibrate(vPattern);


/**
* Ideally, actions are sent using dismissIntent.setAction(String),
* But here {@link com.wix.reactnativenotifications.core.NotificationIntentAdapter#extractPendingNotificationDataFromIntent(Intent)}
* it checks in the bundle hence, An empty bundle is sent and checked in
* {@link com.zulipmobile.MainApplication#getPushNotification} for this string and then dismissed
*
**/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Intent dismissIntent = new Intent(mContext, ProxyService.class);
Bundle bundle = new Bundle();
bundle.putString(ACTION_NOTIFICATIONS_DISMISS, ACTION_NOTIFICATIONS_DISMISS);
dismissIntent.putExtra(PUSH_NOTIFICATION_EXTRA_NAME, bundle);
PendingIntent piDismiss = PendingIntent.getService(mContext, 0, dismissIntent, 0);
Notification.Action action = new Notification.Action(android.R.drawable.ic_notification_clear_all, "Clear", piDismiss);
builder.addAction(action);
}
return builder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.util.Pair;
import android.util.TypedValue;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedHashMap;
import java.util.Map;

public class NotificationHelper {

Expand Down Expand Up @@ -49,4 +52,61 @@ public static String addHost(String url, String baseURL) {
}
return url;
}


public static String extractName(String key) {
return key.split(":")[0];
}

public static String buildNotificationContent(LinkedHashMap<String, Pair<String, Integer>> conversations) {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, Pair<String, Integer>> map : conversations.entrySet()) {
stringBuilder.append(extractName(map.getKey())).append(" (").append(map.getValue().second).append("): ").append(map.getValue().first).append("\n");
}
return stringBuilder.toString();
}

public static int extractTotalMessagesCount(LinkedHashMap<String, Pair<String, Integer>> conversations) {
int totalNumber = 0;
for (Map.Entry<String, Pair<String, Integer>> map : conversations.entrySet()) {
totalNumber += map.getValue().second;
}
return totalNumber;
}

/**
* Formats -
* private message - fullName:Email
* stream message - fullName:Email:stream
*/
public static String buildKeyString(PushNotificationsProp prop) {
if (prop.getRecipientType() == "stream")
return prop.getSenderFullName() + ":" + prop.getEmail();
else
return String.format("%s:%s:stream", prop.getSenderFullName(), prop.getEmail());
}

public static String[] extractNames(LinkedHashMap<String, Pair<String, Integer>> conversations) {
String[] names = new String[conversations.size()];
int index = 0;
for (Map.Entry<String, Pair<String, Integer>> map : conversations.entrySet()) {
names[index++] = map.getKey().split(":")[0];
}
return names;
}

public static void addConversationToMap(PushNotificationsProp prop, LinkedHashMap<String, Pair<String, Integer>> conversations) {
String key = buildKeyString(prop);
Pair<String, Integer> messages = conversations.get(key);
if (messages != null) {
conversations.put(key, new Pair<>(prop.getContent(), messages.second + 1));
} else {
conversations.put(key, new Pair<>(prop.getContent(), 1));
}
}


public static void clearConversations(LinkedHashMap<String, Pair<String, Integer>> conversations) {
conversations.clear();
}
}