├── .github
└── ISSUE_TEMPLATE
│ └── config.yml
├── .gitignore
├── Gruntfile.js
├── LICENSE
├── README.md
├── assets
└── img
│ └── icons.png
├── bower.json
├── changelog.txt
├── examples
└── anonymous
│ └── index.html
├── package.json
├── rules.json
├── screenshot.png
├── src
├── js
│ ├── firechat-ui.js
│ ├── firechat.js
│ ├── libs
│ │ └── underscore-1.7.0.min.js
│ └── shims.js
└── less
│ └── styles.less
├── templates
├── layout-full.html
├── layout-popout.html
├── message-context-menu.html
├── message.html
├── prompt-alert.html
├── prompt-create-room.html
├── prompt-invitation.html
├── prompt-invite-private.html
├── prompt-invite-reply.html
├── prompt-user-mute.html
├── prompt.html
├── room-list-item.html
├── room-user-list-item.html
├── room-user-search-list-item.html
├── tab-content.html
├── tab-menu-item.html
└── user-search-list-item.html
└── website
├── LICENSE
├── README.md
├── _config.yml
├── _layouts
└── docs.html
├── css
├── pygments-borland.css
└── styles.css
├── docs
├── index.md
└── public
│ ├── fonts
│ ├── aller-bold.eot
│ ├── aller-bold.ttf
│ ├── aller-bold.woff
│ ├── aller-light.eot
│ ├── aller-light.ttf
│ ├── aller-light.woff
│ ├── novecento-bold.eot
│ ├── novecento-bold.ttf
│ └── novecento-bold.woff
│ └── stylesheets
│ └── normalize.css
├── firebase.json
├── images
├── customer-cbs.png
├── favicon.ico
├── fork-on-github-white.png
├── powered-by-firebase.png
├── sign-in-with-twitter.png
└── top-shadow.png
└── index.html
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 🛑 Repository Archived
4 | url: https://firebase.google.com/docs/samples
5 | about: This repository is archived. Visit our samples page for a list of up-to-date samples.
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OS-X
2 | .DS_Store
3 |
4 | # Node / NPM
5 | node_modules/
6 |
7 | # Bower
8 | bower_components/
9 |
10 | # Distribution files
11 | dist/
12 |
13 | # Jekyll
14 | _site/
15 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | "use strict";
4 |
5 | // Initializes the Grunt tasks with the following settings
6 | grunt.initConfig({
7 |
8 | // A list of files which will be syntax-checked by JSHint.
9 | jshint: {
10 | files: ['src/js/shims.js', 'src/js/firechat.js', 'src/js/firechat-ui.js'],
11 | options: {
12 | regexdash: false
13 | }
14 | },
15 |
16 | // Precompile templates and strip whitespace with 'processContent'.
17 | jst: {
18 | compile: {
19 | options: {
20 | path: 'templates',
21 | namespace: 'FirechatDefaultTemplates',
22 | prettify: true,
23 | processContent: function(src) {
24 | return src.replace(/(^\s+|\s+$)/gm, '');
25 | }
26 | },
27 | files: {
28 | 'compiled/templates.js': ['templates/*.html']
29 | }
30 | }
31 | },
32 |
33 | // Compile and minify LESS CSS for production.
34 | less: {
35 | development: {
36 | files: {
37 | "dist/firechat.css": "src/less/styles.less"
38 | }
39 | },
40 | production: {
41 | options: {
42 | yuicompress: true
43 | },
44 | files: {
45 | "dist/firechat.min.css": "src/less/styles.less"
46 | }
47 | }
48 | },
49 |
50 | // Concatenate files in a specific order.
51 | concat: {
52 | js: {
53 | src: [
54 | 'src/js/libs/underscore-1.7.0.min.js',
55 | 'compiled/templates.js',
56 | 'src/js/shims.js',
57 | 'src/js/firechat.js',
58 | 'src/js/firechat-ui.js'
59 | ],
60 | dest: 'dist/firechat.js'
61 | }
62 | },
63 |
64 | // Minify concatenated files.
65 | uglify: {
66 | 'dist/firechat.min.js': ['dist/firechat.js'],
67 | },
68 |
69 | // Clean up temporary files.
70 | clean: ['compiled/'],
71 |
72 | // Tasks to execute upon file change when using `grunt watch`.
73 | watch: {
74 | src: {
75 | files: ['src/**/*.*', 'templates/**/*.*'],
76 | tasks: ['default']
77 | }
78 | }
79 | });
80 |
81 | // Load specific plugins, which have been installed and specified in package.json.
82 | grunt.loadNpmTasks('grunt-contrib-clean');
83 | grunt.loadNpmTasks('grunt-contrib-concat');
84 | grunt.loadNpmTasks('grunt-contrib-jshint');
85 | grunt.loadNpmTasks('grunt-contrib-jst');
86 | grunt.loadNpmTasks('grunt-contrib-less');
87 | grunt.loadNpmTasks('grunt-contrib-uglify');
88 | grunt.loadNpmTasks('grunt-contrib-watch');
89 |
90 | // Default task operations if simply calling `grunt` without options.
91 | grunt.registerTask('default', ['jshint', 'jst', 'less', 'concat', 'uglify', 'clean']);
92 |
93 | };
94 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Firebase
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Firechat [](http://badge.fury.io/gh/firebase%2Ffirechat)
2 |
3 | Firechat is a simple, extensible chat widget powered by
4 | [Firebase](https://firebase.google.com/?utm_source=firechat). It is intended to serve as a concise,
5 | documented foundation for chat products built on Firebase. It works out of the box, and is easily
6 | extended.
7 |
8 | ## Status
9 |
10 | 
11 |
12 | This sample is no longer actively maintained and is left here for reference only.
13 |
14 | ## Live Demo
15 |
16 | Visit [firechat.firebaseapp.com](https://firechat.firebaseapp.com/) to see a live demo of Firechat.
17 |
18 | [](https://firechat.firebaseapp.com/)
19 |
20 | ## Setup
21 |
22 | Firechat uses the [Firebase Realtime Database](https://firebase.google.com/docs/database/?utm_source=firechat)
23 | as a backend, so it requires no server-side code. It can be added to any web app by including a few
24 | JavaScript files:
25 |
26 | ```HTML
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | ```
37 |
38 | giving your users a way to authenticate:
39 |
40 | ```HTML
41 |
57 |
58 |
59 | ```
60 |
61 | and initializing the chat:
62 |
63 | ```HTML
64 |
76 |
77 |
78 | ```
79 |
80 | For detailed integration instructions, see the [Firechat documentation](https://firechat.firebaseapp.com/docs/).
81 |
82 | ## Getting Started with Firebase
83 |
84 | Firechat requires Firebase in order to authenticate users and store data. You can
85 | [sign up here](https://console.firebase.google.com/?utm_source=firechat) for a free account.
86 |
87 | ## Getting Help
88 |
89 | If you have a question about Firechat, feel free to reach out through one of our
90 | [official support channels](https://firebase.google.com/support/?utm_source=firechat).
91 |
--------------------------------------------------------------------------------
/assets/img/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/assets/img/icons.png
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "firechat",
3 | "description": "Realtime open source chat client powered by Firebase",
4 | "version": "3.0.1",
5 | "authors": [
6 | "Firebase (https://firebase.google.com/)"
7 | ],
8 | "homepage": "https://firechat.firebaseapp.com/",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/firebase/firechat.git"
12 | },
13 | "license": "MIT",
14 | "keywords": [
15 | "chat",
16 | "talk",
17 | "firebase",
18 | "realtime",
19 | "websocket",
20 | "synchronization"
21 | ],
22 | "main": "dist/firechat.js",
23 | "ignore": [
24 | "**/.*",
25 | "src",
26 | "examples",
27 | "node_modules",
28 | "bower_components",
29 | "package.json",
30 | "Gruntfile.js",
31 | "changelog.txt"
32 | ],
33 | "dependencies": {
34 | "firebase": "3.x.x"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/changelog.txt
--------------------------------------------------------------------------------
/examples/anonymous/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
34 |
35 |
36 |
49 |
50 |
51 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "firechat",
3 | "description": "Realtime open source chat client powered by Firebase",
4 | "version": "3.0.1",
5 | "author": "Firebase (https://firebase.google.com/)",
6 | "homepage": "https://firechat.firebaseapp.com/",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/firebase/firechat.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/firebase/firechat/issues"
13 | },
14 | "license": "MIT",
15 | "keywords": [
16 | "chat",
17 | "talk",
18 | "firebase",
19 | "realtime",
20 | "websocket",
21 | "synchronization"
22 | ],
23 | "main": "dist/firechat.js",
24 | "files": [
25 | "dist/**",
26 | "LICENSE",
27 | "README.md",
28 | "package.json"
29 | ],
30 | "dependencies": {
31 | "firebase": "3.x.x"
32 | },
33 | "devDependencies": {
34 | "grunt": "~0.4.0",
35 | "grunt-cli": "^0.1.13",
36 | "grunt-contrib-clean": "^1.0.0",
37 | "grunt-contrib-concat": "^1.0.1",
38 | "grunt-contrib-jshint": "~0.1.0",
39 | "grunt-contrib-jst": "~0.5.0",
40 | "grunt-contrib-less": "~0.5.0",
41 | "grunt-contrib-uglify": "^2.0.0",
42 | "grunt-contrib-watch": "^1.0.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | // Firechat sample security rules
3 | "rules": {
4 | // By default, make all data private unless specified otherwise.
5 | ".read": false,
6 | ".write": false,
7 | "room-metadata": {
8 | ".read": true,
9 | "$roomId": {
10 | // Append-only by anyone, and admins can add official rooms, and edit or remove rooms as well.
11 | ".write": "(auth != null) && (!data.exists() || root.child('moderators').hasChild(auth.uid) || data.child('createdByUserId').val() === auth.uid)",
12 | ".validate": "newData.hasChildren(['name','type'])",
13 | "id": {
14 | ".validate": "(newData.val() === $roomId)"
15 | },
16 | "createdByUserId": {
17 | ".validate": "(auth.uid === newData.val())"
18 | },
19 | "numUsers": {
20 | ".validate": "(newData.isNumber())"
21 | },
22 | "type": {
23 | ".validate": "('public' === newData.val()) || 'private' === newData.val() || ('official' === newData.val() && (root.child('moderators').hasChild(auth.uid)))"
24 | },
25 | // A list of users that may read messages from this room.
26 | "authorizedUsers": {
27 | ".write": "(auth != null) && (!data.exists() || root.child('moderators').hasChild(auth.uid) || data.hasChild(auth.uid))"
28 | }
29 | }
30 | },
31 | "room-messages": {
32 | "$roomId": {
33 | // A list of messages by room, viewable by anyone for public rooms, or authorized users for private rooms.
34 | ".read": "(root.child('room-metadata').child($roomId).child('type').val() != 'private' || root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.uid))",
35 | "$msgId": {
36 | // Allow anyone to append to this list and allow admins to edit or remove.
37 | ".write": "(auth != null) && (data.val() === null || root.child('moderators').hasChild(auth.uid)) && (root.child('room-metadata').child($roomId).child('type').val() != 'private' || root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.uid)) && (!root.child('suspensions').hasChild(auth.uid) || root.child('suspensions').child(auth.uid).val() < now)",
38 | ".validate": "(newData.hasChildren(['userId','name','message','timestamp']))"
39 | }
40 | }
41 | },
42 | "room-users": {
43 | "$roomId": {
44 | ".read": "(root.child('room-metadata').child($roomId).child('type').val() != 'private' || root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.uid))",
45 | "$userId": {
46 | // A list of users by room, viewable by anyone for public rooms, or authorized users for private rooms.
47 | ".write": "(auth != null) && ($userId === auth.uid || root.child('moderators').hasChild(auth.uid))",
48 | "$sessionId": {
49 | ".validate": "(!newData.exists() || newData.hasChildren(['id','name']))"
50 | }
51 | }
52 | }
53 | },
54 | "users": {
55 | // A list of users and their associated metadata, which can be updated by the single user or a moderator.
56 | "$userId": {
57 | ".write": "(auth != null) && (auth.uid === $userId || (root.child('moderators').hasChild(auth.uid)))",
58 | ".read": "(auth != null) && (auth.uid === $userId || (root.child('moderators').hasChild(auth.uid)))",
59 | ".validate": "($userId === newData.child('id').val())",
60 | "invites": {
61 | // A list of chat invitations from other users, append-only by anyone.
62 | "$inviteId": {
63 | // Allow the user who created the invitation to read the status of the invitation.
64 | ".read": "(auth != null) && (auth.uid === data.child('fromUserId').val())",
65 | ".write": "(auth != null) && (!data.exists() || $userId === auth.uid || data.child('fromUserId').val() === auth.uid)",
66 | ".validate": "newData.hasChildren(['fromUserId','fromUserName','roomId']) && (newData.child('id').val() === $inviteId)"
67 | }
68 | },
69 | "notifications": {
70 | // A list of notifications, which can only be appended to by moderators.
71 | "$notificationId": {
72 | ".write": "(auth != null) && (data.val() === null) && (root.child('moderators').hasChild(auth.uid))",
73 | ".validate": "newData.hasChildren(['fromUserId','timestamp','notificationType'])",
74 | "fromUserId": {
75 | ".validate": "newData.val() === auth.uid"
76 | }
77 | }
78 | }
79 | }
80 | },
81 | "user-names-online": {
82 | // A mapping of active, online lowercase usernames to sessions and user ids.
83 | ".read": true,
84 | "$username": {
85 | "$sessionId": {
86 | ".write": "(auth != null) && (!data.exists() || !newData.exists() || data.child('id').val() === auth.uid)",
87 | "id": {
88 | ".validate": "(newData.val() === auth.uid)"
89 | },
90 | "name": {
91 | ".validate": "(newData.isString())"
92 | }
93 | }
94 | }
95 | },
96 | "moderators": {
97 | ".read": "(auth != null)"
98 | },
99 | "suspensions": {
100 | ".write": "(auth != null) && (root.child('moderators').hasChild(auth.uid))",
101 | ".read": "(auth != null) && (root.child('moderators').hasChild(auth.uid))"
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/screenshot.png
--------------------------------------------------------------------------------
/src/js/firechat-ui.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 |
4 | if (!$ || (parseInt($().jquery.replace(/\./g, ""), 10) < 170)) {
5 | throw new Error("jQuery 1.7 or later required!");
6 | }
7 |
8 | var root = this,
9 | previousFirechatUI = root.FirechatUI;
10 |
11 | root.FirechatUI = FirechatUI;
12 |
13 | if (!self.FirechatDefaultTemplates) {
14 | throw new Error("Unable to find chat templates!");
15 | }
16 |
17 | function FirechatUI(firebaseRef, el, options) {
18 | var self = this;
19 |
20 | if (!firebaseRef) {
21 | throw new Error('FirechatUI: Missing required argument `firebaseRef`');
22 | }
23 |
24 | if (!el) {
25 | throw new Error('FirechatUI: Missing required argument `el`');
26 | }
27 |
28 | options = options || {};
29 | this._options = options;
30 |
31 | this._el = el;
32 | this._user = null;
33 | this._chat = new Firechat(firebaseRef, options);
34 |
35 | // A list of rooms to enter once we've made room for them (once we've hit the max room limit).
36 | this._roomQueue = [];
37 |
38 | // Define some constants regarding maximum lengths, client-enforced.
39 | this.maxLengthUsername = 15;
40 | this.maxLengthUsernameDisplay = 15;
41 | this.maxLengthRoomName = 24;
42 | this.maxLengthMessage = 120;
43 | this.maxUserSearchResults = 100;
44 |
45 | // Define some useful regexes.
46 | this.urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
47 | this.pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
48 |
49 | this._renderLayout();
50 |
51 | // Grab shortcuts to commonly used jQuery elements.
52 | this.$wrapper = $('#firechat');
53 | this.$roomList = $('#firechat-room-list');
54 | this.$tabList = $('#firechat-tab-list');
55 | this.$tabContent = $('#firechat-tab-content');
56 | this.$messages = {};
57 |
58 | // Rate limit messages from a given user with some defaults.
59 | this.$rateLimit = {
60 | limitCount: 10, // max number of events
61 | limitInterval: 10000, // max interval for above count in milliseconds
62 | limitWaitTime: 30000, // wait time if a user hits the wait limit
63 | history: {}
64 | };
65 |
66 | // Setup UI bindings for chat controls.
67 | this._bindUIEvents();
68 |
69 | // Setup bindings to internal methods
70 | this._bindDataEvents();
71 | }
72 |
73 | // Run FirechatUI in *noConflict* mode, returning the `FirechatUI` variable to
74 | // its previous owner, and returning a reference to the FirechatUI object.
75 | FirechatUI.noConflict = function noConflict() {
76 | root.FirechatUI = previousFirechatUI;
77 | return FirechatUI;
78 | };
79 |
80 | FirechatUI.prototype = {
81 |
82 | _bindUIEvents: function() {
83 | // Chat-specific custom interactions and functionality.
84 | this._bindForHeightChange();
85 | this._bindForTabControls();
86 | this._bindForRoomList();
87 | this._bindForUserRoomList();
88 | this._bindForUserSearch();
89 | this._bindForUserMuting();
90 | this._bindForChatInvites();
91 | this._bindForRoomListing();
92 |
93 | // Generic, non-chat-specific interactive elements.
94 | this._setupTabs();
95 | this._setupDropdowns();
96 | this._bindTextInputFieldLimits();
97 | },
98 |
99 | _bindDataEvents: function() {
100 | this._chat.on('user-update', this._onUpdateUser.bind(this));
101 |
102 | // Bind events for new messages, enter / leaving rooms, and user metadata.
103 | this._chat.on('room-enter', this._onEnterRoom.bind(this));
104 | this._chat.on('room-exit', this._onLeaveRoom.bind(this));
105 | this._chat.on('message-add', this._onNewMessage.bind(this));
106 | this._chat.on('message-remove', this._onRemoveMessage.bind(this));
107 |
108 | // Bind events related to chat invitations.
109 | this._chat.on('room-invite', this._onChatInvite.bind(this));
110 | this._chat.on('room-invite-response', this._onChatInviteResponse.bind(this));
111 |
112 | // Binds events related to admin or moderator notifications.
113 | this._chat.on('notification', this._onNotification.bind(this));
114 | },
115 |
116 | _renderLayout: function() {
117 | var template = FirechatDefaultTemplates["templates/layout-full.html"];
118 | $(this._el).html(template({
119 | maxLengthUsername: this.maxLengthUsername
120 | }));
121 | },
122 |
123 | _onUpdateUser: function(user) {
124 | // Update our current user state and render latest user name.
125 | this._user = user;
126 |
127 | // Update our interface to reflect which users are muted or not.
128 | var mutedUsers = this._user.muted || {};
129 | $('[data-event="firechat-user-mute-toggle"]').each(function(i, el) {
130 | var userId = $(this).closest('[data-user-id]').data('user-id');
131 | $(this).toggleClass('red', !!mutedUsers[userId]);
132 | });
133 |
134 | // Ensure that all messages from muted users are removed.
135 | for (var userId in mutedUsers) {
136 | $('.message[data-user-id="' + userId + '"]').fadeOut();
137 | }
138 | },
139 |
140 | _onEnterRoom: function(room) {
141 | this.attachTab(room.id, room.name);
142 | },
143 | _onLeaveRoom: function(roomId) {
144 | this.removeTab(roomId);
145 |
146 | // Auto-enter rooms in the queue
147 | if ((this._roomQueue.length > 0)) {
148 | this._chat.enterRoom(this._roomQueue.shift(roomId));
149 | }
150 | },
151 | _onNewMessage: function(roomId, message) {
152 | var userId = message.userId;
153 | if (!this._user || !this._user.muted || !this._user.muted[userId]) {
154 | this.showMessage(roomId, message);
155 | }
156 | },
157 | _onRemoveMessage: function(roomId, messageId) {
158 | this.removeMessage(roomId, messageId);
159 | },
160 |
161 | // Events related to chat invitations.
162 | _onChatInvite: function(invitation) {
163 | var self = this;
164 | var template = FirechatDefaultTemplates["templates/prompt-invitation.html"];
165 | var $prompt = this.prompt('Invite', template(invitation));
166 | $prompt.find('a.close').click(function() {
167 | $prompt.remove();
168 | self._chat.declineInvite(invitation.id);
169 | return false;
170 | });
171 |
172 | $prompt.find('[data-toggle=accept]').click(function() {
173 | $prompt.remove();
174 | self._chat.acceptInvite(invitation.id);
175 | return false;
176 | });
177 |
178 | $prompt.find('[data-toggle=decline]').click(function() {
179 | $prompt.remove();
180 | self._chat.declineInvite(invitation.id);
181 | return false;
182 | });
183 | },
184 | _onChatInviteResponse: function(invitation) {
185 | if (!invitation.status) return;
186 |
187 | var self = this,
188 | template = FirechatDefaultTemplates["templates/prompt-invite-reply.html"],
189 | $prompt;
190 |
191 | if (invitation.status && invitation.status === 'accepted') {
192 | $prompt = this.prompt('Accepted', template(invitation));
193 | this._chat.getRoom(invitation.roomId, function(room) {
194 | self.attachTab(invitation.roomId, room.name);
195 | });
196 | } else {
197 | $prompt = this.prompt('Declined', template(invitation));
198 | }
199 |
200 | $prompt.find('a.close').click(function() {
201 | $prompt.remove();
202 | return false;
203 | });
204 | },
205 |
206 | // Events related to admin or moderator notifications.
207 | _onNotification: function(notification) {
208 | if (notification.notificationType === 'warning') {
209 | this.renderAlertPrompt('Warning', 'You are being warned for inappropriate messaging. Further violation may result in temporary or permanent ban of service.');
210 | } else if (notification.notificationType === 'suspension') {
211 | var suspendedUntil = notification.data.suspendedUntil,
212 | secondsLeft = Math.round((suspendedUntil - new Date().getTime()) / 1000),
213 | timeLeft = '';
214 |
215 | if (secondsLeft > 0) {
216 | if (secondsLeft > 2*3600) {
217 | var hours = Math.floor(secondsLeft / 3600);
218 | timeLeft = hours + ' hours, ';
219 | secondsLeft -= 3600*hours;
220 | }
221 | timeLeft += Math.floor(secondsLeft / 60) + ' minutes';
222 | this.renderAlertPrompt('Suspended', 'A moderator has suspended you for violating site rules. You cannot send messages for another ' + timeLeft + '.');
223 | }
224 | }
225 | }
226 | };
227 |
228 | /**
229 | * Initialize an authenticated session with a user id and name.
230 | * This method assumes that the underlying Firebase reference has
231 | * already been authenticated.
232 | */
233 | FirechatUI.prototype.setUser = function(userId, userName) {
234 | var self = this;
235 |
236 | // Initialize data events
237 | self._chat.setUser(userId, userName, function(user) {
238 | self._user = user;
239 |
240 | if (self._chat.userIsModerator()) {
241 | self._bindSuperuserUIEvents();
242 | }
243 |
244 | self._chat.resumeSession();
245 | });
246 | };
247 |
248 | /**
249 | * Exposes internal chat bindings via this external interface.
250 | */
251 | FirechatUI.prototype.on = function(eventType, cb) {
252 | var self = this;
253 |
254 | this._chat.on(eventType, cb);
255 | };
256 |
257 | /**
258 | * Binds a custom context menu to messages for superusers to warn or ban
259 | * users for violating terms of service.
260 | */
261 | FirechatUI.prototype._bindSuperuserUIEvents = function() {
262 | var self = this,
263 | parseMessageVars = function(event) {
264 | var $this = $(this),
265 | messageId = $this.closest('[data-message-id]').data('message-id'),
266 | userId = $('[data-message-id="' + messageId + '"]').closest('[data-user-id]').data('user-id'),
267 | roomId = $('[data-message-id="' + messageId + '"]').closest('[data-room-id]').data('room-id');
268 |
269 | return { messageId: messageId, userId: userId, roomId: roomId };
270 | },
271 | clearMessageContextMenus = function() {
272 | // Remove any context menus currently showing.
273 | $('[data-toggle="firechat-contextmenu"]').each(function() {
274 | $(this).remove();
275 | });
276 |
277 | // Remove any messages currently highlighted.
278 | $('#firechat .message.highlighted').each(function() {
279 | $(this).removeClass('highlighted');
280 | });
281 | },
282 | showMessageContextMenu = function(event) {
283 | var $this = $(this),
284 | $message = $this.closest('[data-message-id]'),
285 | template = FirechatDefaultTemplates["templates/message-context-menu.html"],
286 | messageVars = parseMessageVars.call(this, event),
287 | $template;
288 |
289 | event.preventDefault();
290 |
291 | // Clear existing menus.
292 | clearMessageContextMenus();
293 |
294 | // Highlight the relevant message.
295 | $this.addClass('highlighted');
296 |
297 | self._chat.getRoom(messageVars.roomId, function(room) {
298 | // Show the context menu.
299 | $template = $(template({
300 | id: $message.data('message-id'),
301 | allowKick: false
302 | }));
303 | $template.css({
304 | left: event.clientX,
305 | top: event.clientY
306 | }).appendTo(self.$wrapper);
307 | });
308 | };
309 |
310 | // Handle dismissal of message context menus (any non-right-click click event).
311 | $(document).bind('click', { self: this }, function(event) {
312 | if (!event.button || event.button != 2) {
313 | clearMessageContextMenus();
314 | }
315 | });
316 |
317 | // Handle display of message context menus (via right-click on a message).
318 | $(document).delegate('[data-class="firechat-message"]', 'contextmenu', showMessageContextMenu);
319 |
320 | // Handle click of the 'Warn User' contextmenu item.
321 | $(document).delegate('[data-event="firechat-user-warn"]', 'click', function(event) {
322 | var messageVars = parseMessageVars.call(this, event);
323 | self._chat.warnUser(messageVars.userId);
324 | });
325 |
326 | // Handle click of the 'Suspend User (1 Hour)' contextmenu item.
327 | $(document).delegate('[data-event="firechat-user-suspend-hour"]', 'click', function(event) {
328 | var messageVars = parseMessageVars.call(this, event);
329 | self._chat.suspendUser(messageVars.userId, /* 1 Hour = 3600s */ 60*60);
330 | });
331 |
332 | // Handle click of the 'Suspend User (1 Day)' contextmenu item.
333 | $(document).delegate('[data-event="firechat-user-suspend-day"]', 'click', function(event) {
334 | var messageVars = parseMessageVars.call(this, event);
335 | self._chat.suspendUser(messageVars.userId, /* 1 Day = 86400s */ 24*60*60);
336 | });
337 |
338 | // Handle click of the 'Delete Message' contextmenu item.
339 | $(document).delegate('[data-event="firechat-message-delete"]', 'click', function(event) {
340 | var messageVars = parseMessageVars.call(this, event);
341 | self._chat.deleteMessage(messageVars.roomId, messageVars.messageId);
342 | });
343 | };
344 |
345 | /**
346 | * Binds to height changes in the surrounding div.
347 | */
348 | FirechatUI.prototype._bindForHeightChange = function() {
349 | var self = this,
350 | $el = $(this._el),
351 | lastHeight = null;
352 |
353 | setInterval(function() {
354 | var height = $el.height();
355 | if (height != lastHeight) {
356 | lastHeight = height;
357 | $('.chat').each(function(i, el) {
358 |
359 | });
360 | }
361 | }, 500);
362 | };
363 |
364 | /**
365 | * Binds custom inner-tab events.
366 | */
367 | FirechatUI.prototype._bindForTabControls = function() {
368 | var self = this;
369 |
370 | // Handle click of tab close button.
371 | $(document).delegate('[data-event="firechat-close-tab"]', 'click', function(event) {
372 | var roomId = $(this).closest('[data-room-id]').data('room-id');
373 | self._chat.leaveRoom(roomId);
374 | return false;
375 | });
376 | };
377 |
378 | /**
379 | * Binds room list dropdown to populate room list on-demand.
380 | */
381 | FirechatUI.prototype._bindForRoomList = function() {
382 | var self = this;
383 |
384 | $('#firechat-btn-rooms').bind('click', function() {
385 | if ($(this).parent().hasClass('open')) {
386 | return;
387 | }
388 |
389 | var $this = $(this),
390 | template = FirechatDefaultTemplates["templates/room-list-item.html"],
391 | selectRoomListItem = function() {
392 | var parent = $(this).parent(),
393 | roomId = parent.data('room-id'),
394 | roomName = parent.data('room-name');
395 |
396 | if (self.$messages[roomId]) {
397 | self.focusTab(roomId);
398 | } else {
399 | self._chat.enterRoom(roomId, roomName);
400 | }
401 | return false;
402 | };
403 |
404 | self._chat.getRoomList(function(rooms) {
405 | self.$roomList.empty();
406 | for (var roomId in rooms) {
407 | var room = rooms[roomId];
408 | if (room.type != "public") continue;
409 | room.isRoomOpen = !!self.$messages[room.id];
410 | var $roomItem = $(template(room));
411 | $roomItem.children('a').bind('click', selectRoomListItem);
412 | self.$roomList.append($roomItem.toggle(true));
413 | }
414 | });
415 | });
416 | };
417 |
418 | /**
419 | * Binds user list dropdown per room to populate user list on-demand.
420 | */
421 | FirechatUI.prototype._bindForUserRoomList = function() {
422 | var self = this;
423 |
424 | // Upon click of the dropdown, autofocus the input field and trigger list population.
425 | $(document).delegate('[data-event="firechat-user-room-list-btn"]', 'click', function(event) {
426 | event.stopPropagation();
427 |
428 | var $this = $(this),
429 | roomId = $this.closest('[data-room-id]').data('room-id'),
430 | template = FirechatDefaultTemplates["templates/room-user-list-item.html"],
431 | targetId = $this.data('target'),
432 | $target = $('#' + targetId);
433 |
434 | $target.empty();
435 | self._chat.getUsersByRoom(roomId, function(users) {
436 | for (var username in users) {
437 | user = users[username];
438 | user.disableActions = (!self._user || user.id === self._user.id);
439 | user.nameTrimmed = self.trimWithEllipsis(user.name, self.maxLengthUsernameDisplay);
440 | user.isMuted = (self._user && self._user.muted && self._user.muted[user.id]);
441 | $target.append($(template(user)));
442 | }
443 | self.sortListLexicographically('#' + targetId);
444 | });
445 | });
446 | };
447 |
448 | /**
449 | * Binds user search buttons, dropdowns, and input fields for searching all
450 | * active users currently in chat.
451 | */
452 | FirechatUI.prototype._bindForUserSearch = function() {
453 | var self = this,
454 | handleUserSearchSubmit = function(event) {
455 | var $this = $(this),
456 | targetId = $this.data('target'),
457 | controlsId = $this.data('controls'),
458 | templateId = $this.data('template'),
459 | prefix = $this.val() || $this.data('prefix') || '',
460 | startAt = $this.data('startAt') || null,
461 | endAt = $this.data('endAt') || null;
462 |
463 | event.preventDefault();
464 |
465 | userSearch(targetId, templateId, controlsId, prefix, startAt, endAt);
466 | },
467 | userSearch = function(targetId, templateId, controlsId, prefix, startAt, endAt) {
468 | var $target = $('#' + targetId),
469 | $controls = $('#' + controlsId),
470 | template = FirechatDefaultTemplates[templateId];
471 |
472 | // Query results, filtered by prefix, using the defined startAt and endAt markets.
473 | self._chat.getUsersByPrefix(prefix, startAt, endAt, self.maxUserSearchResults, function(users) {
474 | var numResults = 0,
475 | $prevBtn, $nextBtn, username, firstResult, lastResult;
476 |
477 | $target.empty();
478 |
479 | for (username in users) {
480 | var user = users[username];
481 |
482 | // Disable buttons for .
483 | user.disableActions = (!self._user || user.id === self._user.id);
484 |
485 | numResults += 1;
486 |
487 | $target.append(template(user));
488 |
489 | // If we've hit our result limit, the additional value signifies we should paginate.
490 | if (numResults === 1) {
491 | firstResult = user.name.toLowerCase();
492 | } else if (numResults >= self.maxUserSearchResults) {
493 | lastResult = user.name.toLowerCase();
494 | break;
495 | }
496 | }
497 |
498 | if ($controls) {
499 | $prevBtn = $controls.find('[data-toggle="firechat-pagination-prev"]');
500 | $nextBtn = $controls.find('[data-toggle="firechat-pagination-next"]');
501 |
502 | // Sort out configuration for the 'next' button
503 | if (lastResult) {
504 | $nextBtn
505 | .data('event', 'firechat-user-search')
506 | .data('startAt', lastResult)
507 | .data('prefix', prefix)
508 | .removeClass('disabled').removeAttr('disabled');
509 | } else {
510 | $nextBtn
511 | .data('event', null)
512 | .data('startAt', null)
513 | .data('prefix', null)
514 | .addClass('disabled').attr('disabled', 'disabled');
515 | }
516 | }
517 | });
518 | };
519 |
520 | $(document).delegate('[data-event="firechat-user-search"]', 'keyup', handleUserSearchSubmit);
521 | $(document).delegate('[data-event="firechat-user-search"]', 'click', handleUserSearchSubmit);
522 |
523 | // Upon click of the dropdown, autofocus the input field and trigger list population.
524 | $(document).delegate('[data-event="firechat-user-search-btn"]', 'click', function(event) {
525 | event.stopPropagation();
526 | var $input = $(this).next('div.firechat-dropdown-menu').find('input');
527 | $input.focus();
528 | $input.trigger(jQuery.Event('keyup'));
529 | });
530 |
531 | // Ensure that the dropdown stays open despite clicking on the input element.
532 | $(document).delegate('[data-event="firechat-user-search"]', 'click', function(event) {
533 | event.stopPropagation();
534 | });
535 | };
536 |
537 | /**
538 | * Binds user mute toggles and removes all messages for a given user upon mute.
539 | */
540 | FirechatUI.prototype._bindForUserMuting = function() {
541 | var self = this;
542 | $(document).delegate('[data-event="firechat-user-mute-toggle"]', 'click', function(event) {
543 | var $this = $(this),
544 | userId = $this.closest('[data-user-id]').data('user-id'),
545 | userName = $this.closest('[data-user-name]').data('user-name'),
546 | isMuted = $this.hasClass('red'),
547 | template = FirechatDefaultTemplates["templates/prompt-user-mute.html"];
548 |
549 | event.preventDefault();
550 |
551 | // Require user confirmation for muting.
552 | if (!isMuted) {
553 | var $prompt = self.prompt('Mute User?', template({
554 | userName: userName
555 | }));
556 |
557 | $prompt.find('a.close').first().click(function() {
558 | $prompt.remove();
559 | return false;
560 | });
561 |
562 | $prompt.find('[data-toggle=decline]').first().click(function() {
563 | $prompt.remove();
564 | return false;
565 | });
566 |
567 | $prompt.find('[data-toggle=accept]').first().click(function() {
568 | self._chat.toggleUserMute(userId);
569 | $prompt.remove();
570 | return false;
571 | });
572 | } else {
573 | self._chat.toggleUserMute(userId);
574 | }
575 | });
576 | };
577 |
578 | /**
579 | * Binds to elements with the data-event='firechat-user-(private)-invite' and
580 | * handles invitations as well as room creation and entering.
581 | */
582 | FirechatUI.prototype._bindForChatInvites = function() {
583 | var self = this,
584 | renderInvitePrompt = function(event) {
585 | var $this = $(this),
586 | userId = $this.closest('[data-user-id]').data('user-id'),
587 | roomId = $this.closest('[data-room-id]').data('room-id'),
588 | userName = $this.closest('[data-user-name]').data('user-name'),
589 | template = FirechatDefaultTemplates["templates/prompt-invite-private.html"],
590 | $prompt;
591 |
592 | self._chat.getRoom(roomId, function(room) {
593 | $prompt = self.prompt('Invite', template({
594 | userName: userName,
595 | roomName: room.name
596 | }));
597 |
598 | $prompt.find('a.close').click(function() {
599 | $prompt.remove();
600 | return false;
601 | });
602 |
603 | $prompt.find('[data-toggle=decline]').click(function() {
604 | $prompt.remove();
605 | return false;
606 | });
607 |
608 | $prompt.find('[data-toggle=accept]').first().click(function() {
609 | $prompt.remove();
610 | self._chat.inviteUser(userId, roomId, room.name);
611 | return false;
612 | });
613 | return false;
614 | });
615 | return false;
616 | },
617 | renderPrivateInvitePrompt = function(event) {
618 | var $this = $(this),
619 | userId = $this.closest('[data-user-id]').data('user-id'),
620 | userName = $this.closest('[data-user-name]').data('user-name'),
621 | template = FirechatDefaultTemplates["templates/prompt-invite-private.html"],
622 | $prompt;
623 |
624 | if (userId && userName) {
625 | $prompt = self.prompt('Private Invite', template({
626 | userName: userName,
627 | roomName: 'Private Chat'
628 | }));
629 |
630 | $prompt.find('a.close').click(function() {
631 | $prompt.remove();
632 | return false;
633 | });
634 |
635 | $prompt.find('[data-toggle=decline]').click(function() {
636 | $prompt.remove();
637 | return false;
638 | });
639 |
640 | $prompt.find('[data-toggle=accept]').first().click(function() {
641 | $prompt.remove();
642 | var roomName = 'Private Chat';
643 | self._chat.createRoom(roomName, 'private', function(roomId) {
644 | self._chat.inviteUser(userId, roomId, roomName);
645 | });
646 | return false;
647 | });
648 | }
649 | return false;
650 | };
651 |
652 | $(document).delegate('[data-event="firechat-user-chat"]', 'click', renderPrivateInvitePrompt);
653 | $(document).delegate('[data-event="firechat-user-invite"]', 'click', renderInvitePrompt);
654 | };
655 |
656 | /**
657 | * Binds to room dropdown button, menu items, and create room button.
658 | */
659 | FirechatUI.prototype._bindForRoomListing = function() {
660 | var self = this,
661 | $createRoomPromptButton = $('#firechat-btn-create-room-prompt'),
662 | $createRoomButton = $('#firechat-btn-create-room'),
663 | renderRoomList = function(event) {
664 | var type = $(this).data('room-type');
665 |
666 | self.sortListLexicographically('#firechat-room-list');
667 | };
668 |
669 | // Handle click of the create new room prompt-button.
670 | $createRoomPromptButton.bind('click', function(event) {
671 | self.promptCreateRoom();
672 | return false;
673 | });
674 |
675 | // Handle click of the create new room button.
676 | $createRoomButton.bind('click', function(event) {
677 | var roomName = $('#firechat-input-room-name').val();
678 | $('#firechat-prompt-create-room').remove();
679 | self._chat.createRoom(roomName);
680 | return false;
681 | });
682 | };
683 |
684 | /**
685 | * A stripped-down version of bootstrap-tab.js.
686 | *
687 | * Original bootstrap-tab.js Copyright 2012 Twitter, Inc.,licensed under the Apache v2.0
688 | */
689 | FirechatUI.prototype._setupTabs = function() {
690 | var self = this,
691 | show = function($el) {
692 | var $this = $el,
693 | $ul = $this.closest('ul:not(.firechat-dropdown-menu)'),
694 | selector = $this.attr('data-target'),
695 | previous = $ul.find('.active:last a')[0],
696 | $target,
697 | e;
698 |
699 | if (!selector) {
700 | selector = $this.attr('href');
701 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '');
702 | }
703 |
704 | if ($this.parent('li').hasClass('active')) return;
705 |
706 | e = $.Event('show', { relatedTarget: previous });
707 |
708 | $this.trigger(e);
709 |
710 | if (e.isDefaultPrevented()) return;
711 |
712 | $target = $(selector);
713 |
714 | activate($this.parent('li'), $ul);
715 | activate($target, $target.parent(), function () {
716 | $this.trigger({
717 | type: 'shown',
718 | relatedTarget: previous
719 | });
720 | });
721 | },
722 | activate = function (element, container, callback) {
723 | var $active = container.find('> .active'),
724 | transition = callback && $.support.transition && $active.hasClass('fade');
725 |
726 | function next() {
727 | $active
728 | .removeClass('active')
729 | .find('> .firechat-dropdown-menu > .active')
730 | .removeClass('active');
731 |
732 | element.addClass('active');
733 |
734 | if (transition) {
735 | element.addClass('in');
736 | } else {
737 | element.removeClass('fade');
738 | }
739 |
740 | if (element.parent('.firechat-dropdown-menu')) {
741 | element.closest('li.firechat-dropdown').addClass('active');
742 | }
743 |
744 | if (callback) {
745 | callback();
746 | }
747 | }
748 |
749 | if (transition) {
750 | $active.one($.support.transition.end, next);
751 | } else {
752 | next();
753 | }
754 |
755 | $active.removeClass('in');
756 | };
757 |
758 | $(document).delegate('[data-toggle="firechat-tab"]', 'click', function(event) {
759 | event.preventDefault();
760 | show($(this));
761 | });
762 | };
763 |
764 | /**
765 | * A stripped-down version of bootstrap-dropdown.js.
766 | *
767 | * Original bootstrap-dropdown.js Copyright 2012 Twitter, Inc., licensed under the Apache v2.0
768 | */
769 | FirechatUI.prototype._setupDropdowns = function() {
770 | var self = this,
771 | toggle = '[data-toggle=firechat-dropdown]',
772 | toggleDropdown = function(event) {
773 | var $this = $(this),
774 | $parent = getParent($this),
775 | isActive = $parent.hasClass('open');
776 |
777 | if ($this.is('.disabled, :disabled')) return;
778 |
779 | clearMenus();
780 |
781 | if (!isActive) {
782 | $parent.toggleClass('open');
783 | }
784 |
785 | $this.focus();
786 |
787 | return false;
788 | },
789 | clearMenus = function() {
790 | $('[data-toggle=firechat-dropdown]').each(function() {
791 | getParent($(this)).removeClass('open');
792 | });
793 | },
794 | getParent = function($this) {
795 | var selector = $this.attr('data-target'),
796 | $parent;
797 |
798 | if (!selector) {
799 | selector = $this.attr('href');
800 | selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
801 | }
802 |
803 | $parent = selector && (selector !== '#') && $(selector);
804 |
805 | if (!$parent || !$parent.length) $parent = $this.parent();
806 |
807 | return $parent;
808 | };
809 |
810 | $(document)
811 | .bind('click', clearMenus)
812 | .delegate('.firechat-dropdown-menu', 'click', function(event) { event.stopPropagation(); })
813 | .delegate('[data-toggle=firechat-dropdown]', 'click', toggleDropdown);
814 | };
815 |
816 | /**
817 | * Binds to any text input fields with data-provide='limit' and
818 | * data-counter='', and upon value change updates the selector
819 | * content to reflect the number of characters remaining, as the 'maxlength'
820 | * attribute less the current value length.
821 | */
822 | FirechatUI.prototype._bindTextInputFieldLimits = function() {
823 | $('body').delegate('input[data-provide="limit"], textarea[data-provide="limit"]', 'keyup', function(event) {
824 | var $this = $(this),
825 | $target = $($this.data('counter')),
826 | limit = $this.attr('maxlength'),
827 | count = $this.val().length;
828 |
829 | $target.html(Math.max(0, limit - count));
830 | });
831 | };
832 |
833 | /**
834 | * Given a title and message content, show an alert prompt to the user.
835 | *
836 | * @param {string} title
837 | * @param {string} message
838 | */
839 | FirechatUI.prototype.renderAlertPrompt = function(title, message) {
840 | var template = FirechatDefaultTemplates["templates/prompt-alert.html"],
841 | $prompt = this.prompt(title, template({ message: message }));
842 |
843 | $prompt.find('.close').click(function() {
844 | $prompt.remove();
845 | return false;
846 | });
847 | return;
848 | };
849 |
850 | /**
851 | * Toggle input field s if we want limit / unlimit input fields.
852 | */
853 | FirechatUI.prototype.toggleInputs = function(isEnabled) {
854 | $('#firechat-tab-content textarea').each(function() {
855 | var $this = $(this);
856 | if (isEnabled) {
857 | $(this).val('');
858 | } else {
859 | $(this).val('You have exceeded the message limit, please wait before sending.');
860 | }
861 | $this.prop('disabled', !isEnabled);
862 | });
863 | $('#firechat-input-name').prop('disabled', !isEnabled);
864 | };
865 |
866 | /**
867 | * Given a room id and name, attach the tab to the interface and setup events.
868 | *
869 | * @param {string} roomId
870 | * @param {string} roomName
871 | */
872 | FirechatUI.prototype.attachTab = function(roomId, roomName) {
873 | var self = this;
874 |
875 | // If this tab already exists, give it focus.
876 | if (this.$messages[roomId]) {
877 | this.focusTab(roomId);
878 | return;
879 | }
880 |
881 | var room = {
882 | id: roomId,
883 | name: roomName
884 | };
885 |
886 | // Populate and render the tab content template.
887 | var tabTemplate = FirechatDefaultTemplates["templates/tab-content.html"];
888 | var $tabContent = $(tabTemplate(room));
889 | this.$tabContent.prepend($tabContent);
890 | var $messages = $('#firechat-messages' + roomId);
891 |
892 | // Keep a reference to the message listing for later use.
893 | this.$messages[roomId] = $messages;
894 |
895 | // Attach on-enter event to textarea.
896 | var $textarea = $tabContent.find('textarea').first();
897 | $textarea.bind('keydown', function(e) {
898 | var message = self.trimWithEllipsis($textarea.val(), self.maxLengthMessage);
899 | if ((e.which === 13) && (message !== '')) {
900 | $textarea.val('');
901 | self._chat.sendMessage(roomId, message);
902 | return false;
903 | }
904 | });
905 |
906 | // Populate and render the tab menu template.
907 | var tabListTemplate = FirechatDefaultTemplates["templates/tab-menu-item.html"];
908 | var $tab = $(tabListTemplate(room));
909 | this.$tabList.prepend($tab);
910 |
911 | // Attach on-shown event to move tab to front and scroll to bottom.
912 | $tab.bind('shown', function(event) {
913 | $messages.scrollTop($messages[0].scrollHeight);
914 | });
915 |
916 | // Dynamically update the width of each tab based upon the number open.
917 | var tabs = this.$tabList.children('li');
918 | var tabWidth = Math.floor($('#firechat-tab-list').width() / tabs.length);
919 | this.$tabList.children('li').css('width', tabWidth);
920 |
921 | // Update the room listing to reflect that we're now in the room.
922 | this.$roomList.children('[data-room-id=' + roomId + ']').children('a').addClass('highlight');
923 |
924 | // Sort each item in the user list alphabetically on click of the dropdown.
925 | $('#firechat-btn-room-user-list-' + roomId).bind('click', function() {
926 | self.sortListLexicographically('#firechat-room-user-list-' + roomId);
927 | return false;
928 | });
929 |
930 | // Automatically select the new tab.
931 | this.focusTab(roomId);
932 | };
933 |
934 | /**
935 | * Given a room id, focus the given tab.
936 | *
937 | * @param {string} roomId
938 | */
939 | FirechatUI.prototype.focusTab = function(roomId) {
940 | if (this.$messages[roomId]) {
941 | var $tabLink = this.$tabList.find('[data-room-id=' + roomId + ']').find('a');
942 | if ($tabLink.length) {
943 | $tabLink.first().trigger('click');
944 | }
945 | }
946 | };
947 |
948 | /**
949 | * Given a room id, remove the tab and all child elements from the interface.
950 | *
951 | * @param {string} roomId
952 | */
953 | FirechatUI.prototype.removeTab = function(roomId) {
954 | delete this.$messages[roomId];
955 |
956 | // Remove the inner tab content.
957 | this.$tabContent.find('[data-room-id=' + roomId + ']').remove();
958 |
959 | // Remove the tab from the navigation menu.
960 | this.$tabList.find('[data-room-id=' + roomId + ']').remove();
961 |
962 | // Dynamically update the width of each tab based upon the number open.
963 | var tabs = this.$tabList.children('li');
964 | var tabWidth = Math.floor($('#firechat-tab-list').width() / tabs.length);
965 | this.$tabList.children('li').css('width', tabWidth);
966 |
967 | // Automatically select the next tab if there is one.
968 | this.$tabList.find('[data-toggle="firechat-tab"]').first().trigger('click');
969 |
970 | // Update the room listing to reflect that we're now in the room.
971 | this.$roomList.children('[data-room-id=' + roomId + ']').children('a').removeClass('highlight');
972 | };
973 |
974 | /**
975 | * Render a new message in the specified chat room.
976 | *
977 | * @param {string} roomId
978 | * @param {string} message
979 | */
980 | FirechatUI.prototype.showMessage = function(roomId, rawMessage) {
981 | var self = this;
982 |
983 | // Setup defaults
984 | var message = {
985 | id : rawMessage.id,
986 | localtime : self.formatTime(rawMessage.timestamp),
987 | message : rawMessage.message || '',
988 | userId : rawMessage.userId,
989 | name : rawMessage.name,
990 | type : rawMessage.type || 'default',
991 | isSelfMessage : (self._user && rawMessage.userId == self._user.id),
992 | disableActions : (!self._user || rawMessage.userId == self._user.id)
993 | };
994 |
995 | // While other data is escaped in the Underscore.js templates, escape and
996 | // process the message content here to add additional functionality (add links).
997 | // Also trim the message length to some client-defined maximum.
998 | var messageConstructed = '';
999 | message.message = _.map(message.message.split(' '), function(token) {
1000 | if (self.urlPattern.test(token) || self.pseudoUrlPattern.test(token)) {
1001 | return self.linkify(encodeURI(token));
1002 | } else {
1003 | return _.escape(token);
1004 | }
1005 | }).join(' ');
1006 | message.message = self.trimWithEllipsis(message.message, self.maxLengthMessage);
1007 |
1008 | // Populate and render the message template.
1009 | var template = FirechatDefaultTemplates["templates/message.html"];
1010 | var $message = $(template(message));
1011 | var $messages = self.$messages[roomId];
1012 | if ($messages) {
1013 |
1014 | var scrollToBottom = false;
1015 | if ($messages.scrollTop() / ($messages[0].scrollHeight - $messages[0].offsetHeight) >= 0.95) {
1016 | // Pinned to bottom
1017 | scrollToBottom = true;
1018 | } else if ($messages[0].scrollHeight <= $messages.height()) {
1019 | // Haven't added the scrollbar yet
1020 | scrollToBottom = true;
1021 | }
1022 |
1023 | $messages.append($message);
1024 |
1025 | if (scrollToBottom) {
1026 | $messages.scrollTop($messages[0].scrollHeight);
1027 | }
1028 | }
1029 | };
1030 |
1031 | /**
1032 | * Remove a message by id.
1033 | *
1034 | * @param {string} roomId
1035 | * @param {string} messageId
1036 | */
1037 | FirechatUI.prototype.removeMessage = function(roomId, messageId) {
1038 | $('.message[data-message-id="' + messageId + '"]').remove();
1039 | };
1040 |
1041 | /**
1042 | * Given a selector for a list element, sort the items alphabetically.
1043 | *
1044 | * @param {string} selector
1045 | */
1046 | FirechatUI.prototype.sortListLexicographically = function(selector) {
1047 | $(selector).children("li").sort(function(a, b) {
1048 | var upA = $(a).text().toUpperCase();
1049 | var upB = $(b).text().toUpperCase();
1050 | return (upA < upB) ? -1 : (upA > upB) ? 1 : 0;
1051 | }).appendTo(selector);
1052 | };
1053 |
1054 | /**
1055 | * Remove leading and trailing whitespace from a string and shrink it, with
1056 | * added ellipsis, if it exceeds a specified length.
1057 | *
1058 | * @param {string} str
1059 | * @param {number} length
1060 | * @return {string}
1061 | */
1062 | FirechatUI.prototype.trimWithEllipsis = function(str, length) {
1063 | str = str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
1064 | return (length && str.length <= length) ? str : str.substring(0, length) + '...';
1065 | };
1066 |
1067 | /**
1068 | * Given a timestamp, format it in the form hh:mm am/pm. Defaults to now
1069 | * if the timestamp is undefined.
1070 | *
1071 | * @param {Number} timestamp
1072 | * @param {string} date
1073 | */
1074 | FirechatUI.prototype.formatTime = function(timestamp) {
1075 | var date = (timestamp) ? new Date(timestamp) : new Date(),
1076 | hours = date.getHours() || 12,
1077 | minutes = '' + date.getMinutes(),
1078 | ampm = (date.getHours() >= 12) ? 'pm' : 'am';
1079 |
1080 | hours = (hours > 12) ? hours - 12 : hours;
1081 | minutes = (minutes.length < 2) ? '0' + minutes : minutes;
1082 | return '' + hours + ':' + minutes + ampm;
1083 | };
1084 |
1085 | /**
1086 | * Launch a prompt to allow the user to create a new room.
1087 | */
1088 | FirechatUI.prototype.promptCreateRoom = function() {
1089 | var self = this;
1090 | var template = FirechatDefaultTemplates["templates/prompt-create-room.html"];
1091 |
1092 | var $prompt = this.prompt('Create Public Room', template({
1093 | maxLengthRoomName: this.maxLengthRoomName,
1094 | isModerator: self._chat.userIsModerator()
1095 | }));
1096 | $prompt.find('a.close').first().click(function() {
1097 | $prompt.remove();
1098 | return false;
1099 | });
1100 |
1101 |
1102 | $prompt.find('[data-toggle=submit]').first().click(function() {
1103 | var name = $prompt.find('[data-input=firechat-room-name]').first().val();
1104 | if (name !== '') {
1105 | self._chat.createRoom(name, 'public');
1106 | $prompt.remove();
1107 | }
1108 | return false;
1109 | });
1110 |
1111 | $prompt.find('[data-input=firechat-room-name]').first().focus();
1112 | $prompt.find('[data-input=firechat-room-name]').first().bind('keydown', function(e) {
1113 | if (e.which === 13) {
1114 | var name = $prompt.find('[data-input=firechat-room-name]').first().val();
1115 | if (name !== '') {
1116 | self._chat.createRoom(name, 'public');
1117 | $prompt.remove();
1118 | return false;
1119 | }
1120 | }
1121 | });
1122 | };
1123 |
1124 | /**
1125 | * Inner method to launch a prompt given a specific title and HTML content.
1126 | * @param {string} title
1127 | * @param {string} content
1128 | */
1129 | FirechatUI.prototype.prompt = function(title, content) {
1130 | var template = FirechatDefaultTemplates["templates/prompt.html"],
1131 | $prompt;
1132 |
1133 | $prompt = $(template({
1134 | title: title,
1135 | content: content
1136 | })).css({
1137 | top: this.$wrapper.position().top + (0.333 * this.$wrapper.height()),
1138 | left: this.$wrapper.position().left + (0.125 * this.$wrapper.width()),
1139 | width: 0.75 * this.$wrapper.width()
1140 | });
1141 | this.$wrapper.append($prompt.removeClass('hidden'));
1142 | return $prompt;
1143 | };
1144 |
1145 | // see http://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
1146 | FirechatUI.prototype.linkify = function(str) {
1147 | var self = this;
1148 | return str
1149 | .replace(self.urlPattern, '$&')
1150 | .replace(self.pseudoUrlPattern, '$1$2');
1151 | };
1152 |
1153 | })(jQuery);
1154 |
--------------------------------------------------------------------------------
/src/js/firechat.js:
--------------------------------------------------------------------------------
1 | // Firechat is a simple, easily-extensible data layer for multi-user,
2 | // multi-room chat, built entirely on [Firebase](https://firebase.google.com).
3 | //
4 | // The Firechat object is the primary conduit for all underlying data events.
5 | // It exposes a number of methods for binding event listeners, creating,
6 | // entering, or leaving chat rooms, initiating chats, sending messages,
7 | // and moderator actions such as warning, kicking, or suspending users.
8 | //
9 | // firechat.js 0.0.0
10 | // https://firebase.google.com
11 | // (c) 2016 Firebase
12 | // License: MIT
13 |
14 | // Setup
15 | // --------------
16 | (function() {
17 | // Establish a reference to the `window` object, and save the previous value
18 | // of the `Firechat` variable.
19 | var root = this,
20 | previousFirechat = root.Firechat;
21 |
22 | function Firechat(firebaseRef, options) {
23 |
24 | // Cache the provided Database reference and the firebase.App instance
25 | this._firechatRef = firebaseRef;
26 | this._firebaseApp = firebaseRef.database.app;
27 |
28 | // User-specific instance variables.
29 | this._user = null;
30 | this._userId = null;
31 | this._userName = null;
32 | this._isModerator = false;
33 |
34 | // A unique id generated for each session.
35 | this._sessionId = null;
36 |
37 | // A mapping of event IDs to an array of callbacks.
38 | this._events = {};
39 |
40 | // A mapping of room IDs to a boolean indicating presence.
41 | this._rooms = {};
42 |
43 | // A mapping of operations to re-queue on disconnect.
44 | this._presenceBits = {};
45 |
46 | // Commonly-used Firebase references.
47 | this._userRef = null;
48 | this._messageRef = this._firechatRef.child('room-messages');
49 | this._roomRef = this._firechatRef.child('room-metadata');
50 | this._privateRoomRef = this._firechatRef.child('room-private-metadata');
51 | this._moderatorsRef = this._firechatRef.child('moderators');
52 | this._suspensionsRef = this._firechatRef.child('suspensions');
53 | this._usersOnlineRef = this._firechatRef.child('user-names-online');
54 |
55 | // Setup and establish default options.
56 | this._options = options || {};
57 |
58 | // The number of historical messages to load per room.
59 | this._options.numMaxMessages = this._options.numMaxMessages || 50;
60 | }
61 |
62 | // Run Firechat in *noConflict* mode, returning the `Firechat` variable to
63 | // its previous owner, and returning a reference to the Firechat object.
64 | Firechat.noConflict = function noConflict() {
65 | root.Firechat = previousFirechat;
66 | return Firechat;
67 | };
68 |
69 | // Export the Firechat object as a global.
70 | root.Firechat = Firechat;
71 |
72 | // Firechat Internal Methods
73 | // --------------
74 | Firechat.prototype = {
75 |
76 | // Load the initial metadata for the user's account and set initial state.
77 | _loadUserMetadata: function(onComplete) {
78 | var self = this;
79 |
80 | // Update the user record with a default name on user's first visit.
81 | this._userRef.transaction(function(current) {
82 | if (!current || !current.id || !current.name) {
83 | return {
84 | id: self._userId,
85 | name: self._userName
86 | };
87 | }
88 | }, function(error, committed, snapshot) {
89 | self._user = snapshot.val();
90 | self._moderatorsRef.child(self._userId).once('value', function(snapshot) {
91 | self._isModerator = !!snapshot.val();
92 | root.setTimeout(onComplete, 0);
93 | });
94 | });
95 | },
96 |
97 | // Initialize Firebase listeners and callbacks for the supported bindings.
98 | _setupDataEvents: function() {
99 | // Monitor connection state so we can requeue disconnect operations if need be.
100 | var connectedRef = this._firechatRef.root.child('.info/connected');
101 | connectedRef.on('value', function(snapshot) {
102 | if (snapshot.val() === true) {
103 | // We're connected (or reconnected)! Set up our presence state.
104 | for (var path in this._presenceBits) {
105 | var op = this._presenceBits[path],
106 | ref = op.ref;
107 |
108 | ref.onDisconnect().set(op.offlineValue);
109 | ref.set(op.onlineValue);
110 | }
111 | }
112 | }, this);
113 |
114 | // Queue up a presence operation to remove the session when presence is lost
115 | this._queuePresenceOperation(this._sessionRef, true, null);
116 |
117 | // Register our username in the public user listing.
118 | var usernameRef = this._usersOnlineRef.child(this._userName.toLowerCase());
119 | var usernameSessionRef = usernameRef.child(this._sessionId);
120 | this._queuePresenceOperation(usernameSessionRef, {
121 | id: this._userId,
122 | name: this._userName
123 | }, null);
124 |
125 | // Listen for state changes for the given user.
126 | this._userRef.on('value', this._onUpdateUser, this);
127 |
128 | // Listen for chat invitations from other users.
129 | this._userRef.child('invites').on('child_added', this._onFirechatInvite, this);
130 |
131 | // Listen for messages from moderators and adminstrators.
132 | this._userRef.child('notifications').on('child_added', this._onNotification, this);
133 | },
134 |
135 | // Append the new callback to our list of event handlers.
136 | _addEventCallback: function(eventId, callback) {
137 | this._events[eventId] = this._events[eventId] || [];
138 | this._events[eventId].push(callback);
139 | },
140 |
141 | // Retrieve the list of event handlers for a given event id.
142 | _getEventCallbacks: function(eventId) {
143 | if (this._events.hasOwnProperty(eventId)) {
144 | return this._events[eventId];
145 | }
146 | return [];
147 | },
148 |
149 | // Invoke each of the event handlers for a given event id with specified data.
150 | _invokeEventCallbacks: function(eventId) {
151 | var args = [],
152 | callbacks = this._getEventCallbacks(eventId);
153 |
154 | Array.prototype.push.apply(args, arguments);
155 | args = args.slice(1);
156 |
157 | for (var i = 0; i < callbacks.length; i += 1) {
158 | callbacks[i].apply(null, args);
159 | }
160 | },
161 |
162 | // Keep track of on-disconnect events so they can be requeued if we disconnect the reconnect.
163 | _queuePresenceOperation: function(ref, onlineValue, offlineValue) {
164 | ref.onDisconnect().set(offlineValue);
165 | ref.set(onlineValue);
166 | this._presenceBits[ref.toString()] = {
167 | ref: ref,
168 | onlineValue: onlineValue,
169 | offlineValue: offlineValue
170 | };
171 | },
172 |
173 | // Remove an on-disconnect event from firing upon future disconnect and reconnect.
174 | _removePresenceOperation: function(ref, value) {
175 | var path = ref.toString();
176 | ref.onDisconnect().cancel();
177 | ref.set(value);
178 | delete this._presenceBits[path];
179 | },
180 |
181 | // Event to monitor current user state.
182 | _onUpdateUser: function(snapshot) {
183 | this._user = snapshot.val();
184 | this._userName = this._user.name;
185 | this._invokeEventCallbacks('user-update', this._user);
186 | },
187 |
188 | // Event to monitor current auth + user state.
189 | _onAuthRequired: function() {
190 | this._invokeEventCallbacks('auth-required');
191 | },
192 |
193 | // Events to monitor room entry / exit and messages additional / removal.
194 | _onEnterRoom: function(room) {
195 | this._invokeEventCallbacks('room-enter', room);
196 | },
197 | _onNewMessage: function(roomId, snapshot) {
198 | var message = snapshot.val();
199 | message.id = snapshot.key;
200 | this._invokeEventCallbacks('message-add', roomId, message);
201 | },
202 | _onRemoveMessage: function(roomId, snapshot) {
203 | var messageId = snapshot.key;
204 | this._invokeEventCallbacks('message-remove', roomId, messageId);
205 | },
206 | _onLeaveRoom: function(roomId) {
207 | this._invokeEventCallbacks('room-exit', roomId);
208 | },
209 |
210 | // Event to listen for notifications from administrators and moderators.
211 | _onNotification: function(snapshot) {
212 | var notification = snapshot.val();
213 | if (!notification.read) {
214 | if (notification.notificationType !== 'suspension' || notification.data.suspendedUntil < new Date().getTime()) {
215 | snapshot.ref.child('read').set(true);
216 | }
217 | this._invokeEventCallbacks('notification', notification);
218 | }
219 | },
220 |
221 | // Events to monitor chat invitations and invitation replies.
222 | _onFirechatInvite: function(snapshot) {
223 | var self = this,
224 | invite = snapshot.val();
225 |
226 | // Skip invites we've already responded to.
227 | if (invite.status) {
228 | return;
229 | }
230 |
231 | invite.id = invite.id || snapshot.key;
232 | self.getRoom(invite.roomId, function(room) {
233 | invite.toRoomName = room.name;
234 | self._invokeEventCallbacks('room-invite', invite);
235 | });
236 | },
237 | _onFirechatInviteResponse: function(snapshot) {
238 | var self = this,
239 | invite = snapshot.val();
240 |
241 | invite.id = invite.id || snapshot.key;
242 | this._invokeEventCallbacks('room-invite-response', invite);
243 | }
244 | };
245 |
246 | // Firechat External Methods
247 | // --------------
248 |
249 | // Initialize the library and setup data listeners.
250 | Firechat.prototype.setUser = function(userId, userName, callback) {
251 | var self = this;
252 |
253 | self._firebaseApp.auth().onAuthStateChanged(function(user) {
254 | if (user) {
255 | self._userId = userId.toString();
256 | self._userName = userName.toString();
257 | self._userRef = self._firechatRef.child('users').child(self._userId);
258 | self._sessionRef = self._userRef.child('sessions').push();
259 | self._sessionId = self._sessionRef.key;
260 |
261 | self._loadUserMetadata(function() {
262 | root.setTimeout(function() {
263 | callback(self._user);
264 | self._setupDataEvents();
265 | }, 0);
266 | });
267 | } else {
268 | self.warn('Firechat requires an authenticated Firebase reference. Pass an authenticated reference before loading.');
269 | }
270 | });
271 | };
272 |
273 | // Resumes the previous session by automatically entering rooms.
274 | Firechat.prototype.resumeSession = function() {
275 | this._userRef.child('rooms').once('value', function(snapshot) {
276 | var rooms = snapshot.val();
277 | for (var roomId in rooms) {
278 | this.enterRoom(rooms[roomId].id);
279 | }
280 | }, /* onError */ function(){}, /* context */ this);
281 | };
282 |
283 | // Callback registration. Supports each of the following events:
284 | Firechat.prototype.on = function(eventType, cb) {
285 | this._addEventCallback(eventType, cb);
286 | };
287 |
288 | // Create and automatically enter a new chat room.
289 | Firechat.prototype.createRoom = function(roomName, roomType, callback) {
290 | var self = this,
291 | newRoomRef = this._roomRef.push();
292 |
293 | var newRoom = {
294 | id: newRoomRef.key,
295 | name: roomName,
296 | type: roomType || 'public',
297 | createdByUserId: this._userId,
298 | createdAt: firebase.database.ServerValue.TIMESTAMP
299 | };
300 |
301 | if (roomType === 'private') {
302 | newRoom.authorizedUsers = {};
303 | newRoom.authorizedUsers[this._userId] = true;
304 | }
305 |
306 | newRoomRef.set(newRoom, function(error) {
307 | if (!error) {
308 | self.enterRoom(newRoomRef.key);
309 | }
310 | if (callback) {
311 | callback(newRoomRef.key);
312 | }
313 | });
314 | };
315 |
316 | // Enter a chat room.
317 | Firechat.prototype.enterRoom = function(roomId) {
318 | var self = this;
319 | self.getRoom(roomId, function(room) {
320 | var roomName = room.name;
321 |
322 | if (!roomId || !roomName) return;
323 |
324 | // Skip if we're already in this room.
325 | if (self._rooms[roomId]) {
326 | return;
327 | }
328 |
329 | self._rooms[roomId] = true;
330 |
331 | if (self._user) {
332 | // Save entering this room to resume the session again later.
333 | self._userRef.child('rooms').child(roomId).set({
334 | id: roomId,
335 | name: roomName,
336 | active: true
337 | });
338 |
339 | // Set presence bit for the room and queue it for removal on disconnect.
340 | var presenceRef = self._firechatRef.child('room-users').child(roomId).child(self._userId).child(self._sessionId);
341 | self._queuePresenceOperation(presenceRef, {
342 | id: self._userId,
343 | name: self._userName
344 | }, null);
345 | }
346 |
347 | // Invoke our callbacks before we start listening for new messages.
348 | self._onEnterRoom({ id: roomId, name: roomName });
349 |
350 | // Setup message listeners
351 | self._roomRef.child(roomId).once('value', function(snapshot) {
352 | self._messageRef.child(roomId).limitToLast(self._options.numMaxMessages).on('child_added', function(snapshot) {
353 | self._onNewMessage(roomId, snapshot);
354 | }, /* onCancel */ function() {
355 | // Turns out we don't have permission to access these messages.
356 | self.leaveRoom(roomId);
357 | }, /* context */ self);
358 |
359 | self._messageRef.child(roomId).limitToLast(self._options.numMaxMessages).on('child_removed', function(snapshot) {
360 | self._onRemoveMessage(roomId, snapshot);
361 | }, /* onCancel */ function(){}, /* context */ self);
362 | }, /* onFailure */ function(){}, self);
363 | });
364 | };
365 |
366 | // Leave a chat room.
367 | Firechat.prototype.leaveRoom = function(roomId) {
368 | var self = this,
369 | userRoomRef = self._firechatRef.child('room-users').child(roomId);
370 |
371 | // Remove listener for new messages to this room.
372 | self._messageRef.child(roomId).off();
373 |
374 | if (self._user) {
375 | var presenceRef = userRoomRef.child(self._userId).child(self._sessionId);
376 |
377 | // Remove presence bit for the room and cancel on-disconnect removal.
378 | self._removePresenceOperation(presenceRef, null);
379 |
380 | // Remove session bit for the room.
381 | self._userRef.child('rooms').child(roomId).remove();
382 | }
383 |
384 | delete self._rooms[roomId];
385 |
386 | // Invoke event callbacks for the room-exit event.
387 | self._onLeaveRoom(roomId);
388 | };
389 |
390 | Firechat.prototype.sendMessage = function(roomId, messageContent, messageType, cb) {
391 | var self = this,
392 | message = {
393 | userId: self._userId,
394 | name: self._userName,
395 | timestamp: firebase.database.ServerValue.TIMESTAMP,
396 | message: messageContent,
397 | type: messageType || 'default'
398 | },
399 | newMessageRef;
400 |
401 | if (!self._user) {
402 | self._onAuthRequired();
403 | if (cb) {
404 | cb(new Error('Not authenticated or user not set!'));
405 | }
406 | return;
407 | }
408 |
409 | newMessageRef = self._messageRef.child(roomId).push();
410 | newMessageRef.setWithPriority(message, firebase.database.ServerValue.TIMESTAMP, cb);
411 | };
412 |
413 | Firechat.prototype.deleteMessage = function(roomId, messageId, cb) {
414 | var self = this;
415 |
416 | self._messageRef.child(roomId).child(messageId).remove(cb);
417 | };
418 |
419 | // Mute or unmute a given user by id. This list will be stored internally and
420 | // all messages from the muted clients will be filtered client-side after
421 | // receipt of each new message.
422 | Firechat.prototype.toggleUserMute = function(userId, cb) {
423 | var self = this;
424 |
425 | if (!self._user) {
426 | self._onAuthRequired();
427 | if (cb) {
428 | cb(new Error('Not authenticated or user not set!'));
429 | }
430 | return;
431 | }
432 |
433 | self._userRef.child('muted').child(userId).transaction(function(isMuted) {
434 | return (isMuted) ? null : true;
435 | }, cb);
436 | };
437 |
438 | // Send a moderator notification to a specific user.
439 | Firechat.prototype.sendSuperuserNotification = function(userId, notificationType, data, cb) {
440 | var self = this,
441 | userNotificationsRef = self._firechatRef.child('users').child(userId).child('notifications');
442 |
443 | userNotificationsRef.push({
444 | fromUserId: self._userId,
445 | timestamp: firebase.database.ServerValue.TIMESTAMP,
446 | notificationType: notificationType,
447 | data: data || {}
448 | }, cb);
449 | };
450 |
451 | // Warn a user for violating the terms of service or being abusive.
452 | Firechat.prototype.warnUser = function(userId) {
453 | var self = this;
454 |
455 | self.sendSuperuserNotification(userId, 'warning');
456 | };
457 |
458 | // Suspend a user by putting the user into read-only mode for a period.
459 | Firechat.prototype.suspendUser = function(userId, timeLengthSeconds, cb) {
460 | var self = this,
461 | suspendedUntil = new Date().getTime() + 1000*timeLengthSeconds;
462 |
463 | self._suspensionsRef.child(userId).set(suspendedUntil, function(error) {
464 | if (error && cb) {
465 | return cb(error);
466 | } else {
467 | self.sendSuperuserNotification(userId, 'suspension', {
468 | suspendedUntil: suspendedUntil
469 | });
470 | return cb(null);
471 | }
472 | });
473 | };
474 |
475 | // Invite a user to a specific chat room.
476 | Firechat.prototype.inviteUser = function(userId, roomId) {
477 | var self = this,
478 | sendInvite = function() {
479 | var inviteRef = self._firechatRef.child('users').child(userId).child('invites').push();
480 | inviteRef.set({
481 | id: inviteRef.key,
482 | fromUserId: self._userId,
483 | fromUserName: self._userName,
484 | roomId: roomId
485 | });
486 |
487 | // Handle listen unauth / failure in case we're kicked.
488 | inviteRef.on('value', self._onFirechatInviteResponse, function(){}, self);
489 | };
490 |
491 | if (!self._user) {
492 | self._onAuthRequired();
493 | return;
494 | }
495 |
496 | self.getRoom(roomId, function(room) {
497 | if (room.type === 'private') {
498 | var authorizedUserRef = self._roomRef.child(roomId).child('authorizedUsers');
499 | authorizedUserRef.child(userId).set(true, function(error) {
500 | if (!error) {
501 | sendInvite();
502 | }
503 | });
504 | } else {
505 | sendInvite();
506 | }
507 | });
508 | };
509 |
510 | Firechat.prototype.acceptInvite = function(inviteId, cb) {
511 | var self = this;
512 |
513 | self._userRef.child('invites').child(inviteId).once('value', function(snapshot) {
514 | var invite = snapshot.val();
515 | if (invite === null && cb) {
516 | return cb(new Error('acceptInvite(' + inviteId + '): invalid invite id'));
517 | } else {
518 | self.enterRoom(invite.roomId);
519 | self._userRef.child('invites').child(inviteId).update({
520 | 'status': 'accepted',
521 | 'toUserName': self._userName
522 | }, cb);
523 | }
524 | }, self);
525 | };
526 |
527 | Firechat.prototype.declineInvite = function(inviteId, cb) {
528 | var self = this,
529 | updates = {
530 | 'status': 'declined',
531 | 'toUserName': self._userName
532 | };
533 |
534 | self._userRef.child('invites').child(inviteId).update(updates, cb);
535 | };
536 |
537 | Firechat.prototype.getRoomList = function(cb) {
538 | var self = this;
539 |
540 | self._roomRef.once('value', function(snapshot) {
541 | cb(snapshot.val());
542 | });
543 | };
544 |
545 | Firechat.prototype.getUsersByRoom = function() {
546 | var self = this,
547 | roomId = arguments[0],
548 | query = self._firechatRef.child('room-users').child(roomId),
549 | cb = arguments[arguments.length - 1],
550 | limit = null;
551 |
552 | if (arguments.length > 2) {
553 | limit = arguments[1];
554 | }
555 |
556 | query = (limit) ? query.limitToLast(limit) : query;
557 |
558 | query.once('value', function(snapshot) {
559 | var usernames = snapshot.val() || {},
560 | usernamesUnique = {};
561 |
562 | for (var username in usernames) {
563 | for (var session in usernames[username]) {
564 | // Skip all other sessions for this user as we only need one.
565 | usernamesUnique[username] = usernames[username][session];
566 | break;
567 | }
568 | }
569 |
570 | root.setTimeout(function() {
571 | cb(usernamesUnique);
572 | }, 0);
573 | });
574 | };
575 |
576 | Firechat.prototype.getUsersByPrefix = function(prefix, startAt, endAt, limit, cb) {
577 | var self = this,
578 | query = this._usersOnlineRef,
579 | prefixLower = prefix.toLowerCase();
580 |
581 | if (startAt) {
582 | query = query.startAt(null, startAt);
583 | } else if (endAt) {
584 | query = query.endAt(null, endAt);
585 | } else {
586 | query = (prefixLower) ? query.startAt(null, prefixLower) : query.startAt();
587 | }
588 |
589 | query = (limit) ? query.limitToLast(limit) : query;
590 |
591 | query.once('value', function(snapshot) {
592 | var usernames = snapshot.val() || {},
593 | usernamesFiltered = {};
594 |
595 | for (var userNameKey in usernames) {
596 | var sessions = usernames[userNameKey],
597 | userName, userId, usernameClean;
598 |
599 | // Grab the user data from the first registered active session.
600 | for (var sessionId in sessions) {
601 | userName = sessions[sessionId].name;
602 | userId = sessions[sessionId].id;
603 |
604 | // Skip all other sessions for this user as we only need one.
605 | break;
606 | }
607 |
608 | // Filter out any usernames that don't match our prefix and break.
609 | if ((prefix.length > 0) && (userName.toLowerCase().indexOf(prefixLower) !== 0))
610 | continue;
611 |
612 | usernamesFiltered[userName] = {
613 | name: userName,
614 | id: userId
615 | };
616 | }
617 |
618 | root.setTimeout(function() {
619 | cb(usernamesFiltered);
620 | }, 0);
621 | });
622 | };
623 |
624 | // Miscellaneous helper methods.
625 | Firechat.prototype.getRoom = function(roomId, callback) {
626 | this._roomRef.child(roomId).once('value', function(snapshot) {
627 | callback(snapshot.val());
628 | });
629 | };
630 |
631 | Firechat.prototype.userIsModerator = function() {
632 | return this._isModerator;
633 | };
634 |
635 | Firechat.prototype.warn = function(msg) {
636 | if (console) {
637 | msg = 'Firechat Warning: ' + msg;
638 | if (typeof console.warn === 'function') {
639 | console.warn(msg);
640 | } else if (typeof console.log === 'function') {
641 | console.log(msg);
642 | }
643 | }
644 | };
645 | })();
646 |
--------------------------------------------------------------------------------
/src/js/libs/underscore-1.7.0.min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.7.0
2 | // http://underscorejs.org
3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 | (function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
6 |
--------------------------------------------------------------------------------
/src/js/shims.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 | // Shim for Function.bind(...) - (Required by IE < 9, FF < 4, SF < 6)
4 | if (!Function.prototype.bind) {
5 | Function.prototype.bind = function(oThis) {
6 | if (typeof this !== "function") {
7 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
8 | }
9 |
10 | var aArgs = Array.prototype.slice.call(arguments, 1),
11 | fToBind = this,
12 | fNOP = function() {},
13 | fBound = function() {
14 | return fToBind.apply(this instanceof fNOP && oThis ? this : oThis,
15 | aArgs.concat(Array.prototype.slice.call(arguments)));
16 | };
17 |
18 | fNOP.prototype = this.prototype;
19 | fBound.prototype = new fNOP();
20 | return fBound;
21 | };
22 | }
23 |
24 | // Shim for Object.keys(...) - (Required by IE < 9, FF < 4)
25 | Object.keys = Object.keys || function(oObj) {
26 | var result = [];
27 | for (var name in oObj) {
28 | if (oObj.hasOwnProperty(name)) {
29 | result.push(name);
30 | }
31 | }
32 | return result;
33 | };
34 |
35 | })();
36 |
--------------------------------------------------------------------------------
/src/less/styles.less:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | /* Boilerplate: Reset
4 | ============================================================ */
5 | #firechat {
6 | div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:12px;font-family:arial,helvetica,sans-serif;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
7 | }
8 |
9 | /* Boilerplate: Mixins
10 | ============================================================ */
11 | .clearfix {
12 | *zoom: 1;
13 | &:before,
14 | &:after {
15 | display: table;
16 | content: "";
17 | line-height: 0;
18 | }
19 | &:after {
20 | clear: both;
21 | }
22 | }
23 |
24 | .background-clip(@clip) {
25 | -webkit-background-clip: @clip;
26 | -moz-background-clip: @clip;
27 | background-clip: @clip;
28 | }
29 |
30 | .border-radius(@radius) {
31 | -webkit-border-radius: @radius;
32 | -moz-border-radius: @radius;
33 | border-radius: @radius;
34 | }
35 |
36 | .border-radii(@topright, @bottomright, @bottomleft, @topleft) {
37 | -webkit-border-top-right-radius: @topright;
38 | -webkit-border-bottom-right-radius: @bottomright;
39 | -webkit-border-bottom-left-radius: @bottomleft;
40 | -webkit-border-top-left-radius: @topleft;
41 | -moz-border-radius-topright: @topright;
42 | -moz-border-radius-bottomright: @bottomright;
43 | -moz-border-radius-bottomleft: @bottomleft;
44 | -moz-border-radius-topleft: @topleft;
45 | border-top-right-radius: @topright;
46 | border-bottom-right-radius: @bottomright;
47 | border-bottom-left-radius: @bottomleft;
48 | border-top-left-radius: @topleft;
49 | }
50 |
51 | .box-shadow(@shadow) {
52 | -webkit-box-shadow: @shadow;
53 | -moz-box-shadow: @shadow;
54 | box-shadow: @shadow;
55 | }
56 |
57 | .box-sizing(@boxmodel) {
58 | -webkit-box-sizing: @boxmodel;
59 | -moz-box-sizing: @boxmodel;
60 | box-sizing: @boxmodel;
61 | }
62 |
63 | .placeholder(@color) {
64 | &:-moz-placeholder { color: @color; }
65 | &:-ms-input-placeholder { color: @color; }
66 | &::-webkit-input-placeholder { color: @color; }
67 | }
68 |
69 | .transition(@transition) {
70 | -webkit-transition: @transition;
71 | -moz-transition: @transition;
72 | -o-transition: @transition;
73 | transition: @transition;
74 | }
75 |
76 | .user-select(@user-select) {
77 | -webkit-touch-callout: @user-select;
78 | -webkit-user-select: @user-select;
79 | -khtml-user-select: @user-select;
80 | -moz-user-select: @user-select;
81 | -ms-user-select: @user-select;
82 | user-select: @user-select;
83 | }
84 |
85 |
86 | /* Boilerplate: Responsive Layout
87 | ============================================================ */
88 | #firechat {
89 | .center {
90 | float: none !important;
91 | margin-left: auto !important;
92 | margin-right: auto !important;
93 | }
94 | .left { float: left !important; }
95 | .right { float: right !important; }
96 | .alignleft { text-align: left !important; }
97 | .alignright { text-align: right !important; }
98 | .aligncenter { text-align: center !important; }
99 | .hidden { display: none !important; }
100 |
101 | .row { clear: both; }
102 |
103 | .fifth,
104 | .fivesixth,
105 | .fourfifth,
106 | .half,
107 | .ninetenth,
108 | .quarter,
109 | .sevententh,
110 | .sixth,
111 | .tenth,
112 | .third,
113 | .threefifth,
114 | .threequarter,
115 | .threetenth,
116 | .twofifth,
117 | .twothird,
118 | .full {
119 | margin-left: 2.127659574468085%;
120 | float: left;
121 | min-height: 1px;
122 | }
123 |
124 | .fifth:first-child,
125 | .fivesixth:first-child,
126 | .fourfifth:first-child,
127 | .half:first-child,
128 | .ninetenth:first-child,
129 | .quarter:first-child,
130 | .sevententh:first-child,
131 | .sixth:first-child,
132 | .tenth:first-child,
133 | .third:first-child,
134 | .threefifth:first-child,
135 | .threequarter:first-child,
136 | .threetenth:first-child,
137 | .twofifth:first-child,
138 | .twothird:first-child,
139 | .full:first-child { margin-left: 0; }
140 |
141 | .tenth { width: 8.08510638297872%; }
142 | .sixth { width: 14.893617021276595%; }
143 | .fifth { width: 18.297872340425535%; }
144 | .quarter { width: 23.404255319148938%; }
145 | .threetenth { width: 26.3829787235% }
146 | .third { width: 31.914893617021278%; }
147 | .twofifth { width: 38.72340425531915%; }
148 | .half { width: 48.93617021276596%; }
149 | .sevententh { width: 58.7234042555% }
150 | .threefifth { width: 59.14893617021278%; }
151 | .twothird { width: 65.95744680851064%; }
152 | .threequarter { width: 74.46808510638297%; }
153 | .ninetenth { width: 74.8936170215% }
154 | .fourfifth { width: 79.57446808510639%; }
155 | .fivesixth { width: 82.9787234042553%; }
156 | .full { width: 100%; }
157 |
158 | .clipped { overflow: hidden; }
159 |
160 | strong { font-weight: bold; }
161 | em { font-style: italic; }
162 | label { display: block; }
163 | a {
164 | color: #005580;
165 | &:visited,
166 | &:hover,
167 | &:active {
168 | color: #005580;
169 | }
170 | }
171 | p { margin: 10px 0; }
172 | h1, h2, h3, h4, h5, h6 {
173 | margin: 10px 0;
174 | font-family: inherit;
175 | font-weight: bold;
176 | line-height: 20px;
177 | color: inherit;
178 | }
179 | h1, h2, h3 {
180 | line-height: 40px;
181 | }
182 | h1 { font-size: 38.5px; }
183 | h2 { font-size: 31.5px; }
184 | h3 { font-size: 24.5px; }
185 | h4 { font-size: 17.5px; }
186 | h5 { font-size: 14px; }
187 | h6 { font-size: 11.9px; }
188 | small { font-size: 90%; }
189 |
190 | color: #333;
191 | text-align: left;
192 | }
193 |
194 | /* Component: Tabs
195 | ============================================================ */
196 | #firechat {
197 | .nav {
198 | list-style: none;
199 | }
200 | .nav > li > a {
201 | display: block;
202 | background-color: #eeeeee;
203 | text-decoration: none;
204 | overflow: hidden;
205 | white-space: nowrap;
206 | &:hover,
207 | &:focus {
208 | background-color: #ffffff;
209 | }
210 | }
211 | .nav-tabs {
212 | border-bottom: 1px solid #ddd;
213 | clear: both;
214 | }
215 | .nav-tabs > li {
216 | float: left;
217 | margin-bottom: -1px;
218 | max-width: 45%;
219 | }
220 | .nav-tabs > li > a {
221 | .border-radii(4px, 0, 0, 4px);
222 | padding: 4px 8px;
223 | margin-right: 2px;
224 | line-height: 20px;
225 | border: 1px solid transparent;
226 | border-color: #cccccc;
227 | }
228 | .nav-tabs > .active > a,
229 | .nav-tabs > .active > a:hover,
230 | .nav-tabs > .active > a:focus {
231 | border-bottom-color: transparent;
232 | background-color: #ffffff;
233 | cursor: default;
234 | }
235 | .tab-content {
236 | overflow: auto;
237 | }
238 | .tab-content > .tab-pane {
239 | display: none;
240 | }
241 | .tab-content > .active {
242 | display: block;
243 | background-color: #ffffff;
244 | }
245 | }
246 |
247 | /* Component: dropdowns
248 | ============================================================ */
249 | #firechat {
250 | .caret {
251 | display: inline-block;
252 | width: 0;
253 | height: 0;
254 | vertical-align: top;
255 | border-top: 4px solid #000000;
256 | border-right: 4px solid transparent;
257 | border-left: 4px solid transparent;
258 | content: "";
259 | margin-top: 8px;
260 | margin-left: 2px;
261 | }
262 | .firechat-dropdown {
263 | position: relative;
264 | }
265 | .firechat-dropdown-toggle {
266 | .user-select(none);
267 | text-decoration: none;
268 | &:focus,
269 | &:active {
270 | outline: none;
271 | text-decoration: none;
272 | }
273 | &.btn {
274 | padding: 4px 0 0;
275 | height: 22px;
276 | }
277 | }
278 | .firechat-dropdown-menu {
279 | .clearfix();
280 | .border-radii(0, 4px, 4px, 0);
281 | z-index: 1000;
282 | display: none;
283 | float: left;
284 | min-width: 98%;
285 | position: absolute;
286 | top: 100%;
287 | left: 0;
288 | width: 100%;
289 | background-color: #ffffff;
290 | -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
291 | -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
292 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
293 | -webkit-background-clip: padding-box;
294 | -moz-background-clip: padding;
295 | background-clip: padding-box;
296 | border: 1px solid #ccc;
297 | min-width: 98%;
298 | padding: 0;
299 | margin: -1px 0 0;
300 | }
301 | .firechat-dropdown-menu ul {
302 | background-color: #ffffff;
303 | list-style: none;
304 | overflow-y: scroll;
305 | max-height: 300px;
306 | }
307 | .firechat-dropdown-menu ul > li > a {
308 | display: block;
309 | padding: 1px 1px 1px 3px;
310 | clear: both;
311 | font-weight: normal;
312 | line-height: 20px;
313 | color: #333333;
314 | white-space: nowrap;
315 | }
316 | .firechat-dropdown-menu ul > li > a.highlight {
317 | background-color: #d9edf7;
318 | }
319 | .firechat-dropdown-menu ul > li > a:hover,
320 | .firechat-dropdown-menu ul > li > a:focus,
321 | .firechat-dropdown-menu ul > .active > a,
322 | .firechat-dropdown-menu ul > .active > a:hover,
323 | .firechat-dropdown-menu ul > .active > a:focus {
324 | text-decoration: none;
325 | color: #000000;
326 | background-color: #d9edf7;
327 | outline: 0;
328 | }
329 | .firechat-dropdown-menu ul > .disabled > a,
330 | .firechat-dropdown-menu ul > .disabled > a:hover,
331 | .firechat-dropdown-menu ul > .disabled > a:focus {
332 | color: #999999;
333 | text-decoration: none;
334 | background-color: transparent;
335 | background-image: none;
336 | cursor: default;
337 | }
338 | .firechat-dropdown-header {
339 | position: relative;
340 | width: 100%;
341 | padding: 10px 0;
342 | background-color: #eeeeee;
343 | border-bottom: 1px solid #cccccc;
344 | }
345 | .firechat-dropdown-footer {
346 | position: relative;
347 | width: 100%;
348 | padding: 10px 0px;
349 | background-color: #eeeeee;
350 | border-top: 1px solid #cccccc;
351 | .box-sizing(border-box);
352 | }
353 | .open {
354 | *z-index: 1000;
355 | }
356 | .open > .firechat-dropdown-menu {
357 | display: block;
358 | border: 1px solid #cccccc;
359 | .border-radii(0, 4px, 4px, 0);
360 | }
361 |
362 | .open > .firechat-dropdown-toggle {
363 | outline: none;
364 | text-decoration: none;
365 | .border-radii(4px, 0, 0, 4px);
366 | }
367 | }
368 |
369 | /* Component: Prompts
370 | ============================================================ */
371 | #firechat {
372 | .prompt-wrapper {
373 | position: absolute;
374 | z-index: 1000;
375 | }
376 | .prompt {
377 | position: absolute;
378 | z-index: 1001;
379 | background-color: #ffffff;
380 | .box-shadow(0 5px 10px rgba(0, 0, 0, 0.45));
381 | }
382 | .prompt-header {
383 | padding: 4px 8px;
384 | font-weight: bold;
385 | background-color: #eeeeee;
386 | border: 1px solid #cccccc;
387 | .border-radii(4px, 0, 0, 4px);
388 | }
389 | .prompt-header a.close {
390 | opacity: 0.6;
391 | font-size: 13px;
392 | margin-top: 2px;
393 | &:hover {
394 | opacity: 0.9;
395 | }
396 | }
397 | .prompt-body {
398 | background-color: #ffffff;
399 | padding: 4px 8px;
400 | border-left: 1px solid #cccccc;
401 | border-right: 1px solid #cccccc;
402 | }
403 | .prompt-footer {
404 | padding: 4px 8px;
405 | background-color: #eeeeee;
406 | border: 1px solid #cccccc;
407 | .border-radii(0, 4px, 4px, 0);
408 | }
409 | .prompt-background {
410 | background-color: #333333;
411 | border: 1px solid #333333;
412 | opacity: 0.8;
413 | z-index: 1000;
414 | height: 100%;
415 | width: 100%;
416 | }
417 | }
418 |
419 | /* Component: Buttons
420 | ============================================================ */
421 |
422 | #firechat .btn {
423 | .user-select(none);
424 | .border-radius(4px);
425 | height: 24px;
426 | display: inline-block;
427 | *display: inline;
428 | *zoom: 1;
429 | padding: 2px 5px;
430 | margin-bottom: 0;
431 | text-align: center;
432 | vertical-align: middle;
433 | cursor: pointer;
434 | color: #333333;
435 | font-size: 12px;
436 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
437 | background-color: #f5f5f5;
438 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
439 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
440 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
441 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
442 | background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
443 | background-repeat: repeat-x;
444 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);
445 | border-color: #e6e6e6 #e6e6e6 #bfbfbf;
446 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
447 | *background-color: #e6e6e6;
448 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
449 | border: 1px solid #cccccc;
450 | *border: 0;
451 | border-bottom-color: #b3b3b3;
452 | *margin-left: .3em;
453 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
454 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
455 | box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
456 | }
457 |
458 | #firechat .btn:hover,
459 | #firechat .btn:focus,
460 | #firechat .btn:active,
461 | #firechat .btn.active,
462 | #firechat .btn.disabled,
463 | #firechat .btn[disabled] {
464 | color: #333333;
465 | background-color: #e6e6e6;
466 | *background-color: #d9d9d9;
467 | outline: 0;
468 | }
469 | #firechat .btn:active,
470 | #firechat .btn.active {
471 | background-color: #cccccc;
472 | }
473 | #firechat .btn:first-child {
474 | *margin-left: 0;
475 | }
476 | #firechat .btn:hover,
477 | #firechat .btn:focus {
478 | color: #333333;
479 | text-decoration: none;
480 | background-position: 0 -15px;
481 | .transition(background-position 0.1s linear);
482 | }
483 | #firechat .btn.active,
484 | #firechat .btn:active {
485 | background-image: none;
486 | outline: 0;
487 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);
488 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);
489 | box-shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);
490 | }
491 | #firechat .btn.disabled,
492 | #firechat .btn[disabled] {
493 | cursor: default;
494 | background-image: none;
495 | opacity: 0.65;
496 | filter: alpha(opacity=65);
497 | }
498 | #firechat .btn.disabled:active,
499 | #firechat .btn[disabled]:active {
500 | .box-shadow(inherit);
501 | background-color: #e6e6e6;
502 | }
503 |
504 | /* Component: Context Menu
505 | ============================================================ */
506 | #firechat {
507 | .contextmenu {
508 | position: fixed;
509 | z-index: 1001;
510 | min-width: 150px;
511 | border: 1px solid #cccccc;
512 | .border-radius(4px);
513 | }
514 | .contextmenu ul {
515 | background-color: #ffffff;
516 | list-style: none;
517 | }
518 | .contextmenu ul > li > a {
519 | display: block;
520 | padding: 3px 10px;
521 | clear: both;
522 | font-weight: normal;
523 | line-height: 20px;
524 | color: #333333;
525 | white-space: nowrap;
526 | }
527 | .contextmenu ul > li > a.highlight {
528 | background-color: #d9edf7;
529 | }
530 | .contextmenu ul > li > a:hover,
531 | .contextmenu ul > li > a:focus {
532 | text-decoration: none;
533 | color: #ffffff;
534 | background-color: #0081c2;
535 | outline: 0;
536 | }
537 | }
538 |
539 | /* Custom Styles
540 | ============================================================ */
541 | #firechat {
542 | padding: 0;
543 | font-family: sans-serif;
544 | font-size: 12px;
545 | line-height: 18px;
546 | input,
547 | textarea {
548 | width: 100%;
549 | font-family: sans-serif;
550 | font-size: 12px;
551 | line-height: 18px;
552 | padding: 2px 5px;
553 | border: 1px solid #cccccc;
554 | .placeholder(#aaaaaa);
555 | .border-radius(1px);
556 | .box-sizing(border-box);
557 | }
558 | input[disabled],
559 | textarea[disabled] {
560 | background-color: #eeeeee;
561 | }
562 | input {
563 | height: 24px;
564 | }
565 | textarea {
566 | resize: none;
567 | height: 40px;
568 | }
569 | .search-wrapper {
570 | .border-radius(15px);
571 | border: 1px solid #cccccc;
572 | margin: 0 5px;
573 | padding: 2px 5px;
574 | background: #ffffff;
575 | }
576 | .search-wrapper > input[type=text] {
577 | padding-left: 0px;
578 | border: none;
579 | &:focus,
580 | &:active {
581 | outline: 0;
582 | }
583 | }
584 | .chat {
585 | overflow: auto;
586 | -ms-overflow-x: hidden;
587 | overflow-x: hidden;
588 | height: 290px;
589 | position: relative;
590 | margin-bottom: 5px;
591 | border: 1px solid #cccccc;
592 | border-top: none;
593 | overflow-y: scroll;
594 | }
595 | .chat textarea {
596 | overflow: auto;
597 | vertical-align: top;
598 | }
599 | .message {
600 | color: #333;
601 | padding: 3px 5px;
602 | border-bottom: 1px solid #ccc;
603 | &.highlighted {
604 | background-color: #d9edf7;
605 | }
606 | }
607 | .message .name {
608 | font-weight: bold;
609 | overflow-x: hidden;
610 | }
611 | .message.message-self {
612 | color: #2675ab;
613 | }
614 | .message:nth-child(odd) {
615 | background-color: #f9f9f9;
616 | &.highlighted {
617 | background-color: #d9edf7;
618 | }
619 | &.message-local {
620 | background-color: #effafc;
621 | }
622 | }
623 | .message-content {
624 | word-wrap: break-word;
625 | padding-right: 45px;
626 | &.red {
627 | color: red;
628 | }
629 | }
630 | .message.message-notification .message-content {
631 | font-style: italic;
632 | }
633 | ul::-webkit-scrollbar {
634 | -webkit-appearance: none;
635 | width: 7px;
636 | }
637 | ul::-webkit-scrollbar-thumb {
638 | border-radius: 4px;
639 | -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5);
640 | }
641 | #firechat-header {
642 | padding: 6px 0 0 0;
643 | height: 40px;
644 | }
645 | #firechat-tabs {
646 | height: 435px;
647 | }
648 | #firechat-tab-list {
649 | background-color: #ffffff;
650 | }
651 | #firechat-tab-content {
652 | width: 100%;
653 | background-color: #ffffff;
654 | }
655 | .tab-pane-menu {
656 | border: 1px solid #ccc;
657 | border-top: none;
658 | vertical-align: middle;
659 | padding-bottom: 5px;
660 | }
661 | .tab-pane-menu .firechat-dropdown {
662 | margin: 5px 0 0 5px;
663 | }
664 | .tab-pane-menu > .icon {
665 | margin: 5px 2px 0;
666 | }
667 | .icon {
668 | display: inline-block;
669 | *margin-right: .3em;
670 | line-height: 20px;
671 | vertical-align: middle;
672 | background-repeat: no-repeat;
673 | padding: 0;
674 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAL8AAAANEAYAAACoeGM7AAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAM80lEQVR42t2cZ4xWRRfHZ3dhwbK7CJZYYiOxV+wlUQzCKmBBsWPvYjQiaiyxYUwULLErltgrNpRF4q5iV0BRLFE0diUi7LIq6Pqw74f//rzO4Zm9d+5z9c37ni//3OfemXtm5sypc58q55xzrrpauHSpcOONheedJ9x9d2Hv3sLvvxfefbfw5puFf/5Zvr+8VFUl7Ozs/rkNNxTef79w222F77wjPOoo4ccfF/O+UDvGzTVEf8wH1zU1wlJJePTRwv33Fx57rPDnn/1+wbzzG+IXvrhmPUPzY/kJEc/HzqslK1fLLy8cMUK4667CFVYQ/vqr8PXXhU884f8eL6eXXKJ2wqSd3tDY2KOHc84NGqSRnnCC7tbVCdvbhRMnasWnTdt5Z+eca2pK6797Yh6ef1641lrCPfYQfv21UNwl62qv115b2Nws/PZb4d57C3/7LftalaOQ/C63nPCII4Tsg6228scHffqp8LHHhPfe6/8eu49feEH44YfCq64S/v67cMGC7tuvu65w6FDhllsKkS/0zoQJaZzMmSO+JaEzZmgkY8b88Ydzzs2b19HhnHNffSWJ6tVLM1cq/fKLrjXiUaPUrn9/XZ9yip6rqVmyxDnnnnrKvPbAA4V6TTJxP/zgT4zdyC+9JOzXz++PgceSVSS1tUJtE+c22si/f+21Pj9Wwd54o//8ZpsJd9xR2KtX9+8vmkLzcsABPt9z5wr32af88xiOf4oaGoSHH17M+/LKg203aJDwiy+EVh5DyHzuuWcxfDn31lvOOffMM7Nn6z1SsZ2dn30m/OQT4eefC7/8UvjBB0LaVzav7D873vfeE6LQISvv3Of50P6OJetgWFp5ZeFzz5V/b1bEEd1333x80g+O4mWXCXEQMDB33CG8/XYhBhuDmMZnOn30kZ5/+23nnGtpefNN55wbP96Xl/nzhYsWCdvauEbeQP1Of6US/XRZfDz8Bx8U9uwpPPFE4eOPC/EMVl9dOG6ccORI4Q03CA87LHbA5RcCwgO47TbhokXCYcOERCKQ7GJiMLiPokeQ2QCjRwtvukkY6zHg0V10kXDAAH++EIz77hM++aRwxRWFq64q3H57v9/+/YVPPy2cOFF47rnCNE8kll/Gi4eIAWU9v/vOnz8MA4qjb18hG51+2JiLF+ebX/hA4U+b5t+/7jrhAw8IkQ88WvnPiZzi4Q0eXL6/dEJhawaGDydmk4uEQmhu1kj79tVImb8RI8TZwIG0p78ddnAubOhDhIcPIXd4nnh4OHYYzPXXF7K/ed5GBLb/NLKRFOtMfyCZAiIKns8aSVp9xP4i8nv//Ti+icjgl0iDeUsjpKDSyJYeqqs1A+3taDBpkn79Qi5YW9vf5wXq0YP+1L5U6loAUjpW4WPhQhN00EFC2SXnDj1UiAF44w2hTWnEEqkcCIWJYkShQNbDIIJZZx2h9Xhs/7EpFDwCNs6UKf584tGQsiFCGTJEuMEG/jxBzBe/H3+8cJddhBiAZ58thl/eM2uW8MgjhWyAyy8XojhRUGuuKayvF7Jhf/pJ+O67wltvFU6dGscv78fTgg45RPjII+XbYZA23bT8/Vtu8ecBDy9MpHS0EROFr1B76lQp7r32su20EwYMkGSOG4dq4I0a4fDh9G9TQN0TKR0iB8aD3G+9tRDHDoPPenKf53GYiABiPWm7f046SUgKDs8aQwxlVfiQTVXh0BChxir+kDol5WPv24im+Ahcb6iu9jVnR4euGX9oXsLj7GKYHP68eUI8ANsRA7WKE8sNkUopdvgJsW0QUBbc3oewl6GQs9LUDgr85JOF5OZHjRKyESZNEp55ppBIC7IbxgoSkQzt2OgXX1wMv9RCFBAuG4HgSREKU0vB42Jjo6gxtPvtJ8TzTM91+kQOf731hHj4KHybA0bRK8B1bpNNhAqkE8OH40CKLZ3I4a+0kq6l8JuapPAbGx991Lm/r5s8+aFDlWOdOVMuS0ODNm5bm1RWSwsSrJY2FZVGRFYYYgwt+wO5EpeJ4eaa+zxPexQ+/Wcl+iFCxeBvs43QRujW048lu09Y16KI/m3Ewu8h/VS55w/5b7AGMrbW5lyXImQh8EhtqGBDGOu525QDnm5RhOcBzZ8vfO01IYIaev/Mmf5zpALsfSiv5bYLfeqpQhSUrYHY92GYbD+shx0XKSOKlrFk1xHFTQRon7M1FNtepaOErwsvFBIqkzKKlQ8MDoTnCpFKwyCi2FZbTYhDgydO6gfaaaesnFC0xQcUUQwlBi6VpPCHDJGKmDyZGWMldY2nPmmSP5NEdlkJRYSCZr7FRbiIzTX3eZ72tiiclZjf444rzyd6hkid9+fNCNhxvfJKvn5C/YVy9nZVYw89ZCdfI6TVEuCLCI4Zhp+Ojq6FIAdLFR0PjpQOFpyFsQMidw5ZxRo7ATaEI6fMgFD8FJsxXCgee00qSiWPJNfKaYLp08u/P1YQ4ZfxWgWDAcOjZmMR8G+3nZBTPbwffhYuFJ5zjhCPKi9ZBUzkh8dpBTkk2KSKLr1UiAIJEYYhK2GQIOto4OGj8NdYQ4hck8tnvkmtQSGDHKK6On8nUNtIPHwUvp05SeSwYTI1s2YpJhk5kliO/uP4Qe6IxMnhk9JhXqyCtQ4Oz9Oe/nwzl05EtJbI5fMeTs8QscIP+zwtEuA+csz+fvjhOH5tf6SSoZCjEipeKw5MVj12PemZ9tXVvunt2dOuHE+K06oqvbG2lp2ikS1ZIknv3burO45lXn+9kKItOfzQwhO6jR0r5PiXPaUQmzNHkMllIyhMMBudmgKnjqxnwnSssooQhWpTLOR6UVwc/4ul1lYhx7rwRBF4PBEEwxLFavhkeTmmN2aMEAG3ghdrqHx149z555efx9DGY37vvFNoPcxQe1v0SyObe8cRIYf/4otCPHwUPsVge4yXWgTEumWl9nZtsPp6irbk8OVyTJ5sYyRJdGOjkitJjUP3FyzwNzIOV1ZivdinFG3J4dt5twqW9eJ5nUFKDIB1xNIIh8p6oijUa64RcnyT2SLSJHWXdtqKcdD/BRcIifBiCf44TIGe2WILYaiGhWHFoaFGxnxtvnlWDvwZY/yLF6toy+8LF2rGamrESXK8WRIwe7YcjKYmzdD06Xpi7lzFWqR4/1pYeywMT5ncL4rpiiuE9tgnIRyeozP9ZyVSBNZyskHxpFFUCGroOCf36Zf28MtzRCqxOXPaM24iJxsJQQgOCgyiqM4GJvduKZRbjOWXXH+fPkKKiUQmOnyYRBrwZeeXCIxxQ/mPSfpEkZn3KZO+7PHiH38U2mKurQHY478ooHSSaZswgWOZum5uFra2iqPOTs0g93EE4NzWAKZM4RioIoDx4/PNE4YQxWNTs6HUgL1Pe3sMNCuF9qOVG2penIPHYOHokcJ8+WVhS4sQR4NaJP3F7luI9tSALBFxovhBW6uonFSORm7mzMEAzJjhnHMNDdLI9fXgq68651xdnZLYffpY+cpIhLzkUNNySd98I7QKFPs0cGC+4dv3sCAYIE4bhXJboWvwrruExxwjJPWStyjD8/b0AyEiCt4Wh6xBpCi+225CG5kUdWogxC/HMyldshGJ7IhcWIfTTxciLxhWqKgcJxuPc/h2nTAAaYrq4IP9dni01jCEiVM3nIdWdaizUxuTDdvaSgQQ6kfPNTVhKPgOgP7j5id0Dh9DbR05Unlch54PfQeQRuyrUM7ZOoxkCq6+WkikhiOBIQ/JEx7sGWfE8QlZOYKyOi7sS+uQgdn3ra/4LT/ZSS1razEEfBgoOf0rdRUaIB4rXyCedpqQFAu5KzxdPyBJPPRlj7d1Tw89JLznHiGeb+hDFVIWIQPll9SS3ynKUUwj5RWbI6Q/inWVEgJsUxRFKdKi+T3rLKEtshf9IZytPdgIgNM75PA5dWQ9fBAFE0+cu+dDGQyAHwGMHi0P/sordT12LB4+Ct//0Cbvh1zWINr9gAePIYe4thGCbU//sYRHn/fDLBwyawhDOfe8ESbvCx3/zHuKJp5Cil8cVlVlxcjXhv5yICvh4aPwreDlJY5nUay1Cj2rINn2lD4qPX6KwcPzRQHhKeP5WeQ+ng2nSyhCcv4fKip1EuIXRQlfXIMUn7nPl9SkhKhFFM2v7QcDEIoAQoiHbxV+5V/uEgGQAuJLXRQ7X/KS0sHD///8cpcIl/P69MN3GDh2RBzUvvjLFVI2NuK1einty+CsZDMY/z0qyuOvkGwIY8+zMuE2ZYGHj8JHUWQlm/sm1ENRZ81dWrShLEVAy1/20F9EEZrTQnk9HJCNYL+YLMrD+F/jF7IbnBQQNQDOi/MFJ9fk8O26xisMQmb7OykaagD+p/RLl3JNDj+U0gn13z0xLhQ0BtEqfLtP7TXP057+YvcDCpvDInwvYInUcnIqqjLKK2/IydlnF8NHfvq3FP9/ANgTCJ9z9ZF7AAAAAElFTkSuQmCC) no-repeat top left;
675 | opacity: 0.3;
676 | font-size: 22px;
677 | font-family: Arial;
678 | font-weight: bold;
679 | overflow: hidden;
680 | &.plus {
681 | margin-top: 0;
682 | vertical-align: top;
683 | background: transparent;
684 | }
685 | &.search {
686 | background-position: 0 0; width: 13px; height: 13px;
687 | }
688 | &.close {
689 | background-position: -120px 0; width: 13px; height: 13px;
690 | }
691 | &.user-chat {
692 | background-position: -138px 0; width: 17px; height: 13px;
693 | }
694 | &.user-group {
695 | background-position: -18px 0; width: 17px; height: 13px;
696 | }
697 | &.user-mute {
698 | background-position: -84px 0; width: 13px; height: 13px;
699 | &.red {
700 | background-position: -102px 0; width: 13px; height: 13px;
701 | }
702 | }
703 | }
704 | .icon:hover,
705 | .btn:hover > .icon {
706 | opacity: 0.6;
707 | }
708 | a > .icon {
709 | margin: 3px 1px;
710 | }
711 | }
712 |
--------------------------------------------------------------------------------
/templates/layout-full.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/layout-popout.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/message-context-menu.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/message.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | <% if (!disableActions) { %>
9 |
13 | <% } %>
14 |
15 |
16 |
17 | <%= message %>
18 |
19 |
--------------------------------------------------------------------------------
/templates/prompt-alert.html:
--------------------------------------------------------------------------------
1 |
2 |
<%- message %>
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/templates/prompt-create-room.html:
--------------------------------------------------------------------------------
1 |
2 |
Give your chat room a name:
3 |
4 |
--------------------------------------------------------------------------------
/templates/prompt-invitation.html:
--------------------------------------------------------------------------------
1 |
2 |
<%- fromUserName %>
3 |
invited you to join
4 |
<%- toRoomName %>
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/templates/prompt-invite-private.html:
--------------------------------------------------------------------------------
1 |
2 |
Invite <%- userName %> to <%- roomName %>?
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/templates/prompt-invite-reply.html:
--------------------------------------------------------------------------------
1 |
2 |
<%- toUserName %>
3 |
4 | <% if (status === 'accepted') { %> accepted your invite. <% } else { %> declined your invite. <% } %>
5 |
6 |
--------------------------------------------------------------------------------
/templates/prompt-user-mute.html:
--------------------------------------------------------------------------------
1 |
2 |
<%- userName %>
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/templates/prompt.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | <%= content %>
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/room-list-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- name %>
4 |
5 |
--------------------------------------------------------------------------------
/templates/room-user-list-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- name %>
4 |
5 | <% if (!disableActions) { %>
6 |
7 |
8 | <% } %>
9 |
10 |
--------------------------------------------------------------------------------
/templates/room-user-search-list-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if (disableActions) { %>
4 | <%- name %>
5 | <% } else { %>
6 | <%- name %>
7 | +
8 | <% } %>
9 |
10 |
--------------------------------------------------------------------------------
/templates/tab-content.html:
--------------------------------------------------------------------------------
1 |
2 |
47 |
48 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/templates/tab-menu-item.html:
--------------------------------------------------------------------------------
1 |
2 | <%- name %>
3 |
--------------------------------------------------------------------------------
/templates/user-search-list-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if (disableActions) { %>
4 | <%- name %>
5 | <% } else { %>
6 | <%- name %>
7 |
8 | <% } %>
9 |
10 |
--------------------------------------------------------------------------------
/website/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Firebase
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # Firechat Website
2 |
3 | ## Initial Setup
4 |
5 | To deploy the Firechat website, first make sure you have Firebase Hosting deploy privileges for the
6 | `firechat` Firebase project.
7 |
8 | Also, make sure you have [Jekyll](https://jekyllrb.com/docs/installation/) and [Rdiscount](https://github.com/davidfstr/rdiscount) installed:
9 |
10 | ```
11 | $ gem install jekyll rdiscount
12 | ```
13 |
14 | ## Recurring Setup
15 |
16 | Once you have deploy privileges and have Jekyll installed, you can deploy the Firechat website by
17 | running the following two commands from this directory:
18 |
19 | ```
20 | $ jekyll build
21 | $ firebase deploy
22 | ```
23 |
--------------------------------------------------------------------------------
/website/_config.yml:
--------------------------------------------------------------------------------
1 | safe: true
2 | lsi: false
3 | highlighter: true
4 | markdown: rdiscount
5 |
--------------------------------------------------------------------------------
/website/_layouts/docs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Firechat - Documentation
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
39 |
40 |
41 |
42 |
53 |
54 |
55 | {{ content }}
56 | Build something cool using Firechat? We'd
57 | love to
hear from you!
58 |
59 |
60 |
61 |
62 |
63 |
64 |
77 |
78 |
79 |
80 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/website/css/pygments-borland.css:
--------------------------------------------------------------------------------
1 | .hll { background-color: #ffffcc }
2 | .c { color: #008800; font-style: italic } /* Comment */
3 | .err { color: #a61717; background-color: #e3d2d2 } /* Error */
4 | .k { color: #000080; font-weight: bold } /* Keyword */
5 | .cm { color: #008800; font-style: italic } /* Comment.Multiline */
6 | .cp { color: #008080 } /* Comment.Preproc */
7 | .c1 { color: #008800; font-style: italic } /* Comment.Single */
8 | .cs { color: #008800; font-weight: bold } /* Comment.Special */
9 | .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
10 | .ge { font-style: italic } /* Generic.Emph */
11 | .gr { color: #aa0000 } /* Generic.Error */
12 | .gh { color: #999999 } /* Generic.Heading */
13 | .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
14 | .go { color: #888888 } /* Generic.Output */
15 | .gp { color: #555555 } /* Generic.Prompt */
16 | .gs { font-weight: bold } /* Generic.Strong */
17 | .gu { color: #aaaaaa } /* Generic.Subheading */
18 | .gt { color: #aa0000 } /* Generic.Traceback */
19 | .kc { color: #000080; font-weight: bold } /* Keyword.Constant */
20 | .kd { color: #000080; font-weight: bold } /* Keyword.Declaration */
21 | .kn { color: #000080; font-weight: bold } /* Keyword.Namespace */
22 | .kp { color: #000080; font-weight: bold } /* Keyword.Pseudo */
23 | .kr { color: #000080; font-weight: bold } /* Keyword.Reserved */
24 | .kt { color: #000080; font-weight: bold } /* Keyword.Type */
25 | .m { color: #0000FF } /* Literal.Number */
26 | .s { color: #0000FF } /* Literal.String */
27 | .na { color: #FF0000 } /* Name.Attribute */
28 | .nt { color: #000080; font-weight: bold } /* Name.Tag */
29 | .ow { font-weight: bold } /* Operator.Word */
30 | .w { color: #bbbbbb } /* Text.Whitespace */
31 | .mf { color: #0000FF } /* Literal.Number.Float */
32 | .mh { color: #0000FF } /* Literal.Number.Hex */
33 | .mi { color: #0000FF } /* Literal.Number.Integer */
34 | .mo { color: #0000FF } /* Literal.Number.Oct */
35 | .sb { color: #0000FF } /* Literal.String.Backtick */
36 | .sc { color: #800080 } /* Literal.String.Char */
37 | .sd { color: #0000FF } /* Literal.String.Doc */
38 | .s2 { color: #0000FF } /* Literal.String.Double */
39 | .se { color: #0000FF } /* Literal.String.Escape */
40 | .sh { color: #0000FF } /* Literal.String.Heredoc */
41 | .si { color: #0000FF } /* Literal.String.Interpol */
42 | .sx { color: #0000FF } /* Literal.String.Other */
43 | .sr { color: #0000FF } /* Literal.String.Regex */
44 | .s1 { color: #0000FF } /* Literal.String.Single */
45 | .ss { color: #0000FF } /* Literal.String.Symbol */
46 | .il { color: #0000FF } /* Literal.Number.Integer.Long */
47 |
--------------------------------------------------------------------------------
/website/css/styles.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Satisfy|Lato:300,700,300italic,700italic);
2 |
3 | /* Global
4 | -------------------------------------------------- */
5 | body {
6 | font: 14.5px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif;
7 | color:#777;
8 | font-weight:300;
9 | }
10 |
11 | a {
12 | color: #08c;
13 | }
14 |
15 | a:hover {
16 | text-decoration: underline;
17 | color: #004f77;
18 | }
19 |
20 | img.fork-on-github {
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | border: 0;
25 | width: 149px;
26 | height: 149px;
27 | z-index: 1;
28 | }
29 |
30 | .strong { font-weight: bold; }
31 |
32 | .satisfy { font-family: 'Satisfy', cursive; }
33 |
34 | /* Global: Header
35 | -------------------------------------------------- */
36 | header {
37 | position: relative;
38 | min-height: 150px;
39 | width: 100%;
40 | color: white;
41 | background-color: #ff8b00;
42 | }
43 |
44 | .header-title {
45 | background-color: #d7690d;
46 | }
47 |
48 | .header-content {
49 | background-color: #ff8b00;
50 | }
51 |
52 | .header-links {
53 | padding-top: 15px;
54 | text-align: right;
55 | font-size: 15px;
56 | }
57 |
58 | .header-links a {
59 | display: inline-block;
60 | margin: 0 15px;
61 | text-decoration: none;
62 | color: #fff;
63 | }
64 |
65 | .header-links a:hover {
66 | text-decoration: underline;
67 | color: #feffff;
68 | }
69 |
70 | .header-links a.selected {
71 | text-decoration: underline;
72 | /* color: #fff; */
73 | color: #feffff;
74 | cursor: default;
75 | }
76 |
77 | #download-on-github {
78 | float: right;
79 | font-size: 24px;
80 | padding: 10px 40px;
81 | margin: 35px 0;
82 | border-bottom: 6px solid #ccc;
83 | }
84 |
85 | #top-shadow {
86 | position: absolute;
87 | width: 100%;
88 | bottom: 0;
89 | height: 60px;
90 | background: url('../images/top-shadow.png') no-repeat center;
91 | -webkit-background-size: cover;
92 | -moz-background-size: cover;
93 | -o-background-size: cover;
94 | background-size: cover;
95 | }
96 |
97 | #title-small {
98 | color: #fff;
99 | position: absolute;
100 | left: 150px;
101 | top: 15px;
102 | width: 126px;
103 | height: 42px;
104 | font-size: 34px;
105 | }
106 |
107 | #page-title {
108 | position: absolute;
109 | left: 150px;
110 | top: 60px;
111 | font-size: 60px;
112 | font-weight: bold;
113 | line-height: 1.2em;
114 | }
115 |
116 | #page-links {
117 | position: absolute;
118 | left: 90px;
119 | top: 155px;
120 | font-size: 18px;
121 | }
122 |
123 | #page-links a {
124 | color: #fff;
125 | }
126 |
127 | /* Firechat Demo
128 | -------------------------------------------------- */
129 | #firechat-container {
130 | height: 475px;
131 | max-width: 325px;
132 | padding: 10px;
133 | border: 1px solid #ccc;
134 | background-color: #fff;
135 | margin: auto auto;
136 | text-align: center;
137 | -webkit-border-radius: 4px;
138 | -moz-border-radius: 4px;
139 | border-radius: 4px;
140 | -webkit-box-shadow: 0 5px 25px #666;
141 | -moz-box-shadow: 0 5px 25px #666;
142 | box-shadow: 0 5px 25px #666;
143 | }
144 |
145 | #auth-modal {
146 | width: 30%;
147 | margin-left: -15%;
148 | left: 50%;
149 | }
150 |
151 | #user-info {
152 | display: block;
153 | margin-top: 10px;
154 | }
155 |
156 | #user-info a {
157 | color: #fff;
158 | text-decoration: underline;
159 | }
160 |
161 | /* Social Buttons
162 | -------------------------------------------------- */
163 | .social-container {
164 | padding: 10px 0;
165 | color: #5e5e5e;
166 | text-align: center;
167 | border-bottom: 1px solid #e0e0e0;
168 | background-color: #f7f7f7;
169 | }
170 | .social-buttons {
171 | margin-top: 2px;
172 | margin-left: 0;
173 | margin-bottom: 0;
174 | padding-left: 0;
175 | list-style: none;
176 | }
177 | .social-buttons li {
178 | display: inline-block;
179 | padding: 6px 8px 5px;
180 | line-height: 1;
181 | *display: inline;
182 | *zoom: 1;
183 | }
184 |
185 | /************************************************/
186 | /* Stuff below-the-fold (the white part) */
187 | /************************************************/
188 | #bottom-container {
189 | border: 1px solid #e2e2e2;
190 | border-top: 0;
191 | background-color: white;
192 | margin: 0 auto;
193 | z-index: 1;
194 | -webkit-box-shadow: 0 0 6px 1px #bbb;
195 | -moz-box-shadow: 0 0 6px 1px #bbb;
196 | box-shadow: 0 0 6px 1px #bbb;
197 | }
198 |
199 | .bottom-content {
200 | padding: 30px 0px 0;
201 | color: #5e5e5e;
202 | margin-bottom: 40px;
203 | }
204 |
205 | .bottom-content.separator {
206 | margin-top: 40px;
207 | border-top: 1px solid #e0e0e0;
208 | }
209 |
210 | .build-something {
211 | margin: auto;
212 | margin-top: 30px;
213 | font-style: italic;
214 | text-align: center;
215 | }
216 |
217 | /* Footer
218 | -------------------------------------------------- */
219 | footer {
220 | border-top: 1px solid #ccc;
221 | background-color: #f4f4f4;
222 | margin-top: 50px;
223 | height: 200px;
224 | margin: 0 auto;
225 | position: relative;
226 | }
227 |
228 | #footer-links {
229 | padding-top: 24px;
230 | text-align: center;
231 | }
232 |
233 | #powered-by-firebase {
234 | display: inline-block;
235 | width: 200px;
236 | }
237 |
238 | #footer-links a {
239 | font-size: 14px;
240 | color: #999;
241 | text-decoration: none;
242 | margin: 0 20px;
243 | display: inline-block;
244 | }
245 |
246 | #footer-links a:hover {
247 | text-decoration: underline;
248 | color: #333;
249 | }
250 |
251 |
252 | /* index.html
253 | -------------------------------------------------- */
254 | .home-page {
255 | background-color: #f4f4f4;
256 | }
257 |
258 | .home-page #top-content {
259 | height: 925px;
260 | padding-bottom: 35px;
261 | }
262 |
263 | .home-page footer {
264 | border-top: none;
265 | }
266 |
267 | #home-title {
268 | margin: 45px auto -15px auto;
269 | font-size: 72px;
270 | text-align: center;
271 | }
272 |
273 | #home-subtitle {
274 | font-size: 36px;
275 | text-align: center;
276 | margin-bottom: 60px;
277 | font-weight: 100;
278 | }
279 |
280 | #home-download-on-github {
281 | font-size: 24px;
282 | padding: 25px 55px 19px;
283 | margin: -40px auto 35px;
284 | border-bottom: 6px solid #ccc;
285 | }
286 |
287 | #code-container {
288 | width: 600px;
289 | margin: 22px auto;
290 | }
291 |
292 | #code-heading {
293 | text-align: left;
294 | font-size: 22px;
295 | margin-bottom: 2px;
296 | }
297 |
298 | #code-heading a {
299 | color: #f9e701;
300 | text-decoration: underline;
301 | }
302 |
303 | #code-container pre {
304 | background-color: rgba(255, 255, 255, 0.3);
305 | border: 1px solid rgba(0, 0, 0, 0.3);
306 | margin: 0;
307 | padding: 20px;
308 | text-align: left;
309 | font-size: 14px;
310 | color: #000;
311 | line-height: 20px;
312 | font-family: monospace;
313 | -webkit-border-radius: 4px;
314 | -moz-border-radius: 4px;
315 | border-radius: 4px;
316 | }
317 |
318 | #code-examples-link {
319 | display: block;
320 | float: right;
321 | margin-top: 5px;
322 | font-size: 17px;
323 | color: #fff;
324 | }
325 |
326 | .bottom-title {
327 | font-size: 42px;
328 | color: #333;
329 | text-align: center;
330 | padding: 30px 0 0;
331 | }
332 |
333 | /* Docs
334 | -------------------------------------------------- */
335 | .docs-page {
336 | background: white;
337 | }
338 | .docs-content {
339 | color: #5e5e5e;
340 | margin-bottom: 40px;
341 | }
342 | .docs-page pre {
343 | margin: 20px 10px;
344 | padding: 15px 10px;
345 | }
346 | .docs-separator {
347 | height: 0;
348 | margin-top: 40px;
349 | margin-bottom: 30px;
350 | border-top: 1px solid #e0e0e0;
351 | }
352 | .docs-content p {
353 | margin: 0 0 10px;
354 | }
355 |
356 | .docs-content h1 {
357 | margin: 20px 0 10px;
358 | }
359 | .docs-content h2 {
360 | margin: 20px 0 10px;
361 | }
362 | .docs-content h3 {
363 | margin: 20px 0 10px;
364 | }
365 | .docs-content h4 {
366 | margin: 20px 0 10px;
367 | }
368 | .docs-content a {
369 | color: #eb8717;
370 | }
371 | .docs-content .emphasis-box {
372 | border: 1px solid #e5a165;
373 | background: #ffbf86;
374 | padding: 10px;
375 | color: #000;
376 | font-size: 16px;
377 | text-align: center;
378 | }
379 | .docs-content .emphasis-box a {
380 | color: #dc4700;
381 | }
382 | .error-page #top {
383 | height: 200px;
384 | }
385 |
386 |
387 | /* Sidenav for Docs
388 | -------------------------------------------------- */
389 | .sidenav {
390 | width: 220px;
391 | margin: 35px 0 0;
392 | padding: 0;
393 | background-color: #fcfcfc;
394 | -webkit-border-radius: 6px;
395 | -moz-border-radius: 6px;
396 | border-radius: 6px;
397 | -webkit-box-shadow: 0 1px 4px rgba(0,0,0,.065);
398 | -moz-box-shadow: 0 1px 4px rgba(0,0,0,.065);
399 | box-shadow: 0 1px 4px rgba(0,0,0,.065);
400 | }
401 | .sidenav > li > a {
402 | display: block;
403 | width: 190px \9;
404 | margin: 0 0 -1px;
405 | padding: 8px 14px;
406 | border: 1px solid #e5e5e5;
407 | }
408 | .sidenav > li:first-child > a {
409 | -webkit-border-radius: 6px 6px 0 0;
410 | -moz-border-radius: 6px 6px 0 0;
411 | border-radius: 6px 6px 0 0;
412 | }
413 | .sidenav > li:last-child > a {
414 | -webkit-border-radius: 0 0 6px 6px;
415 | -moz-border-radius: 0 0 6px 6px;
416 | border-radius: 0 0 6px 6px;
417 | }
418 | .sidenav > .active > a {
419 | position: relative;
420 | z-index: 2;
421 | padding: 9px 15px;
422 | border: 0;
423 | text-shadow: 0 1px 0 rgba(0,0,0,.15);
424 | -webkit-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1);
425 | -moz-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1);
426 | box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1);
427 | }
428 | .sidenav .icon-chevron-right {
429 | float: right;
430 | margin-top: 2px;
431 | margin-right: -6px;
432 | opacity: .25;
433 | }
434 | .sidenav > li > a:hover {
435 | background-color: #f5f5f5;
436 | }
437 | .sidenav a:hover .icon-chevron-right {
438 | opacity: .5;
439 | }
440 | .sidenav .active .icon-chevron-right,
441 | .sidenav .active a:hover .icon-chevron-right {
442 | background-image: url(//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/bootstrap.css);
443 | opacity: 1;
444 | }
445 | .sidenav.affix {
446 | top: 40px;
447 | }
448 | .sidenav.affix-bottom {
449 | position: absolute;
450 | top: auto;
451 | bottom: 270px;
452 | }
453 |
454 |
455 | /* Responsive
456 | -------------------------------------------------- */
457 |
458 | /* Desktop large
459 | ------------------------- */
460 | @media (min-width: 1200px) {
461 | .sidenav {
462 | width: 258px;
463 | }
464 | .sidenav > li > a {
465 | width: 230px \9;
466 | }
467 | }
468 |
469 | /* Desktop
470 | ------------------------- */
471 | @media (max-width: 980px) {
472 | .sidenav {
473 | top: 0;
474 | width: 218px;
475 | margin-right: 0;
476 | }
477 | }
478 |
479 | /* Tablet to desktop
480 | ------------------------- */
481 | @media (min-width: 768px) and (max-width: 979px) {
482 | .home-page #top-content {
483 | height: 925px;
484 | }
485 | .sidenav {
486 | width: 166px;
487 | }
488 | .sidenav.affix {
489 | top: 0;
490 | }
491 | }
492 |
493 | /* Tablet
494 | ------------------------- */
495 | @media (min-width: 481px) and (max-width: 767px) {
496 | body {
497 | padding: 0;
498 | }
499 | .home-page #top-content {
500 | height: 975px;
501 | }
502 | .sidenav {
503 | width: auto;
504 | margin-bottom: 20px;
505 | }
506 | .sidenav.affix {
507 | position: static;
508 | width: auto;
509 | top: 0;
510 | }
511 | #docs-container {
512 | padding: 0 15px;
513 | }
514 | .bottom-content {
515 | padding: 0 15px;
516 | }
517 | }
518 |
519 | /* Landscape phones
520 | ------------------------- */
521 | @media (max-width: 480px) {
522 | body {
523 | padding: 0;
524 | }
525 | .home-page #top-content {
526 | height: 985px;
527 | }
528 | .header-links {
529 | text-align: center;
530 | }
531 | img.fork-on-github {
532 | display: none;
533 | }
534 | .sidenav {
535 | width: auto;
536 | margin-bottom: 20px;
537 | }
538 | .sidenav.affix {
539 | position: static;
540 | width: auto;
541 | top: 0;
542 | }
543 | #docs-container {
544 | padding: 0 15px;
545 | }
546 | .social-buttons li {
547 | padding: 6px 0px 5px;
548 | }
549 | .bottom-content {
550 | padding: 0 15px;
551 | }
552 | }
553 |
--------------------------------------------------------------------------------
/website/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: docs
3 | ---
4 |
5 |
6 | ### Overview
7 |
8 | Firechat is a simple, extensible chat widget powered by
9 | [Firebase](https://firebase.google.com/?utm_source=firechat).
10 |
11 | It is intended to serve as a concise, documented foundation for chat products built on Firebase. It
12 | works out of the box, and is easily extended.
13 |
14 |
15 |
16 | ### Getting Started
17 |
18 | Firechat works out of the box, provided that you include the correct dependencies in your
19 | application, and configure it to use your Firebase account.
20 |
21 |
22 | #### Downloading Firechat
23 |
24 | In order to use Firechat in your project, you need to include the following files in your HTML:
25 |
26 | {% highlight html %}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {% endhighlight %}
37 |
38 | You can also install Firechat via npm or Bower and its dependencies will be downloaded
39 | automatically:
40 |
41 | ```bash
42 | $ npm install firechat --save
43 | ```
44 |
45 | ```bash
46 | $ bower install firechat --save
47 | ```
48 |
49 | #### Getting Started with Firebase
50 |
51 | Firechat requires Firebase in order to authenticate users and store data. You can
52 | [sign up here](https://console.firebase.google.com/?utm_source=firechat) for a free account.
53 |
54 |
55 | #### Short Example
56 |
57 | ***Firechat requires an authenticated Firebase reference***. Firebase supports authentication with either your own custom authentication system or a number of built-in providers (more on this below).
58 |
59 | Let's put it all together, using Twitter authentication in our example:
60 |
61 | {% highlight html %}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | {% endhighlight %}
112 |
113 |
114 |
115 | ### Authentication
116 |
117 | Firechat uses [Firebase Authentication](https://firebase.google.com/docs/auth/?utm_source=firechat)
118 | and the [Database Security Rules](https://firebase.google.com/docs/database/security/?utm_source=firechat),
119 | giving you the flexibility to authenticate with either your own custom authentication system or a
120 | number of built-in providers.
121 |
122 | #### Integrate Your Own Authentication
123 |
124 | If you already have authentication built into your application, you can integrate it with Firebase
125 | by generating your own JSON Web Tokens (JWT). You can learn how to generate these tokens in our
126 | [custom token documentation](https://firebase.google.com/docs/auth/server/create-custom-tokens/?utm_source=firechat).
127 |
128 | After generating the custom token, authenticate the Firebase SDK with it:
129 |
130 | {% highlight javascript %}
131 | firebase.auth().onAuthStateChanged(function(user) {
132 | // Once authenticated, instantiate Firechat with the logged in user
133 | if (user) {
134 | initChat(user);
135 | }
136 | });
137 |
138 | firebase.auth().signInWithCustomToken().catch(function(error) {
139 | console.log("Error authenticating user:", error);
140 | });
141 | {% endhighlight %}
142 |
143 | #### Delegate Authentication to Firebase
144 |
145 | Firebase has a built-in service that allows you to authenticate with
146 | [Facebook](https://firebase.google.com/docs/auth/web/facebook-login?utm_source=firechat),
147 | [Twitter](https://firebase.google.com/docs/auth/web/twitter-login/?utm_source=firechat),
148 | [GitHub](https://firebase.google.com/docs/auth/web/github-auth/?utm_source=firechat),
149 | [Google](https://firebase.google.com/docs/auth/web/google-signin/?utm_source=firechat), or
150 | [email / password](https://firebase.google.com/docs/auth/web/password-auth/?utm_source=firechat)
151 | using only client-side code.
152 |
153 | * To begin, enable your provider of choice in your Firebase console. Social login services may require you to create and configure an application and an authorized origin for the request.
154 |
155 | * Then authenticate the user on the client using your provider of choice:
156 |
157 | {% highlight javascript %}
158 | firebase.auth().onAuthStateChanged(function(user) {
159 | // Once authenticated, instantiate Firechat with the logged in user
160 | if (user) {
161 | initChat(user);
162 | }
163 | });
164 |
165 | // Log the user in via Twitter (or Google or GitHub or email / password or etc.)
166 | var provider = new firebase.auth.TwitterAuthProvider();
167 | firebase.auth().signInWithPopup(provider).catch(function(error) {
168 | console.log("Error authenticating user:", error);
169 | });
170 | {% endhighlight %}
171 |
172 | For more information, check out the documentation for
173 | [Firebase Authentication](https://firebase.google.com/docs/auth/?utm_source=firechat).
174 |
175 |
176 |
177 | ### Customizing Firechat
178 |
179 | Dive into the Firechat code to tweak the default interface or add a new one, change behavior or add new functionality.
180 |
181 | #### Code Structure
182 |
183 | * **`firechat.js`** is a conduit for data actions and bindings, allowing you to do things like enter or exit chat rooms, send and receive messages, create rooms and invite users to chat rooms, etc. Its sole dependency is Firebase.
184 |
185 | * **`firechat-ui.js`** is a full-fledged chat interface that demonstrates hooking into `firechat.js` and exposes a rich set of functionality to end users out-of-the-box.
186 |
187 | * **`rules.json`** defines a rule structure that maps to the data structure defined in `firechat.js`, defining both the data structure requirements and which users may read or write to which locations in Firebase. When uploaded to your Firebase, this configuration offers robust security to ensure that only properly authenticated users may chat, and neither user data nor private chat messages can be compromised.
188 |
189 | #### Modifying the Default UI
190 |
191 | The default Firechat UI is built using jQuery and Underscore.js, as well as Bootstrap for some styles and UI elements. To get started making changes, see `firechat.js` and `styles.less` to begin modifying the look and feel of the UI.
192 |
193 | When you're ready to build, simply execute `grunt` from the root directory of the project to compile your code into the combined output.
194 |
195 | #### Building a New UI
196 |
197 | To get started with a new UI layer, create a directory for your new interface under the `layouts` directory, using the name of your new interface.
198 |
199 | Next, create a primary JavaScript interface for your UI using the name `firechat-ui.js`, and add styles, layouts, and templates following the same convention as the default layout.
200 |
201 | Lastly, begin hooking into the Firechat API, detailed below, using the exposed methods and defined bindings.
202 |
203 | Missing something? Send us a
pull request and contribute to the repository!
204 |
205 |
206 |
207 | ### Firechat API
208 |
209 | Firechat exposes a number of useful methods and bindings to initiate chat, enter and exit chat rooms, send invitations, create chat rooms, and send messages.
210 |
211 | #### Instantiating Firechat
212 | {% highlight javascript %}
213 | var firebaseRef = firebase.database().ref("firechat");
214 | var chat = new Firechat(firebaseRef);
215 | chat.setUser(userId, userName, function(user) {
216 | chat.resumeSession();
217 | });
218 | {% endhighlight %}
219 |
220 |
221 |
222 | #### API - Public Methods
223 |
224 | `new Firechat(ref, options)`
225 |
226 | > Creates a new instance of Firechat. `ref` is a Firebase Database reference. `options` is a
227 | > configuration object. The only available option is `numMaxMessages` which overrides the default
228 | > number of messages shown in each chat room.
229 |
230 | `Firechat.setUser(userId, userName, onComplete)`
231 |
232 | > Initiates the authenticated connection to Firebase, loads any user metadata,
233 | > and initializes Firebase listeners for chat events.
234 |
235 | `Firechat.resumeSession()`
236 |
237 | > Automatically re-enters any chat rooms that the user was previously in, if the
238 | > user has history saved.
239 |
240 | `Firechat.on(eventType, callback)`
241 |
242 | > Sets up a binding for the specified event type (*string*), for which the
243 | > callback will be invoked. See [API - Exposed Bindings](#api_bindings)
244 | > for more information.
245 |
246 | `Firechat.createRoom(roomName, roomType, callback(roomId))`
247 |
248 | > Creates a new room with the given name (*string*) and type (*string* - `public` or `private`) and invokes the callback with the room ID on completion.
249 |
250 | `Firechat.enterRoom(roomId)`
251 |
252 | > Enters the chat room with the specified id. On success, all methods bound to the `room-enter` event will be invoked.
253 |
254 | `Firechat.leaveRoom(roomId)`
255 |
256 | > Leaves the chat room with the specified id. On success, all methods bound to the `room-exit` event will be invoked.
257 |
258 | `Firechat.sendMessage(roomId, messageContent, messageType='default', callback)`
259 |
260 | > Sends the message content to the room with the specified id and invokes the callback on completion.
261 |
262 | `Firechat.toggleUserMute(userId, callback)`
263 |
264 | > Mute or unmute a given user by id.
265 |
266 | `Firechat.inviteUser(userId, roomId)`
267 |
268 | > Invite a the specified user to the specific chat room.
269 |
270 | `Firechat.acceptInvite(inviteId, callback)`
271 |
272 | > Accept the specified invite, join the relevant chat room, and notify the user who sent it.
273 |
274 | `Firechat.declineInvite(inviteId, callback)`
275 |
276 | > Decline the specified invite and notify the user who sent it.
277 |
278 | `Firechat.getRoomList(callback)`
279 |
280 | > Fetch the list of all chat rooms.
281 |
282 | `Firechat.getUsersByRoom(roomId, [limit=100], callback)`
283 |
284 | > Fetch the list of users in the specified chat room, with an optional limit.
285 |
286 | `Firechat.getUsersByPrefix(prefix, startAt, endAt, limit, callback)`
287 |
288 | > Fetch the list of all active users, starting with the specified prefix, optionally between the specified startAt and endAt values, up to the optional, specified limit.
289 |
290 | `Firechat.getRoom(roomId, callback)`
291 |
292 | > Fetch the metadata for the specified chat room.
293 |
294 |
295 |
296 | #### API - Exposed Bindings
297 |
298 | To bind events to Firechat, invoke the public `on` method using an event ID and callback function. Public bindings are detailed below:
299 |
300 | > Supported event types include:
301 |
302 | > * `user-update` - Invoked when the user's metadata changes.
303 | > * `room-enter` - Invoked when the user successfully enters a room.
304 | > * `room-exit` - Invoked when the user exists a room.
305 | > * `message-add` - Invoked when a new message is received.
306 | > * `message-remove` - Invoked when a message is deleted.
307 | > * `room-invite` - Invoked when a new room invite is received.
308 | > * `room-invite-response` - Invoked when a response to a previous invite is received.
309 |
310 |
311 |
312 | ### Data Structure
313 |
314 | Firechat uses [Firebase](https://firebase.google.com/?utm_source=firechat) to authenticate users and store and synchronize data. This means (a) you don't need to run any server code and (b) you get access to all the the Firebase features, including first-class data security, automatic scaling, and data portability.
315 |
316 | You own all of the data and can interact with it in a variety of ways. Firechat stores your data at the Firebase location you specify using the
317 | following data structure:
318 |
319 | * `moderators/`
320 | * `` - A list of user ids and their moderator status.
321 | * `true|false` - A boolean value indicating the user's moderator status.
322 | * `room-messages/`
323 | * `
324 | * `
325 | * `userId` - The id of the user that sent the message.
326 | * `name` - The name of the user that sent the message.
327 | * `message` - The content of the message.
328 | * `timestamp` - The time at which the message was sent.
329 | * `room-metadata/`
330 | * ``
331 | * `createdAt` - The time at which the room was created.
332 | * `createdByUserId`- The id of the user that created the room.
333 | * `id` - The id of the room.
334 | * `name` - The public display name of the room.
335 | * `type` - The type of room, `public` or `private`.
336 | * `room-users/`
337 | * `user-names-online/`
338 | * `users/`
339 | * `
340 | * `id` - The id of the user.
341 | * `name` - The display name of the user.
342 | * `invites` - A list of invites the user has received.
343 | * `muted` - A list of user ids currently muted by the user.
344 | * `rooms` - A list of currently active rooms, used for sessioning.
345 |
346 | You may find it useful to interact directly with the Firebase data when building related features on your site. See the code or view the data (just enter your Firebase URL in a browser) for more details.
347 |
348 | ### Security
349 | To lock down your Firechat data, you can use Firebase's built-in
350 | [Database Security Rules](https://firebase.google.com/docs/database/security/?utm_source=firechat).
351 | For some example Security Rules for Firechat, see these
352 | [example rules on GitHub](https://github.com/firebase/firechat/tree/master/rules.json).
353 |
--------------------------------------------------------------------------------
/website/docs/public/fonts/aller-bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/aller-bold.eot
--------------------------------------------------------------------------------
/website/docs/public/fonts/aller-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/aller-bold.ttf
--------------------------------------------------------------------------------
/website/docs/public/fonts/aller-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/aller-bold.woff
--------------------------------------------------------------------------------
/website/docs/public/fonts/aller-light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/aller-light.eot
--------------------------------------------------------------------------------
/website/docs/public/fonts/aller-light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/aller-light.ttf
--------------------------------------------------------------------------------
/website/docs/public/fonts/aller-light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/aller-light.woff
--------------------------------------------------------------------------------
/website/docs/public/fonts/novecento-bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/novecento-bold.eot
--------------------------------------------------------------------------------
/website/docs/public/fonts/novecento-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/novecento-bold.ttf
--------------------------------------------------------------------------------
/website/docs/public/fonts/novecento-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/docs/public/fonts/novecento-bold.woff
--------------------------------------------------------------------------------
/website/docs/public/stylesheets/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */
2 |
3 | /* ==========================================================================
4 | HTML5 display definitions
5 | ========================================================================== */
6 |
7 | /*
8 | * Corrects `block` display not defined in IE 8/9.
9 | */
10 |
11 | article,
12 | aside,
13 | details,
14 | figcaption,
15 | figure,
16 | footer,
17 | header,
18 | hgroup,
19 | nav,
20 | section,
21 | summary {
22 | display: block;
23 | }
24 |
25 | /*
26 | * Corrects `inline-block` display not defined in IE 8/9.
27 | */
28 |
29 | audio,
30 | canvas,
31 | video {
32 | display: inline-block;
33 | }
34 |
35 | /*
36 | * Prevents modern browsers from displaying `audio` without controls.
37 | * Remove excess height in iOS 5 devices.
38 | */
39 |
40 | audio:not([controls]) {
41 | display: none;
42 | height: 0;
43 | }
44 |
45 | /*
46 | * Addresses styling for `hidden` attribute not present in IE 8/9.
47 | */
48 |
49 | [hidden] {
50 | display: none;
51 | }
52 |
53 | /* ==========================================================================
54 | Base
55 | ========================================================================== */
56 |
57 | /*
58 | * 1. Sets default font family to sans-serif.
59 | * 2. Prevents iOS text size adjust after orientation change, without disabling
60 | * user zoom.
61 | */
62 |
63 | html {
64 | font-family: sans-serif; /* 1 */
65 | -webkit-text-size-adjust: 100%; /* 2 */
66 | -ms-text-size-adjust: 100%; /* 2 */
67 | }
68 |
69 | /*
70 | * Removes default margin.
71 | */
72 |
73 | body {
74 | margin: 0;
75 | }
76 |
77 | /* ==========================================================================
78 | Links
79 | ========================================================================== */
80 |
81 | /*
82 | * Addresses `outline` inconsistency between Chrome and other browsers.
83 | */
84 |
85 | a:focus {
86 | outline: thin dotted;
87 | }
88 |
89 | /*
90 | * Improves readability when focused and also mouse hovered in all browsers.
91 | */
92 |
93 | a:active,
94 | a:hover {
95 | outline: 0;
96 | }
97 |
98 | /* ==========================================================================
99 | Typography
100 | ========================================================================== */
101 |
102 | /*
103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
104 | * Safari 5, and Chrome.
105 | */
106 |
107 | h1 {
108 | font-size: 2em;
109 | }
110 |
111 | /*
112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome.
113 | */
114 |
115 | abbr[title] {
116 | border-bottom: 1px dotted;
117 | }
118 |
119 | /*
120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
121 | */
122 |
123 | b,
124 | strong {
125 | font-weight: bold;
126 | }
127 |
128 | /*
129 | * Addresses styling not present in Safari 5 and Chrome.
130 | */
131 |
132 | dfn {
133 | font-style: italic;
134 | }
135 |
136 | /*
137 | * Addresses styling not present in IE 8/9.
138 | */
139 |
140 | mark {
141 | background: #ff0;
142 | color: #000;
143 | }
144 |
145 |
146 | /*
147 | * Corrects font family set oddly in Safari 5 and Chrome.
148 | */
149 |
150 | code,
151 | kbd,
152 | pre,
153 | samp {
154 | font-family: monospace, serif;
155 | font-size: 1em;
156 | }
157 |
158 | /*
159 | * Improves readability of pre-formatted text in all browsers.
160 | */
161 |
162 | pre {
163 | white-space: pre;
164 | white-space: pre-wrap;
165 | word-wrap: break-word;
166 | }
167 |
168 | /*
169 | * Sets consistent quote types.
170 | */
171 |
172 | q {
173 | quotes: "\201C" "\201D" "\2018" "\2019";
174 | }
175 |
176 | /*
177 | * Addresses inconsistent and variable font size in all browsers.
178 | */
179 |
180 | small {
181 | font-size: 80%;
182 | }
183 |
184 | /*
185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers.
186 | */
187 |
188 | sub,
189 | sup {
190 | font-size: 75%;
191 | line-height: 0;
192 | position: relative;
193 | vertical-align: baseline;
194 | }
195 |
196 | sup {
197 | top: -0.5em;
198 | }
199 |
200 | sub {
201 | bottom: -0.25em;
202 | }
203 |
204 | /* ==========================================================================
205 | Embedded content
206 | ========================================================================== */
207 |
208 | /*
209 | * Removes border when inside `a` element in IE 8/9.
210 | */
211 |
212 | img {
213 | border: 0;
214 | }
215 |
216 | /*
217 | * Corrects overflow displayed oddly in IE 9.
218 | */
219 |
220 | svg:not(:root) {
221 | overflow: hidden;
222 | }
223 |
224 | /* ==========================================================================
225 | Figures
226 | ========================================================================== */
227 |
228 | /*
229 | * Addresses margin not present in IE 8/9 and Safari 5.
230 | */
231 |
232 | figure {
233 | margin: 0;
234 | }
235 |
236 | /* ==========================================================================
237 | Forms
238 | ========================================================================== */
239 |
240 | /*
241 | * Define consistent border, margin, and padding.
242 | */
243 |
244 | fieldset {
245 | border: 1px solid #c0c0c0;
246 | margin: 0 2px;
247 | padding: 0.35em 0.625em 0.75em;
248 | }
249 |
250 | /*
251 | * 1. Corrects color not being inherited in IE 8/9.
252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
253 | */
254 |
255 | legend {
256 | border: 0; /* 1 */
257 | padding: 0; /* 2 */
258 | }
259 |
260 | /*
261 | * 1. Corrects font family not being inherited in all browsers.
262 | * 2. Corrects font size not being inherited in all browsers.
263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
264 | */
265 |
266 | button,
267 | input,
268 | select,
269 | textarea {
270 | font-family: inherit; /* 1 */
271 | font-size: 100%; /* 2 */
272 | margin: 0; /* 3 */
273 | }
274 |
275 | /*
276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
277 | * the UA stylesheet.
278 | */
279 |
280 | button,
281 | input {
282 | line-height: normal;
283 | }
284 |
285 | /*
286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
287 | * and `video` controls.
288 | * 2. Corrects inability to style clickable `input` types in iOS.
289 | * 3. Improves usability and consistency of cursor style between image-type
290 | * `input` and others.
291 | */
292 |
293 | button,
294 | html input[type="button"], /* 1 */
295 | input[type="reset"],
296 | input[type="submit"] {
297 | -webkit-appearance: button; /* 2 */
298 | cursor: pointer; /* 3 */
299 | }
300 |
301 | /*
302 | * Re-set default cursor for disabled elements.
303 | */
304 |
305 | button[disabled],
306 | input[disabled] {
307 | cursor: default;
308 | }
309 |
310 | /*
311 | * 1. Addresses box sizing set to `content-box` in IE 8/9.
312 | * 2. Removes excess padding in IE 8/9.
313 | */
314 |
315 | input[type="checkbox"],
316 | input[type="radio"] {
317 | box-sizing: border-box; /* 1 */
318 | padding: 0; /* 2 */
319 | }
320 |
321 | /*
322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
324 | * (include `-moz` to future-proof).
325 | */
326 |
327 | input[type="search"] {
328 | -webkit-appearance: textfield; /* 1 */
329 | -moz-box-sizing: content-box;
330 | -webkit-box-sizing: content-box; /* 2 */
331 | box-sizing: content-box;
332 | }
333 |
334 | /*
335 | * Removes inner padding and search cancel button in Safari 5 and Chrome
336 | * on OS X.
337 | */
338 |
339 | input[type="search"]::-webkit-search-cancel-button,
340 | input[type="search"]::-webkit-search-decoration {
341 | -webkit-appearance: none;
342 | }
343 |
344 | /*
345 | * Removes inner padding and border in Firefox 4+.
346 | */
347 |
348 | button::-moz-focus-inner,
349 | input::-moz-focus-inner {
350 | border: 0;
351 | padding: 0;
352 | }
353 |
354 | /*
355 | * 1. Removes default vertical scrollbar in IE 8/9.
356 | * 2. Improves readability and alignment in all browsers.
357 | */
358 |
359 | textarea {
360 | overflow: auto; /* 1 */
361 | vertical-align: top; /* 2 */
362 | }
363 |
364 | /* ==========================================================================
365 | Tables
366 | ========================================================================== */
367 |
368 | /*
369 | * Remove most spacing between table cells.
370 | */
371 |
372 | table {
373 | border-collapse: collapse;
374 | border-spacing: 0;
375 | }
--------------------------------------------------------------------------------
/website/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firebase": "firechat",
3 | "public": "_site/",
4 | "ignore": [
5 | "README.md",
6 | "firebase.json",
7 | ".*",
8 | "**/node_modules/**"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/website/images/customer-cbs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/images/customer-cbs.png
--------------------------------------------------------------------------------
/website/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/images/favicon.ico
--------------------------------------------------------------------------------
/website/images/fork-on-github-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/images/fork-on-github-white.png
--------------------------------------------------------------------------------
/website/images/powered-by-firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/images/powered-by-firebase.png
--------------------------------------------------------------------------------
/website/images/sign-in-with-twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/images/sign-in-with-twitter.png
--------------------------------------------------------------------------------
/website/images/top-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FirebaseExtended/firechat/479a1d7e7c6cb0f0aa0bd18a8c09ffe7440691e9/website/images/top-shadow.png
--------------------------------------------------------------------------------
/website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Firechat - open source chat built on Firebase
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
63 |
64 |
65 |
66 |
86 |
87 |
88 |
Real-time chat with no server code.
89 |
90 |
91 |
92 |
What is Firechat?
93 |
94 | Firechat is an open-source, real-time chat widget built on Firebase. It offers fully secure multi-user, multi-room chat with flexible authentication, moderator features, user presence and search, private messaging, chat invitations, and more.
95 |
96 |
97 |
98 |
Which technologies does Firechat use?
99 |
100 | The core data layer under Firechat uses Firebase for authentication, real-time data synchronization, and data persistence.
101 |
102 | The default interface uses jQuery, Underscore.js, and Bootstrap. Icons by Glyphicons. Build and compilation managed with Grunt and code hosted by GitHub.
103 |
104 |
105 |
106 |
107 |
108 |
What can I do with Firechat?
109 |
110 | With Firechat, you get full-featured chat in your application with a few simple script includes. Additionally, Firechat is easy to modify and extend. Based upon it's simple underlying data model and Firebase-powered data synchronization, it's easy to add new features, modify the UI, and customize to fit your specific needs.
111 |
112 |
113 | If Firechat doesn't currently meet your needs, feel free to fork the repo and tweak the code!
114 |
115 |
116 |
117 |
Who's behind Firechat?
118 |
119 | Firechat was built by the folks at Firebase in San Francisco, California.
120 |
121 | Community submissions are encouraged! Star Firechat on GitHub and send a pull request when you're ready to contribute!
122 |
123 |
124 |
125 |
126 |
127 |
Who's using Firechat?
128 |
129 |
130 |
131 | CBS Big Brother Live Feeds
132 |
133 |
134 |
135 |
136 |
How is Firechat Licensed?
137 |
138 | Firechat is published under the MIT license.
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
161 |
162 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
246 |
247 |
248 |
249 |
250 |
269 |
270 |
271 |
--------------------------------------------------------------------------------