Notification System in Duke-CSA App

OK, today is the day my JFLAP program ends, so I have a whole evening to write this big post about how I’m implementing notifications in Duke-CSA app.

First I’ll talk about the functionalities I achieved and then about how I implemented them.

Functionalities:

  1. notification initialization

Notifications can be triggered by two circumstances: 1. A user replies/answers questions by another user in QA, replies/comments Rendezvous by another user etc. 2. A new event is pushed by CSA to all users.

The former kind of notifications is created by client apps which call a function in the cloud code to push. The latter, however, is created by an event poster that I created using Parse JavaScript SDK and calls the same cloud function to push.

2. notification presentation

The following table shows how notifications are handled. Rows are the states of the app and columns are how users open the app.

Click on Notification Click on App icon
not running go to corresponding view show badges and red dots
background go to corresponding view go to corresponding view
foreground N/A show badges and red dots

Red colored entry is undesired behavior that I have not figured out how to fix.

3. notification indication

Whenever there is an unread reply/answer etc. The corresponding tab will have a badge showing how many unread notifications that view and its subviews have. Red dots are also shown on the TableViewCells. The badge number will decrement and the dot will disappear whenever the user clicks on unread posts.

 

Implementations:

  • push notifications

I’m using Parse Cloud Code to perform the pushing tasks to Apple Push Notifications (APN) and call this function from any client app. The cloud code looks like this:

Parse.Cloud.define("push", function (request, response) {
    var query = new Parse.Query(Parse.Installation);
    var targetUser;
    var toUser = request.params.toUser; // this is the id of target user
    if (request.user && request.user.id == toUser) {
        return;
    }
    if (toUser) {
        targetUser = new Parse.User();
        targetUser.id = toUser; 
        query.equalTo('user', targetUser);
    }

    Parse.Push.send({
        where: query,
        data: request.params.data
    }, {
        useMasterKey: true,
        success: function () {
            if (targetUser) {
                saveNotifDataForUser(targetUser, request, response);
            }
            else {
                saveNotifDataForAll(request, response);
            }
        },
        error: function (error) {
            response.error("Error! " + error.message);
        }
    });
});

Basically, whenever there is a “toUser” variable in the request sent to the cloud, I push the notification to the user using a query and save the notification to the database (more about this later). If there is nothing in “toUser”, it means the request wants to push to all users. The program will then save the notifications for all.

  • save notifications

Below’s what “saveNotifDataForUser” looks like. I’m gonna save the code for “saveNotifDataForAll” because it’s very similar.

function saveNotifDataForUser (targetUser, request, response) {
    var NotifData = Parse.Object.extend("NotifData");
    var qry = new Parse.Query(NotifData);
    var type = request.params.data.notifType;
    var instanceID = request.params.data.PFInstanceID;
    qry.equalTo('UserID', targetUser.id);
    qry.find().then( function (results) {
        var notifData;

        if (results.length == 0) {
            notifData = new NotifData();
            for (var i = 0; i < notifTypes.length; i++) {
                notifData.set(notifTypes[i], []);
            }
            notifData.set("UserID", targetUser.id);
        } else {
            notifData = results[0];
        }

        var notifOfType = notifData.get(type);
        if (!notifOfType.includes(instanceID)) {
            notifOfType.push(instanceID);
        }
        return notifData.save();
    }).then( function (notifData) {
        console.log("user notif data saved.");
        response.success("Successfully pushed notification to " + targetUser.id + " with type " + type + " and id " + instanceID);
    });
}

Here I’m creating a new instance of “NotifData” class and save relevant information in there. Let me explain a little bit about this class. I came up with the idea of storing notification data online when I was trying to find a way to show badges on tab bar items when users click on the app icon to open the app instead of the notification. If I store notifications to the online database and retrieve them every time app is not launched from a notification. I can correctly update the notification data.

Besides this online class, I’m also storing notifications locally in persistent storage because I don’t want to lose them when users close the app without reading new notifications. For this storage, I classified notifications in categories and saved the arrays of IDs of PFInstances. Variable List:

var events: [String] = []
var rendezvous: [String] = []
var questions: [String] = []
var answers: [String] = []
var ansQuestions: [String] = []
var newEvents: [String] = []

Whenever a new post/reply is read, I remove its ID from the array and save again.

  • retrieve notifications

I’m retrieving notifications in the method didFinishLaunchingWithOptions. If “launchOption” is not nil, then it means the app is launched by the user clicking on the notification. With this notification I am able to create the local storage notification class and go from there. However, when “launchOption” is nil, I have to retrieve from the database the notifications sent to the user when the app was not active. I wrote a cloud function for this:

Parse.Cloud.define("getNotifData", function (request, response) {
    var userID = request.params.userID;
    var NotifData = Parse.Object.extend("NotifData");
    var query = new Parse.Query(NotifData);
    query.equalTo("UserID", userID);
    query.find().then( function(results) {
        if (results.length == 0) {
            response.error("No notification data for this user found.");
            return;
        }
        var notifData = results[0];
        var result = [];
        for (var i = 0; i < notifTypes.length; i++) {
            var instances = notifData.get(notifTypes[i]);
            for (var j = 0; j < instances.length; j++) {
                var notification = {"notifType": notifTypes[i], "PFInstanceID": instances[j]};
                result.push(notification);
            }
        }
        response.success(result);
    });
});

So client apps will just call this function to retrieve the notification data for the current users. After each time of retrieving, local storage should take over so I’m calling another cloud function to wipe the notification data. The code is farely similar to “getNotifData”.

This function is VERY tricky to use. This function is called when:

  1. the app is in the foreground and receives a notification, called twice.
  2. the app is in the background and receives a notification, called once. Note that this function is called in background only when the “remote notification” background mode is enabled in the capabilities of the app.
  • Event Poster

This is a tool to push events to the app and also all users. A demo video is below:

Basically it’s a lot of reading docs and playing around with html forms.

 

Seems like only I understand what I wrote….

A little more notes about debugging/coding:

  1. I’m using Postman as a fantastic tool to send post requests to Parse server. I can also export the requests in a json file so my friend Jay can also test the app.
  2. I find that the Parse.Promise class is very useful for getting rid of block structures wrapping each other.
  3. Moving functions to cloud is nice. In this way even if things go wrong after the app ships I have a chance to fix them.
  4. Vim is the best editor.
  5. I’m very excited that back4app (Parse Server group) promised to release a command line tool for their servers. I won’t have to click on many buttons to deploy cloud code any more. They also said we would be able to run local Parse servers and debug locally.
  6. The Event Poster is not using any kind of authentication. Literally anyone can post to the database. I’m hoping no one would be boring enough to go there and post random things.

LeetCode 106. Construct Binary Tree from Inorder and Postorder Traversal

Problem:

Given inorder and postorder traversal of a tree, construct the binary tree.

Note:
You may assume that duplicates do not exist in the tree.

Solution:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    int[] inorder, postorder;
    int current;
    
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        if (inorder == null || postorder == null || inorder.length != postorder.length) return null;
        this.inorder = inorder; this.postorder = postorder;
        current = postorder.length - 1;
        return buildTree(0, inorder.length - 1);
    }
    
    private int findIndex(int post) {
        for (int i = 0; i < inorder.length; i++)
            if (inorder[i] == postorder[post]) return i;
        return -1;
    }
    
    private TreeNode buildTree(int start, int end) {
        if (start > end) return null;
        TreeNode root = new TreeNode(postorder[current]);
        int index = findIndex(current);
        current--;
        root.right = buildTree(index + 1, end);
        root.left = buildTree(start, index - 1);
        return root;
    }
}

LeetCode 102. Binary Tree Level Order Traversal

Problem:

Given a binary tree, return the level order traversal of its nodes’ values. (ie, from left to right, level by level).

For example:
Given binary tree [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

return its level order traversal as:

[
  [3],
  [9,20],
  [15,7]
]

Solution:

BFS

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        if (root == null) return result;
        Queue<TreeNode> q = new LinkedList<TreeNode>();
        q.offer(root);
        while (!q.isEmpty()) {
            int size = q.size();
            List<Integer> row = new ArrayList<Integer>();
            while (size > 0) {
                TreeNode current = q.poll();
                row.add(current.val);
                if (current.left != null) q.offer(current.left);
                if (current.right != null) q.offer(current.right);
                size--;
            }
            result.add(row);
        }
        return result;
    }
}

LeetCode 66. Plus One

Problem:

Given a non-negative number represented as an array of digits, plus one to the number.

The digits are stored such that the most significant digit is at the head of the list.

Solution:

only need a carry variable

public class Solution {
    public int[] plusOne(int[] digits) {
        int carry = (digits[digits.length - 1] + 1) / 10;
        digits[digits.length - 1] = (digits[digits.length - 1] + 1) % 10;
        for (int i = digits.length - 2; i >= 0; i--) {
            int temp = digits[i];
            digits[i] = (carry + digits[i]) % 10;      
            carry = (carry + temp) / 10;
        }
        if (carry == 0) return digits;
        int[] result = new int[digits.length + 1];
        System.arraycopy(digits, 0, result, 1, digits.length);
        result[0] = 1;
        return result;
    }
}

LeetCode 111. Minimum Depth of Binary Tree

Problem:

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

Solution:

DFS

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) return 0;
        return minDepth(root, 0);
    }
    
    private int minDepth(TreeNode root, int current) {
        if (root.left == null && root.right == null) return current + 1;
        else if (root.left == null) return minDepth(root.right, current + 1);
        else if (root.right == null) return minDepth(root.left, current + 1);
        else return Math.min(minDepth(root.left, current + 1), minDepth(root.right, current + 1));
    }
}

LeetCode 104. Maximum Depth of Binary Tree

Problem:

Given a binary tree, find its maximum depth.

The maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.

Solution:

DFS

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int maxDepth(TreeNode root) {
        return maxDepth(root, 0);
    }
    
    private int maxDepth(TreeNode root, int current) {
        if (root == null) return current;
        return Math.max(maxDepth(root.left, current + 1), maxDepth(root.right, current + 1));
    }
}