Last active
July 13, 2018 21:43
-
-
Save rafaismyname/2826258d9bf6df0235f1bf9f357f4a57 to your computer and use it in GitHub Desktop.
TalkieSpark - Yet another firebase chat module
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
import TalkieSpark from './talkie-spark'; | |
const chat = new TalkieSpark('tenantA', { | |
apiKey: '***', | |
authDomain: ''***',', | |
databaseURL: ''***',', | |
projectId: ''***',', | |
storageBucket: ''***',', | |
messagingSenderId: ''***',', | |
}); | |
chat.on('message', (roomId, message) => console.log('message', roomId, message)); | |
chat.on('userUpdate', event => console.log('userUpdate', event)); | |
chat.on('joinedRoom', event => console.log('joinedRoom', event)); | |
chat.on('leftRoom', event => console.log('leftRoom', event)); | |
chat.init().then(() => { | |
chat.joinRoom('roomA') | |
.then((roomId) => { | |
chat.sendMessage(roomId, { text: 'first message' }).then(() => console.log('message sent')); | |
}); | |
}); |
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": false, | |
".write": false, | |
"$tenant": { | |
"rooms": { | |
".read": "(auth != null)", | |
"$roomId": { | |
".write": "(auth != null)", | |
} | |
}, | |
"messages": { | |
"$roomId": { | |
".read": "(auth != null)", | |
"$msgId": { | |
".write": "(auth != null)", | |
".validate": "(newData.hasChildren(['userId','message','timestamp']))" | |
} | |
} | |
}, | |
"roomUsers": { | |
"$roomId": { | |
".read": "(auth != null)", | |
"$userId": { | |
".write": "(auth != null)", | |
"$sessionId": { | |
".write": "(auth != null)", | |
} | |
} | |
} | |
}, | |
"users": { | |
"$userId": { | |
".write": "(auth != null) && (auth.uid === $userId)", | |
".read": "(auth != null) && (auth.uid === $userId)", | |
".validate": "($userId === newData.child('id').val())", | |
} | |
}, | |
"usersOnline": { | |
".read": "(auth != null)", | |
"$userId": { | |
"$sessionId": { | |
".write": "(auth != null) && (!data.exists() || !newData.exists() || data.child('id').val() === auth.uid)", | |
"id": { | |
".validate": "(newData.val() === auth.uid)" | |
} | |
} | |
} | |
}, | |
} | |
} | |
} |
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
import firebase from 'firebase/app'; | |
import 'firebase/database'; | |
import 'firebase/auth'; | |
function TalkieSpark(tenantId, firebaseConfig, options = {}) { | |
// Firebase initial config | |
this.firebaseConfig = { | |
apiKey: firebaseConfig.apiKey || null, | |
authDomain: firebaseConfig.authDomain || null, | |
databaseURL: firebaseConfig.databaseURL || null, | |
projectId: firebaseConfig.projectId || null, | |
storageBucket: firebaseConfig.storageBucket || null, | |
messagingSenderId: firebaseConfig.messagingSenderId || null, | |
}; | |
this.tenantId = tenantId; | |
this.databaseRef = null; | |
this.firebaseApp = null; | |
// Commonly-used Firebase references. | |
this.sessionRef = null; | |
this.userRef = null; | |
this.tenantRef = null; | |
this.messageRef = null; | |
this.roomRef = null; | |
this.usersOnlineRef = null; | |
this.usersRef = null; | |
this.roomUsersRef = null; | |
// User-specific instance variables. | |
this.user = null; | |
this.userId = null; | |
// A unique id generated for each session. | |
this.sessionId = null; | |
// A mapping of event IDs to an array of callbacks. | |
this.events = {}; | |
// A mapping of room IDs to a boolean indicating presence. | |
this.rooms = {}; | |
// A mapping of operations to re-queue on disconnect. | |
this.presenceBits = {}; | |
// Setup and establish default options. | |
// The number of historical messages to load per room. | |
this.options = Object.assign({ numMaxMessages: 50 }, options); | |
} | |
// TalkieSpark Internal Methods | |
// -------------- | |
TalkieSpark.prototype = { | |
connect() { | |
if (!firebase.apps.length) firebase.initializeApp(this.firebaseConfig); | |
this.databaseRef = firebase.database().ref(); | |
this.firebaseApp = this.databaseRef.database.app; | |
this.tenantRef = this.databaseRef.child(this.tenantId); | |
this.messageRef = this.tenantRef.child('messages'); | |
this.roomRef = this.tenantRef.child('rooms'); | |
this.usersOnlineRef = this.tenantRef.child('usersOnline'); | |
this.usersRef = this.tenantRef.child('users'); | |
this.roomUsersRef = this.tenantRef.child('roomUsers'); | |
}, | |
initUser() { | |
return firebase.auth().signInAnonymously().then(({ user }) => user); | |
}, | |
// Load the initial metadata for the user's account and set initial state. | |
loadUserMetadata() { | |
const transactionUpdate = (currentUser) => { | |
if (currentUser && currentUser.id) return {}; | |
return { id: this.userId }; | |
}; | |
return this.userRef | |
.transaction(transactionUpdate) | |
.then(({ snapshot }) => { | |
this.user = snapshot.val(); | |
return this.user; | |
}); | |
}, | |
// Initialize Firebase listeners and callbacks for the supported bindings. | |
setupDataEvents() { | |
// Monitor connection state so we can requeue disconnect operations if need be. | |
const connectedRef = this.databaseRef.root.child('.info/connected'); | |
connectedRef.on('value', (snapshot) => { | |
if (snapshot.val() !== true) return; | |
// We're connected (or reconnected)! Set up our presence state. | |
Object.keys(this.presenceBits).forEach((path) => { | |
const presence = this.presenceBits[path]; | |
const presenceRef = presence.ref; | |
presenceRef.onDisconnect().set(presence.offlineValue); | |
presenceRef.set(presence.onlineValue); | |
}); | |
}, this); | |
// Queue up a presence operation to remove the session when presence is lost | |
this.queuePresenceOperation(this.sessionRef, true, null); | |
// Register our user in the listing. | |
const onlineUserRef = this.usersOnlineRef.child(this.userId); | |
const userSessionRef = onlineUserRef.child(this.sessionId); | |
const userSessionData = { id: this.userId }; | |
this.queuePresenceOperation(userSessionRef, userSessionData, null); | |
// Listen for state changes for the given user. | |
this.userRef.on('value', this.onUserUpdate, this); | |
}, | |
// Append the new callback to our list of event handlers. | |
addEventCallback(eventId, callback) { | |
const currentEventCallbacks = this.events[eventId] || []; | |
const eventCallbacks = currentEventCallbacks.concat(callback); | |
this.events = Object.assign({}, this.events, { [eventId]: eventCallbacks }); | |
}, | |
// Retrieve the list of event handlers for a given event id. | |
getEventCallbacks(eventId) { | |
return this.events[eventId] || []; | |
}, | |
// Invoke each of the event handlers for a given event id with specified data. | |
invokeEventCallbacks(eventId, ...args) { | |
const callbacks = this.getEventCallbacks(eventId); | |
callbacks.forEach(callback => callback(...args)); | |
}, | |
// Keep track of on-disconnect events so they can be requeued if we disconnect the reconnect. | |
queuePresenceOperation(ref, onlineValue, offlineValue) { | |
ref.onDisconnect().set(offlineValue); | |
ref.set(onlineValue); | |
const refPath = ref.toString(); | |
const refValue = { ref, onlineValue, offlineValue }; | |
this.presenceBits = Object.assign({}, this.presenceBits, { [refPath]: refValue }); | |
}, | |
// Remove an on-disconnect event from firing upon future disconnect and reconnect. | |
removePresenceOperation(ref, value) { | |
const refPath = ref.toString(); | |
ref.onDisconnect().cancel(); | |
ref.set(value); | |
this.presenceBits = Object.assign({}, this.presenceBits, { [refPath]: undefined }); | |
}, | |
// Event to monitor current user state. | |
onUserUpdate(snapshot) { | |
this.user = snapshot.val(); | |
this.invokeEventCallbacks('userUpdate', this.user); | |
}, | |
// Event to monitor current auth + user state. | |
onAuthRequired() { | |
this.invokeEventCallbacks('authRequired'); | |
}, | |
// Event to monitor room entry. | |
onJoinRoom(room) { | |
this.invokeEventCallbacks('joinedRoom', room); | |
}, | |
// Event to monitor room exit. | |
onLeaveRoom(roomId) { | |
this.invokeEventCallbacks('leftRoom', roomId); | |
}, | |
// Event to monitor new messages in a room. | |
onMessage(roomId, snapshot) { | |
const message = Object.assign({}, snapshot.val(), { id: snapshot.key }); | |
this.invokeEventCallbacks('message', roomId, message); | |
}, | |
}; | |
// TalkieSpark External Methods | |
// -------------- | |
// Initialize TalkieSpark: connect to Firebase, init refs, auth anon user, and set user | |
TalkieSpark.prototype.init = function init() { | |
this.connect(); | |
return this.initUser().then(({ uid: userId }) => this.setUser(userId)); | |
}; | |
// Initialize the library and setup data listeners. | |
TalkieSpark.prototype.setUser = function setUser(userId) { | |
return new Promise((resolve, reject) => { | |
this.firebaseApp.auth().onAuthStateChanged((user) => { | |
if (!user) { | |
const error = new Error('TalkieSpark requires an authenticated Firebase reference.'); | |
return reject(error); | |
} | |
this.userId = userId; | |
this.userRef = this.usersRef.child(this.userId); | |
this.sessionRef = this.userRef.child('sessions').push(); | |
this.sessionId = this.sessionRef.key; | |
return this.loadUserMetadata().then((loadeduser) => { | |
this.setupDataEvents(); | |
return resolve(loadeduser); | |
}); | |
}); | |
}); | |
}; | |
// Resumes the previous session by automatically entering rooms. | |
TalkieSpark.prototype.resumeSession = function resumeSession() { | |
return this.userRef.child('rooms') | |
.once('value') | |
.then((snapshot) => { | |
const rooms = snapshot.val(); | |
return Object.keys(rooms).forEach(roomId => this.joinRoom(rooms[roomId].id)); | |
}); | |
}; | |
// Callback registration. Supports each of the following events: | |
TalkieSpark.prototype.on = function on(eventType, cb) { | |
this.addEventCallback(eventType, cb); | |
}; | |
// Create a new chat room. | |
TalkieSpark.prototype.createRoom = function createRoom(roomId) { | |
const roomProperties = { createdAt: firebase.database.ServerValue.TIMESTAMP }; | |
return this.roomRef.child(roomId).set(roomProperties).then(() => roomProperties); | |
}; | |
// Get a room snapshot by id | |
TalkieSpark.prototype.findOrCreateRoom = function findOrCreateRoom(roomId) { | |
return this.roomRef.child(roomId).once('value').then((snapshot) => { | |
const room = snapshot.val(); | |
if (!room) return this.createRoom(roomId); | |
return snapshot.val(); | |
}); | |
}; | |
// Enter a chat room. | |
TalkieSpark.prototype.joinRoom = function joinRoom(roomId) { | |
return this.findOrCreateRoom(roomId) | |
.then(() => { | |
// Skip if we're already in this room. | |
if (this.rooms[roomId]) return roomId; | |
this.rooms = Object.assign({}, this.rooms, { [roomId]: true }); | |
if (this.user) { | |
// Save entering this room to resume the session again later. | |
const refRoomValue = { id: roomId, active: true }; | |
this.userRef.child('rooms').child(roomId).set(refRoomValue); | |
// Set presence bit for the room and queue it for removal on disconnect. | |
const roomUserRef = this.roomUsersRef.child(roomId).child(this.userId); | |
const presenceRef = roomUserRef.child(this.sessionId); | |
const presenceValue = { id: this.userId }; | |
this.queuePresenceOperation(presenceRef, presenceValue, null); | |
} | |
// Invoke our callbacks before we start listening for new messages. | |
this.onJoinRoom({ id: roomId }); | |
// Setup message listeners | |
return this.roomRef.child(roomId).once('value').then(() => { | |
this.messageRef.child(roomId) | |
.limitToLast(this.options.numMaxMessages) | |
.on( | |
'child_added', | |
(snapshot) => { this.onMessage(roomId, snapshot); }, | |
() => { this.leaveRoom(roomId); }, | |
this, | |
); | |
return roomId; | |
}); | |
}); | |
}; | |
// Leave a chat room. | |
TalkieSpark.prototype.leaveRoom = function leaveRoom(roomId) { | |
const userRoomRef = this.roomUsersRef.child(roomId); | |
// Remove listener for new messages to this room. | |
this.messageRef.child(roomId).off(); | |
if (this.user) { | |
const presenceRef = userRoomRef.child(this.userId).child(this.sessionId); | |
// Remove presence bit for the room and cancel on-disconnect removal. | |
this.removePresenceOperation(presenceRef, null); | |
// Remove session bit for the room. | |
this.userRef.child('rooms').child(roomId).remove(); | |
} | |
this.rooms = Object.assign({}, this.rooms, { [roomId]: undefined }); | |
// Invoke event callbacks for the room-exit event. | |
this.onLeaveRoom(roomId); | |
}; | |
// Send a new message in room | |
TalkieSpark.prototype.sendMessage = function sendMessage(roomId, messageContent) { | |
const message = { | |
userId: this.userId, | |
timestamp: firebase.database.ServerValue.TIMESTAMP, | |
message: messageContent, | |
}; | |
if (!this.user) { | |
this.onAuthRequired(); | |
return Promise.reject(new Error('Not authenticated or user not set!')); | |
} | |
const newMessageRef = this.messageRef.child(roomId).push(); | |
return newMessageRef.setWithPriority(message, firebase.database.ServerValue.TIMESTAMP); | |
}; | |
export default TalkieSpark; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment