1

At the moment, I'm learning Firebase with AngularFire and I have one problem where I'm stuck.

I'd like to query two list from Firebase one is a public list of messages (called messages in db) that every authenticated user can get and another one that only the creator can get (private messages users_private/{uid}/messages). The restriction for private messages is done with Firebase rules.

Getting both arrays with $firebaseArray() is working but how can I combine them so I can use them in one ng-repeat? The problem is that the synching with Firebase have to work ($add and $remove methods) but it doesn't. If I've found something like array.concat(userArray) the synch. is broken.

I could probably do two ng-repeats and duplicate my HTML markup, but I'd like to have it in one place to keep it DRY. Later filtering would be also difficult with two list e.g. filter by date. The model scheme of private and public messages are identical. The objects only differ in the visibility property private or public.

Users collection is only storing the name of the user and visible in the browser console for any authenticated user. That's required so I can get the user name with it's uid. So these data are not private that's why I've added users_private with other restrictions.

Why two arrays?

I'd like to keep the private data only visible for the creator. Right now, it is a chat app but later you can think of it as public posts and private posts that only the author can see.

First I've tried to do it with a property visibility in messages collection but then I've noticed that you can't restrict the access to it with firebase rules. See rules are not filters

The data structure looks like this (messages, users, users_private are top level documents): data structure

If you have another idea how to create private and public messages any ideas are welcome. But I think I'm on the right track.

My code is based on the angularFire seed project.

The view where I'd like to list the private messages too, looks like this: chat view

Here is my controller where I'd like to combine the arrays (some details to the code: users is a resolved value from ng-route, the user is also available at $rootScope.user and the currently logged in user and userSvc is used here to get the name of a user from its uid) and the markup snippet of `ng-repeat´:

(function(angular) {
  "use strict";

  var app = angular.module('myApp.chat', [
    'ngRoute', 'firebase.utils', 'firebase', 'angularSpinner'
  ]);

  app.controller('ChatCtrl', ['$scope',
    'messageList', 'privateMessageList', 'user', 'userSvc', '$q',
    function($scope, messageList, privateMessageList, user, userSvc, $q) {
      $scope.messages = [];
      $scope.user = user;

      $scope.getDisplayName = userSvc.getDisplayName;

      // messageList security set with firebase rules
      messageList.$loaded().then(function() {
        if (user) {
          $scope.messages = messageList; //comibinedlist;
          privateMessageList.$loaded().then(function(list) {
            $scope.showSpinner = false;
            $scope.privateMessages = privateMessageList;
            //$scope.displayMessages = [messageList,
            //  privateMessageList];
            //updateDisplayMessages(messageList, privateMessages);
            console.log('messageList', messageList, 'private list', privateMessageList, list);
          });
        }
      });

      $scope.addMessage = function(newMessage) {
        var dataList; // storage users/messages for private or /messages
        if (newMessage) {
          if (newMessage.visibility === 'private') {
            dataList = $scope.privateMessages;
            console.log('added', dataList);
            //angular.extend($scope.messages, $scope.privateMessages);
          } else {
            dataList = $scope.messages; //$scope.messages.$add(newMessage);
          }

          // add a timestamp
          angular.extend(newMessage, {
            timestamp: Firebase.ServerValue.TIMESTAMP
          });

          dataList.$add(newMessage);
        }
      };

      $scope.removeMessage = function(msg) {
        var dataList;
        if (msg.visibility === 'private') {
          dataList = $scope.privateMessages;
        } else {
          dataList = $scope.messages;
        }
        $scope.displayMessages.$remove(msg).then(function(ref) {
          console.log('removed', msg, ref.key() === msg.$id, ref.key(), msg.$id); // true
        });
        // $scope.messages.$remove(msg).then(function(ref) {
        //   console.log('removed', msg, ref.key() === msg.$id, ref.key(), msg.$id); // true
        // });
      };
    }
  ]);

  app.factory('privateMessageList', [
    'fbutil', '$firebaseArray', '$rootScope',
    function(fbutil, $firebaseArray, $rootScope) {
      var ref = fbutil.ref('users_private', $rootScope.user.uid, 'messages')
        .limitToLast(10);
      console.log('privMsg user', $rootScope.user.uid);
      return $firebaseArray(ref);
    }
  ]);

  app.factory('messageList', ['fbutil', '$firebaseArray',
    function(fbutil, $firebaseArray) {
      var ref = fbutil.ref('messages').limitToLast(10);
      return $firebaseArray(ref);
    }
  ]);

  app.config(['$routeProvider',
    function($routeProvider) {
      $routeProvider.whenAuthenticated('/chat', {
        templateUrl: 'chat/chat.html',
        controller: 'ChatCtrl',
        //authRequired: true,
        //   resolve: {
        //     // forces the page to wait for this promise to resolve before controller is loaded
        //     // the controller can then inject `user` as a dependency. This could also be done
        //     // in the controller, but this makes things cleaner (controller doesn't need to worry
        //     // about auth status or timing of accessing data or displaying elements)
        //     user: ['userSvc', function(userSvc) {
        //         return userSvc.getUser();
        //     }]
        //   }
      });
    }
  ]);

})(angular);
<div class="list-group" id="messages" ng-show="messages.length">
  <div class="list-group-item" ng-repeat="message in messages | reverse">
    <strong>{{getDisplayName(message.uid) || 'anonymous'}}: </strong>{{message.text}}
    <button href="#" class="btn btn-default btn-xs" ng-click="removeMessage(message)" ng-if="user.uid === message.uid"><i class="fa fa-remove"></i>
    </button>
    <span class="badge">{{message.visibility}}</span>
  </div>
</div>

Here are my current firebase rules (rules for user_private not checked yet):

{
  "rules": {
    // todo: add other user lists and only keep the username in users/ --> anyone with auth can get that list
    // add users_profile (email etc.), users_roles
    // user roles stored separately so we can keep it secure and only accessible (read & write) with uid
    "users": {
      ".read": "auth !== null",
      "$user_id": {
        //".read": "auth !== null", //  && ( auth.uid === $user_id )",
        ".write": "auth !== null && ( auth.uid === $user_id )"
      }
    },
    "users_private": {
      "$user_id": {
        ".read": "auth !== null", //&& ( auth.uid === $user_id )",
        ".write": "auth !== null && ( auth.uid === $user_id )"
      }
    },
    "messages": {
      ".read": "auth != null", //"(data.child('visibility').val() === 'public')",
      "$message": {
        //".read": true,
        /*".read": "(data.child('visibility').val() === 'public') || 
          //( auth.uid == data.child('uid').val() ) || 
          ( root.child('users').child(auth.uid).child('admin').val() === true )", // the world can view public and author can view their message or admins (just for debugging)*/
        ".write": "(data.child('uid').val() == auth.uid ) || ( auth.uid == newData.child('uid').val() ) ||
                (root.child('users').child(auth.uid).child('admin').val() === true)" // only owner or admin can write, user = 10, moderator = 20, admin = 999
      }
      /*"$uid": {
        ".write": "auth.uid == $uid" // only owner can write/edit to it if not new
      }*/
    }
  }
}
Community
  • 1
  • 1
AWolf
  • 8,770
  • 5
  • 33
  • 39
  • I've created a [fiddle](https://jsfiddle.net/awolf2904/sfwhyuts/) of my app. It's with-out name resolving to keep it simpler. That's why there are uids displayed. Just add a firebase app url and enter credentials for testing to see it in action. I've modified the `privateMessageList` a bit to make it work. It's with two `ng-repeat`s that's why private messages are displayed. – AWolf Dec 28 '15 at 22:51

2 Answers2

0

First, I don't know much about Angular / AngularFire but I think your problem is actually quite simple and unrelated to the framework: Firebase forces you to store the posts in two separate collections so now you have to merge these two collections after you fetch them.

Note that users_private/{uid}/messages is not an Array, but actually a map/object. This probably explains why Array.concat does not do the job. If messages is also map/object - like, you can use algorithm such as

How can I merge properties of two JavaScript objects dynamically?

to merge it. Another possibility is to convert the data to immutable map:

https://facebook.github.io/immutable-js/

apart from immutability (which is nice), you get much nicer API with immutable-js; for example merging maps is supported OOTB.

Community
  • 1
  • 1
Tomas Kulich
  • 14,388
  • 4
  • 30
  • 35
0

Tomas Kulich thanks for your answer that pointed me in the right direction and you're right it's more a conceptional or a javascript issue than a framework issue. I'll check immutable.js later, for now it is working with javascript and the two frameworks I have.

After your answer I've rethought my problem and found a solution that is working.

The merging wasn't that easy because as you mentioned the firebaseArrays are objects. So all array merging stuff is not working here.

That's why I've added the property privateId to my public messages firebaseArray if the message is private so I have a reference to the private entry and that simplified merging.

So every authenticated user could see the privateId but only the user with the correct uid can access the messages stored in users_private/{uid}/messages because they're restricted with firebase rules.

To display the private message I've created a filter for ng-repeat that's checking the messages and if there is a private message it replaces the message with the text (if available for the user otherwise it returns null). The filter also adds the publicId to the private message so toggeling between private/public is easier later.

Creating public messages is easy. It's just adding the message to the messages/ collection. Private message will first add the message to the users_private/{uid}/messages list and in the then callback it will add the privateId with ref.key() to the public messages.

My current firebase rules and source code can be found in this gist. I still need to debug the rules but for now they're a working as expected.

I've updated my fiddle too.

AWolf
  • 8,770
  • 5
  • 33
  • 39