Last active
September 26, 2017 22:43
-
-
Save AWolf81/75a3034b52617843fa65 to your computer and use it in GitHub Desktop.
Firebase chat example with private messages (only visible for owner)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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