Skip to content

Instantly share code, notes, and snippets.

@AWolf81
Last active September 26, 2017 22:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AWolf81/75a3034b52617843fa65 to your computer and use it in GitHub Desktop.
Save AWolf81/75a3034b52617843fa65 to your computer and use it in GitHub Desktop.
Firebase chat example with private messages (only visible for owner)
<!-- created for this SO
http://stackoverflow.com/questions/34491935/how-to-combine-two-firebasearrays-into-one
How to setup this fiddle?
1. Change FBURL constant to your app
2. Create a user for password provider in firebase dashboard
3. Enter credentials in $authWithPassword
-->
<div ng-app="myApp" ng-controller="ChatCtrl as chatCtrl">
<form>
visiblity (checked = private):
<input ng-model="private" type="checkbox" />
<input placeholder="Message..." ng-model="newMessage">
<button type="submit" ng-click="chatCtrl.addMessage({uid: user.uid,
text: newMessage,
visibility: private? 'private': 'public'});newMessage = null;">
send</button>
</form>
<!-- create directive for ng-if="user.uid == message.uid" => is-owner -->
<div class="list-group" id="messages" ng-show="chatCtrl.messages.length">
<div class="list-group-item" ng-if="message" ng-repeat="message in (chatCtrl.messages | reverse | replacePrivate: chatCtrl.privateMessages) track by $index">
<!-- <div class="list-group-item">-->
<!-- ng-if="checkPrivateUser(message)">-->
<!-- ng-if = false if user != private user -->
<!-- ng-init="message = isPrivate(message) ? getPrivate(message.privateId): message"> -->
<h4 class="list-group-item-heading"><strong>{{chatCtrl.getDisplayName(message.uid) || 'anonymous'}}: </strong>
<div class="form-group">
<form class="form-inline" ng-if="chatCtrl.editMode[message.$id]">
<input class="form-control" ng-model="message.text" ng-blur="chatCtrl.editMode[message]=false; message.collection.$save(message)"/>
<button class="btn btn-primary" ng-click="chatCtrl.editMode[message.$id]=false; message.collection.$save(message)">OK</button>
<button class="btn btn-default" ng-click="chatCtrl.cancelEdit(message)">cancel</button>
</form>
<span ng-if="!chatCtrl.editMode[message.$id]"
ng-click="chatCtrl.activateEdit(message)">
{{message.text}}</span>
</div>
</h4>
<span is-owner>
<button href="#" class="btn btn-default btn-xs"
ng-click="chatCtrl.removeMessage(message)"><i class="fa fa-remove"></i>
</button>
<button class="btn btn-default btn-xs"
ng-click="chatCtrl.toggleVisibility(message)">
make {{message.visibility == 'private' ? 'public': 'private'}}
</button>
</span>
<span class="list-group-item-text">created {{message.timestamp | date:"MMM dd, yyyy 'at' HH:mm" : 'UTC'}}</span>
<span class="badge">{{message.visibility}}</span>
<!-- </div> -->
</div>
</div>
</div>
// a working fiddle can be found here https://jsfiddle.net/awolf2904/sfwhyuts/
(function(angular) {
"use strict";
var app = angular.module('myApp', [
//'ngRoute',
'firebase.utils', 'firebase'
]);
app.constant('FBURL', 'https://<your-app>.firebaseio.com');
app.controller('ChatCtrl', ChatController);
function ChatController(Auth, $scope, messageList, privateMessageList, $q) {
var vm = this,
backupMessage = ''; // save message for undo on cancel click
Auth.$authWithPassword({
email: '<your-login-email-here>',
password: '<your-password>'
}, {
rememberMe: true
})
.then(function(user) {
$scope.user = user;
angular.extend(vm, {
messages: messageList,
privateMessages: privateMessageList.createList(user),
user: user,
editMode: {},
getDisplayName: function(user) {
return user.uid;
}, //userSvc.getDisplayName,
addMessage: addMessage,
removeMessage: removeMessage,
toggleVisibility: toggleVisibility,
activateEdit: activateEdit,
cancelEdit: cancelEdit,
})
activate();
function activate() {
$q.all([vm.messages.$loaded(), vm.privateMessages.$loaded()])
.then(function() {
//console.log(lists, 'messageList', messageList, 'private list', privateMessageList);
// update method, needed to update msg after toggleVisibility
function updateMsg(message, id) {
if (message.visibility === 'private') console.log('updating message.msg', vm.privateMessages.$getRecord(id), message, id);
return isPrivate(message) ?
vm.privateMessages.$getRecord(id) :
message;
}
// create a mapping around messages to handle private messages
/* // better use a filter and replace content in place
vm.viewMessages = vm.messages.map(function(message) {
var id = message.privateId,
factory = {
msg: updateMsg(message, id),
update: function() {
factory.msg = updateMsg(factory.msg, id)
},
msgPublicId: message.$id // is public message here
};
return factory;
});*/
vm.showSpinner = false;
}, function(err) {
console.log('failed to load messages', err);
});
}
});
// new idea store ref. of privateMsg into publicMsg.
// adding, removing and toggle is affected by this change!!
function isPrivate(message) {
return message.visibility === 'private';
}
/**
* Extend the message with text
* @param msg private or public message
* @param data of new message
*/
function extendMsg(msg, data) {
return angular.extend(msg, {
text: data.text,
timestamp: Firebase.ServerValue.TIMESTAMP
});
}
function getPrivate(id) {
console.log(id, vm.privateMessages, vm.privateMessages.$getRecord(id));
return vm.privateMessages.$getRecord(id);
}
function checkPrivateUser(msg) {
return isPrivate(msg) ? (msg.uid === vm.user.uid) : true;
}
function addMessage(newMessage) {
var publicMessage = {
uid: newMessage.uid,
visibility: newMessage.visibility
}; // preset with-out text because not checked if msg. is private yet
if (isPrivate(newMessage)) {
vm.privateMessages.$add(extendMsg(angular.copy(publicMessage), newMessage))
.then(function(ref) {
var id = ref.key();
console.log('stored privateMsg', id,
vm.privateMessages.$getRecord(id));
vm.messages.$add(angular.extend(publicMessage, {
'privateId': id
}));
});
} else {
vm.messages.$add(extendMsg(publicMessage, newMessage));
}
}
function removeMessage(message) {
// message = {msg: private or public,
// collection: privateMessages or messages.
// msgPublicId: id of public message}
// msg obj created in ng-repeat with ng-init
console.log('remove msg', message, vm.messages.map(function(obj) {
return obj.$id === message.$id ? obj : null
}));
// remove private (if req.)
if (isPrivate(message)) {
vm.privateMessages.$remove(message).then(function(ref) {
console.log('Removed', ref.key());
});
}
// remove public message
vm.messages.$remove(message).then(function(ref) {
console.log('removed', ref.key());
});
}
function toggleVisibility(message) {
// message = {msg: private or public,
// collection: privateMessages or messages.
// msgPublicId: id of public message}
// msg obj created in ng-repeat with ng-init
//var storedMsg = angular.copy(msg);
// if private we need to copy the text property to public and
// remove the private doc
console.log('toggle visibility', message);
if (isPrivate(message)) {
console.log('make public', message, vm.privateMessages.$indexFor(message.$id)); //, message, message.$id, vm.privateMessages.$indexFor(message.$id), vm.messages.map(function(obj){return obj.$id === message.$id? obj: null}));
var privateMsg = message,
publicId = privateMsg.publicId,
publicMsg = vm.messages.$getRecord(publicId);
//publicMsg = message; //vm.messages.$getRecord(message.$id); //vm.messages.$indexFor(message.$id);
console.log('pub & private', publicMsg, privateMsg);
// remove private msg
vm.privateMessages.$remove(privateMsg).then(function(ref) {
console.log('removed', ref.key(), privateMsg);
//message.update(); // update msg --> check if we can automate this
});
// update public message
console.log('extend public', publicMsg, publicId);
angular.extend(publicMsg, {
privateId: null,
visibility: 'public',
text: privateMsg.text,
timestamp: privateMsg.timestamp
});
console.log('saving', publicMsg, publicId);
vm.messages.$save(publicMsg);
} else {
console.log('make private', message, message.$id);
// going private
// we need to create an entry in private array and update
// the public message (remove text property)
message.visibility = 'private';
vm.privateMessages.$add(message).then(function(ref) {
angular.extend(message, {
text: null, // remove text
privateId: ref.key()
});
vm.messages.$save(message);
//console.log('saving...', publicIndex, vm.messages[publicIndex], message);
/*vm.messages.$save(publicIndex).then(function() {
// message.msg = $scope.privateMessages.$getRecord(ref.key());
//message.update(); // update msg --> check if we can automate
});*/
});
}
}
function activateEdit(msg) {
backupMessage = angular.copy(msg);
console.log('activate', msg.$id);
vm.editMode[msg.$id] = (true && ((msg.uid === vm.user.uid) ||
vm.user.role == 'admin')); // only if owner
}
function cancelEdit(msg) {
msg = angular.copy(backupMessage);
backupMessage = null;
vm.editMode[msg.$id] = false;
}
//$scope.log = function(msg){console.log('saved', msg.once('value', function(obj) {console.log(obj.val())}));};
}
/*ChatController.$inject = ['$scope',
'messageList', 'privateMessageList', '$q'
];*/
app.filter('replacePrivate', function() {
return function(messages, privateMessages) {
var id, privateMsg;
return messages.map(function(msg) {
id = msg.privateId;
if (privateMessages.$indexFor(id) !== -1) {
privateMsg = privateMessages.$getRecord(id);
// add public id for easier toggeling between private/pub
angular.extend(privateMsg, {
publicId: msg.$id
});
}
return angular.isDefined(id) ?
privateMsg :
msg;
});
};
});
app.service('privateMessageList', [
'fbutil', '$firebaseArray',
function(fbutil, $firebaseArray) {
this.createList = function(user) {
var ref = fbutil.ref('users_private', user.uid, 'messages')
.limitToLast(10);
console.log('privMsg user', 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();
// }]
// }
});
}
]);*/
// stuff for login etc. follows...
app.factory('Auth', ['$firebaseAuth', 'fbutil', function($firebaseAuth, fbutil) {
return $firebaseAuth(fbutil.ref());
}]);
app.filter('reverse', function() {
return function(items) {
if (!items) return;
return items.slice().reverse();
};
});
angular.module('firebase.utils', ['firebase']) //,'myApp.config'])
.factory('fbutil', ['$window', 'FBURL', '$q', function($window, FBURL, $q) {
"use strict";
var utils = {
// convert a node or Firebase style callback to a future
handler: function(fn, context) {
return utils.defer(function(def) {
fn.call(context, function(err, result) {
if (err !== null) {
def.reject(err);
} else {
def.resolve(result);
}
});
});
},
// abstract the process of creating a future/promise
defer: function(fn, context) {
var def = $q.defer();
fn.call(context, def);
return def.promise;
},
ref: firebaseRef
};
return utils;
function pathRef(args) {
for (var i = 0; i < args.length; i++) {
if (angular.isArray(args[i])) {
args[i] = pathRef(args[i]);
} else if (typeof args[i] !== 'string') {
throw new Error('Argument ' + i + ' to firebaseRef is not a string: ' + args[i]);
}
}
return args.join('/');
}
/**
* Example:
* <code>
* function(firebaseRef) {
* var ref = firebaseRef('path/to/data');
* }
* </code>
*
* @function
* @name firebaseRef
* @param {String|Array...} path relative path to the root folder in Firebase instance
* @return a Firebase instance
*/
function firebaseRef(path) {
var ref = new $window.Firebase(FBURL);
var args = Array.prototype.slice.call(arguments);
if (args.length) {
ref = ref.child(pathRef(args));
}
return ref;
}
}]);
})(angular);
{
"rules": {
".read": true,
// 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": {
//".read": "auth !== null",
".write": "auth.uid === data.child('uid').val()",
"$user_id": {
".read": "auth.uid === $user_id", //&& ( auth.uid === $user_id )",
".write": "auth.uid === $user_id"
}
},
"users_roles": {
".read": "auth!== null", // any auth user can read
".write": "root.child('users_roles').child(auth.uid).child('role').val() === 'admin'" //'8dbe8740-3c05-491f-9a51-07f09ee67aec'" // only admin can write --> how can we get this uid from firebase and not hardcoded
},
"syncedValue": {
".read": true,
".write": true
},
"messages": {
".read": "auth != null", //"(data.child('visibility').val() === 'public')",
".write": "auth != null", // --> check who can delete messages?
"$messageId": {
// write rules 1. auth user can add data / 2. owner can edit / 3. admin can edit too
".write": "( !data.exists() && auth != null ) || ( newData.child('uid').val() === auth.uid ) || ( $messageId === auth.uid) ||
(root.child('users_private').child(auth.uid).child('profile/role').val() === 'admin')" // only owner or admin can write, user = 10, moderator = 20, admin = 999
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment