Skip to content

Instantly share code, notes, and snippets.

@rafaismyname
Last active July 13, 2018 21:43
Show Gist options
  • Save rafaismyname/2826258d9bf6df0235f1bf9f357f4a57 to your computer and use it in GitHub Desktop.
Save rafaismyname/2826258d9bf6df0235f1bf9f357f4a57 to your computer and use it in GitHub Desktop.
TalkieSpark - Yet another firebase chat module
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'));
});
});
{
"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)"
}
}
}
},
}
}
}
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