Building a Dating site with Neo4j – Part Eleven

Up to this point, our users can send and receive messages, but we don’t have a way to show them all of their conversations, only one conversation at a time and they have to guess who messaged them before they can see those. Not very useful, what we need is a directory of all the conversations our user is part of. Let’s go ahead and add this feature to tie things together.

In our Conversations class, we will add a new method “getConversations”:

 
@Path("/users/{username}/conversations")
public class Conversations {
    @GET
    public Response getConversations(@PathParam("username") final String username,
                                     @QueryParam("limit") @DefaultValue("25") final Integer limit,
                                     @QueryParam("since") final String since,
                                     @Context GraphDatabaseService db) throws IOException {
        ArrayList<Map<String, Object>> results = new ArrayList<>();
        ZonedDateTime latest = getLatestTime(since);

We start off finding our user in the graph, and then get the people they block and also who blocks them. We do this by not specifying a direction when we get the BLOCKS relationships. We add the “other” node to our blocked set.

 
            Node user = Users.findUser(username, db);

            HashSet<Node> blocked = new HashSet<>();
            for (Relationship r1 : user.getRelationships(RelationshipTypes.BLOCKS)) {
                blocked.add(r1.getOtherNode(user));
            }

Next we find all of the conversations the user is PART_OF and set the other node.

 
            for (Relationship r1 : user.getRelationships(Direction.OUTGOING, RelationshipTypes.PART_OF)) {
                Node conversation = r1.getEndNode();
                Node other = null;
                for (Relationship r2 : conversation.getRelationships(Direction.INCOMING, RelationshipTypes.PART_OF)) {
                    if (user.equals(r2.getStartNode())) {
                        continue;
                    }
                    other = r2.getStartNode();
                }

If we find this other node in our blocked set, then either we blocked them or they blocked us, regardless we don’t want to show their conversations any more. We are also checking to see if other is null in case the other node was deleted.

 
                if (blocked.contains(other) || other == null) {
                    continue;
                }

The conversation nodes are empty, so we can’t just return them, that would be useless. Instead what we want is to show who else was involved in this conversation, the last message in the conversation, when it was written, and who wrote it. We could put all the messages in an array, sort them and the the latest, but we can also just grab one at a time and keep the latest.

 
                Node message = null;
                ZonedDateTime last = ZonedDateTime.of(1979,3,4,8,30,0,0, ZoneId.systemDefault());
                for (Relationship r2 : conversation.getRelationships(Direction.INCOMING, RelationshipTypes.ADDED_TO)) {
                    if (message == null) {
                        message = r2.getStartNode();
                        last = (ZonedDateTime) message.getProperty(TIME);
                        continue;
                    }
                    if (last.isBefore((ZonedDateTime) r2.getStartNode().getProperty(TIME))) {
                        message = r2.getStartNode();
                        last = (ZonedDateTime) message.getProperty(TIME);
                    }
                }

Once we get the last message, we can add the status and author to our result set along with the other user information.

 
                if (last.isBefore(latest)) {
                    if (message != null) {
                        Map<String, Object> result = new HashMap<>();
                        result.put(USERNAME, other.getProperty(USERNAME));
                        result.put(NAME, other.getProperty(NAME));
                        result.put(HASH, other.getProperty(HASH));
                        result.put(TIME, last);
                        result.put(STATUS, message.getProperty(STATUS));
                        result.put(AUTHOR, message.getProperty(AUTHOR));
                        results.add(result);
                    }
                }

The last thing we want to do here is sort them by time, so conversations that have been updated recently are at the top of our list.

 
results.sort(timedComparator);

We can add this to our front end by first adding it to the API:

 
    @GET("users/{username}/conversations")
    Call<List<Conversation>> getConversations(@Path("username") String username);

Then adding it to our application. Only logged in users can see messages, so once we have our username we can query the API with it. This is going to return a list of Conversation objects which is a new model we need to add. The funny thing though is that in our database Conversation nodes will be propertyless. This is a dead give away that the Conversation node really represents a hyper-edge between the two users and their messages. That’s perfectly fine, if you want to get comfortable with hyper-edges in your model, take a look at this presentation by Peter Olson on how Marvel Comics represents their universe in a graph:

Yes, I know it’s 40 minutes long, but find the time and watch it. It’s really that good, and if you are struggling with your graph model it may be just what you need. Alright, let’s get back to our front end now by adding messages:

 
        get("/messages", req -> {
            CommonProfile profile = require(CommonProfile.class);
            String username = profile.getUsername();
            User authenticated = App.getUserProfile(username);

            Response<List<Conversation>> conversationResponse = App.api.getConversations(username).execute();
            if (conversationResponse.isSuccessful()) {
                List<Conversation> conversations = conversationResponse.body();
                return views.messages.template(authenticated, conversations);
            } else {
                throw new Err(Status.BAD_REQUEST);
            }
        });

Along with the normal properties we returned, we want to add a humanTime and a preview method to our class. In case somebody wrote a large message it could mess up our formatting, plus we want to entice the user to click on the conversation to see the rest of the messages in a timeline and respond to it.

 
@Data
public class Conversation {
    private String name;
    private String username;
    private String hash;
    private String author;
    private String status;
    private String time;

    public String humanTime() {
        return Humanize.naturalTime(Date.from(ZonedDateTime.parse(time).toInstant()));
    }

    public String preview() {
        return status.substring(0, Math.min(status.length(), 40));
    }

The last message I wrote to Helene said “Nice”. From my point of view it looks like this image with the “you:” in front of the message to signal I sent it.

From her perspective it shows me saying “Nice” without the “you:” obviously.

Once she responds then the conversation screen will update with her latest message instead of mine:

We get this feature from the UI by checking the author name against the username viewing the conversation.

 
                @if(conversation.getAuthor().equals(authenticated.getUsername())) {
                you: @conversation.preview()
                } else {
                @conversation.preview()
                }

Our dating site is almost ready to be tried out…If people were attracted to the things people said more than the way people look. Unfortunately that isn’t the case in our reality, so we need to give people the ability to add images to their posts. We will look at that next.

But before we go… take a look at this presentation from 2009 by Ben Scofield on the struggles he had modeling comic books in a relational database and why graph databases gave him new modeling capabilities he needed. Ben was ahead of his time.

Tagged , , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: