├── .ftppass
├── .gitignore
├── src
├── day-section.handlebars
├── chat-bubble.handlebars
├── demoMessages.json
├── index.html
├── styles.css
└── main.js
├── screenshot.gif
├── CONTRIBUTORS.md
├── dist
└── index.html
├── .editorconfig
├── .eslintrc
├── .jscsrc
├── LICENSE
├── Gruntfile.js
├── package.json
├── webpack.config.js
└── README.md
/.ftppass:
--------------------------------------------------------------------------------
1 | /Users/hein/.ftppass
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bower_components
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/src/day-section.handlebars:
--------------------------------------------------------------------------------
1 | {{text}}
2 |
--------------------------------------------------------------------------------
/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IjzerenHein/famous-flex-chat/HEAD/screenshot.gif
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 |
3 | The list is still small but I hope it will grow :)
4 |
5 | - Hein Rutjes (IjzerenHein)
6 |
--------------------------------------------------------------------------------
/src/chat-bubble.handlebars:
--------------------------------------------------------------------------------
1 |
2 |
{{author}}
3 |
{{time}}
4 |
{{message}}
5 |
--------------------------------------------------------------------------------
/src/demoMessages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "test message one",
4 | "author": "Hein"
5 | },
6 | {
7 | "message": "test message one",
8 | "author": "Hein"
9 | },
10 | {
11 | "message": "test message one",
12 | "author": "Hein"
13 | },
14 | {
15 | "message": "test message one",
16 | "author": "Hein"
17 | },
18 | {
19 | "message": "test message one",
20 | "author": "Hein"
21 | },
22 | {
23 | "message": "test message one",
24 | "author": "Hein"
25 | },
26 | {
27 | "message": "test message one",
28 | "author": "Hein"
29 | }
30 | ]
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | famous-flex-chat
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | famous-flex-chat
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig: http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
16 | [*.js]
17 | indent_style = space
18 | indent_size = 4
19 |
20 | [Gruntfile.js]
21 | indent_style = space
22 | indent_size = 2
23 |
24 | [grunt/*.js]
25 | indent_style = space
26 | indent_size = 2
27 |
28 | [*.json]
29 | indent_style = space
30 | indent_size = 2
31 |
32 | [*.css]
33 | indent_style = space
34 | indent_size = 2
35 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "amd": true
5 | },
6 | "globals": {
7 | "DocumentFragment": true
8 | },
9 | "rules": {
10 | "valid-jsdoc": 0,
11 | "curly": [1, "all"],
12 | "brace-style": [1, "stroustrup"],
13 | "consistent-this": 2,
14 | "no-constant-condition": 1,
15 | "no-underscore-dangle": 0,
16 | "no-use-before-define": 1,
17 | "func-names": 0,
18 | "func-style": [2, "declaration"],
19 | "new-cap": 1,
20 | "new-parens": 2,
21 | "no-ternary": 0,
22 | "no-unused-vars": [1, {"vars": "local", "args": "none"}],
23 | "quotes": [2, "single"],
24 | "one-var": 0,
25 | "space-infix-ops": 0,
26 | "strict": 0
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "requireCurlyBraces": ["do", "try", "catch"],
3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"],
4 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"],
5 | "disallowSpaceAfterPrefixUnaryOperators": true,
6 | "disallowKeywords": ["with"],
7 | "disallowMultipleLineBreaks": true,
8 | "requireBlocksOnNewline": true,
9 | "disallowMixedSpacesAndTabs": true,
10 | "disallowTrailingWhitespace": true,
11 | "requireLineFeedAtFileEnd": true,
12 | "requireSpacesInFunctionExpression": {
13 | "beforeOpeningCurlyBrace": true
14 | },
15 | "disallowSpacesInFunctionExpression": {
16 | "beforeOpeningRoundBrace": true
17 | },
18 | "validateQuoteMarks": "'",
19 | "disallowMultipleVarDecl": true,
20 | "disallowSpacesInsideParentheses": true
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 IjzerenHein
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.
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*global module:false*/
2 | module.exports = function(grunt) {
3 |
4 | // Project configuration.
5 | grunt.initConfig({
6 | eslint: {
7 | target: ['src/*.js'],
8 | options: {
9 | config: '.eslintrc'
10 | }
11 | },
12 | jscs: {
13 | src: ['src/*.js'],
14 | options: {
15 | config: '.jscsrc'
16 | }
17 | },
18 | 'ftp-deploy': {
19 | build: {
20 | auth: {
21 | host: 'ftp.pcextreme.nl',
22 | port: 21,
23 | authKey: 'gloey.nl'
24 | },
25 | src: 'dist',
26 | dest: '/domains/gloey.nl/htdocs/www/apps/chat'
27 | }
28 | },
29 | exec: {
30 | clean: 'rm -rf ./dist',
31 | build: 'webpack -p',
32 | 'build-debug': 'webpack -d',
33 | 'serve': 'webpack-dev-server -d --inline --reload=localhost',
34 | 'open-serve': 'open http://localhost:8080'
35 | }
36 | });
37 |
38 | // These plugins provide necessary tasks.
39 | grunt.loadNpmTasks('grunt-eslint');
40 | grunt.loadNpmTasks('grunt-jscs');
41 | grunt.loadNpmTasks('grunt-ftp-deploy');
42 | grunt.loadNpmTasks('grunt-exec');
43 |
44 | // Default task.
45 | grunt.registerTask('default', ['eslint', 'jscs', 'exec:build']);
46 | grunt.registerTask('clean', ['exec:clean']);
47 | grunt.registerTask('serve', ['eslint', 'jscs', 'exec:open-serve', 'exec:serve']);
48 | grunt.registerTask('deploy', ['eslint', 'jscs', 'exec:clean', 'exec:build-debug', 'ftp-deploy']);
49 | };
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "famous-flex-chat",
3 | "private": "true",
4 | "version": "0.0.1",
5 | "homepage": "https://github.com/IjzerenHein/famous-flex-chat",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/IjzerenHein/famous-flex-chatgit"
9 | },
10 | "author": {
11 | "name": "Hein Rutjes "
12 | },
13 | "description": "iOS inspired table-layout for famo.us",
14 | "keywords": [
15 | "famo.us",
16 | "famous",
17 | "flex",
18 | "famous-flex",
19 | "scrollview",
20 | "famous-flex-chat",
21 | "chat"
22 | ],
23 | "licenses": [
24 | {
25 | "type": "MIT",
26 | "url": "https://github.com/IjzerenHein/famous-flex-chat/blob/master/LICENSE"
27 | }
28 | ],
29 | "readmeFilename": "README.md",
30 | "bugs": {
31 | "url": "https://github.com/IjzerenHein/famous-flex-chat/issues"
32 | },
33 | "engines": {
34 | "node": ">= 0.10.0"
35 | },
36 | "devDependencies": {
37 | "autolayout": "^0.5.3",
38 | "css-loader": "^0.7.0",
39 | "cuid": "^1.2.4",
40 | "expose-loader": "^0.5.3",
41 | "famous": "^0.3.5",
42 | "famous-autolayout": "^0.2.6",
43 | "famous-autosizetextarea": "latest",
44 | "famous-flex": "^0.3.9",
45 | "famous-lagometer": "latest",
46 | "famous-polyfills": "^0.2.2",
47 | "famous-refresh-loader": "latest",
48 | "fastclick": "^1.0.3",
49 | "file-loader": "^0.6.1",
50 | "firebase": "^2.0.4",
51 | "glob": "^4.0.5",
52 | "grunt": "latest",
53 | "grunt-eslint": "latest",
54 | "grunt-exec": "latest",
55 | "grunt-ftp-deploy": "latest",
56 | "grunt-jscs": "latest",
57 | "handlebars-loader": "^0.1.7",
58 | "html-loader": "^0.2.2",
59 | "json-loader": "^0.5.1",
60 | "moment": "^2.8.3",
61 | "optimist": "^0.6.1",
62 | "pleasejs": "^0.2.0",
63 | "script-loader": "^0.6.0",
64 | "style-loader": "^0.6.4",
65 | "ua_parser": "^1.0.14",
66 | "url-loader": "^0.5.5",
67 | "webpack": "latest",
68 | "webpack-dev-server": "latest"
69 | },
70 | "files": [
71 | "src",
72 | "LICENSE"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*global module, process*/
2 | /*eslint no-use-before-define:0 */
3 |
4 | var webpack = require('webpack');
5 | var webpackDevServer = require('webpack-dev-server');
6 | var path = require('path');
7 |
8 | // Support for extra commandline arguments
9 | var argv = require('optimist')
10 | //--env=XXX: sets a global ENV variable (i.e. window.ENV='XXX')
11 | .alias('e','env').default('e','dev')
12 | .argv;
13 |
14 | var config = {
15 | context: path.join(__dirname, 'src'),
16 | entry: {'bundle': './main'},
17 | output: {
18 | path: path.join(__dirname, 'dist'),
19 | filename: '[name].js',
20 | publicPath: isDevServer() ? '/' : ''
21 | },
22 | devServer: {
23 | publicPath: '/'
24 | },
25 | reload: isDevServer() ? 'localhost' : null,
26 | module: {
27 | loaders: [
28 | { test: /\.json$/, loader: 'json-loader' },
29 | { test: /\.css$/, loader: 'style-loader!css-loader' },
30 | { test: /\.handlebars$/, loader: 'handlebars-loader' },
31 | { test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=5000&name=[path][name].[ext]&context=./src' },
32 | { test: /\.eot$/, loader: 'file-loader?name=[path][name].[ext]&context=./src' },
33 | { test: /\.ttf$/, loader: 'file-loader?name=[path][name].[ext]&context=./src' },
34 | { test: /\.svg$/, loader: 'file-loader?name=[path][name].[ext]&context=./src' },
35 | { test: /\.woff$/, loader: 'file-loader?name=[path][name].[ext]&context=./src' },
36 | { test: /index\.html$/, loader: 'file-loader?name=[path][name].[ext]&context=./src' }
37 | ],
38 | noParse: [
39 | /dist\/autolayout\.js$/
40 | ]
41 | },
42 | resolve: {
43 | alias: {
44 | 'famous-flex': 'famous-flex/src'
45 | }
46 | },
47 | plugins: [
48 | new webpack.DefinePlugin({
49 | VERSION: JSON.stringify(require('./package.json').version),
50 | ENV: JSON.stringify(argv.env)
51 | })
52 | ]
53 | };
54 |
55 | function isDevServer() {
56 | return process.argv.join('').indexOf('webpack-dev-server') > -1;
57 | }
58 |
59 | module.exports = config;
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | famous-flex-chat
2 | ==========
3 |
4 | Chat-demo for famo.us using the [famous-flex](https://github.com/IjzerenHein/famous-flex) FlexScrollView. This project shows how to create a native feeling cross-platform chat application using famo.us.
5 |
6 | 
7 |
8 | The following features are demonstrated:
9 |
10 | - True-size chat-bubbles using [famous-flex/FlexScrollView](https://github.com/IjzerenHein/famous-flex/blob/master/tutorials/FlexScrollView.md)
11 | - Sticky headers using [famous-flex/layouts/ListLayout](https://github.com/IjzerenHein/famous-flex/blob/master/docs/layouts/ListLayout.md)
12 | - Resizable text-area input [famous-autosizetextarea](https://github.com/IjzerenHein/famous-autosizetextarea)
13 | - Pull to refresh spinner [famous-refresh-loader](https://github.com/IjzerenHein/famous-refresh-loader)
14 | - Responsive design principles using famous-flex
15 |
16 | [View the live demo here](https://rawgit.com/IjzerenHein/famous-flex-chat/master/dist/index.html)
17 |
18 |
19 | ## Content
20 |
21 | - [Source code](./src/main.js)
22 |
23 |
24 | ## Build
25 |
26 | To build the demo, make sure grunt, webpack and webpack-dev-server are installed globally:
27 |
28 | ```
29 | npm install -g grunt-cli webpack webpack-dev-server
30 | ```
31 |
32 | Run npm to install all dev-dependencies:
33 |
34 | ```
35 | npm install
36 | ```
37 |
38 | To build the output (dist-folder), run the webpack command:
39 |
40 | ```
41 | webpack
42 | ```
43 |
44 |
45 | ## Running
46 |
47 | To run the demo either open `dist/index.html`
48 |
49 | Or use the live-reload server:
50 |
51 | ```
52 | grunt serve
53 | ```
54 |
55 | The app uses Firebase as backend. A demo Firebase instance is hardcoded in main.js.
56 | To use your own database, register as a new user on Firebase.com and create a new free app and give it a name. Then change the firebase URL in main.js from:
57 |
58 | ```
59 | fbMessages = new Firebase('https://famous-flex-chat.firebaseio.com/messages');
60 | ```
61 | to this where <your-app-name> is the name of your Firebase app:
62 |
63 | ```
64 | fbMessages = new Firebase('https://.firebaseio.com/messages');
65 | ```
66 |
67 | The chat should now be empty and ready for new messages. No additonal steps necessary.
68 |
69 | ## Contribute
70 |
71 | If you like this project and want to support it, show some love
72 | and give it a star.
73 |
74 |
75 | © 2015 - Hein Rutjes
76 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | body, div {
2 | font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
3 | font-weight: normal;
4 | }
5 | body {
6 | background-color: white;
7 | position: absolute;
8 | }
9 | body > div {
10 | background-color: white;
11 | }
12 |
13 | /**
14 | * Name-bar
15 | */
16 | .name-input {
17 | font-size: 16px;
18 | padding: 6px 10px 6px 10px;
19 | -webkit-appearance: none;
20 | -moz-appearance: none;
21 | border: none;
22 | border-bottom: 1px solid #CCCCCC;
23 | z-index: 10;
24 | }
25 |
26 | /**
27 | * Message-bar
28 | */
29 | .message-back {
30 | border-top: 1px solid #CCCCCC;
31 | background-color: #EEEEEE;
32 | }
33 | .message-input {
34 | border-radius: 7px;
35 | border-color: #CCCCCC;
36 | font-size: 16px;
37 | padding: 6px 5px 6px 5px;
38 | -webkit-appearance: none;
39 | -moz-appearance: none;
40 | }
41 | .message-send {
42 | text-align: center;
43 | line-height: 34px;
44 | font-weight: 600;
45 | }
46 |
47 |
48 | /**
49 | * Message-day
50 | */
51 | .message-day {
52 | padding: 5px 10px 15px 10px;
53 | overflow: hidden;
54 | text-align: center;
55 | z-index: 10;
56 | /*background: white;*/
57 | /* disable text selection */
58 | -webkit-touch-callout: none;
59 | -webkit-user-select: none;
60 | -khtml-user-select: none;
61 | -moz-user-select: none;
62 | -ms-user-select: none;
63 | user-select: none;
64 | }
65 | .message-day .text{
66 | -webkit-border-radius: 15px;
67 | -moz-border-radius: 15px;
68 | border-radius: 15px;
69 | padding: 5px 10px;
70 | background: rgb(187, 191, 114);
71 | color: white;
72 | display: inline-block;
73 | font-size: 12px;
74 | }
75 |
76 |
77 | /**
78 | * Message-bubbles
79 | */
80 | .message-bubble {
81 | padding: 0 10px 10px 10px;
82 | /* disable text selection */
83 | -webkit-touch-callout: none;
84 | -webkit-user-select: none;
85 | -khtml-user-select: none;
86 | -moz-user-select: none;
87 | -ms-user-select: none;
88 | user-select: none;
89 | overflow: hidden;
90 | background: white;
91 | }
92 | .message-bubble.send {
93 | padding: 0 10px 10px 30px;
94 | }
95 | .message-bubble.received {
96 | padding: 0 30px 10px 10px;
97 | }
98 | .message-bubble .back {
99 | -webkit-border-radius: 10px;
100 | -moz-border-radius: 10px;
101 | border-radius: 10px;
102 | background-color: #DDDDDD;
103 | padding: 8px 8px 8px 8px;
104 | float: left;
105 | max-width: 100%;
106 | }
107 | .message-bubble.send .back {
108 | background-color: rgb(114, 173, 191);
109 | float: right;
110 | }
111 | .message-bubble .author {
112 | font-size: 14px;
113 | line-height: 18px;
114 | font-weight: bold;
115 | }
116 | .message-bubble .time {
117 | float: right;
118 | font-size: 12px;
119 | line-height: 18px;
120 | margin-left: 10px;
121 | color: #888888;
122 | }
123 | .message-bubble.send .time {
124 | color: #444444;
125 | }
126 | .message-bubble .message {
127 | margin-top: 3px;
128 | font-size: 16px;
129 | word-wrap: break-word;
130 | -word-break: break-all;
131 | }
132 | .message-bubble .back:after {
133 | content: "";
134 | position: absolute;
135 | bottom: 16px;
136 | border-style: solid;
137 | border-color: transparent #DDDDDD;
138 | display: block;
139 | width: 0;
140 | }
141 | .message-bubble.send .back:after {
142 | border-width: 5px 0 5px 10px;
143 | right: 2px;
144 | border-color: transparent rgb(114, 173, 191);
145 | }
146 | .message-bubble.received .back:after {
147 | border-width: 5px 10px 5px 0;
148 | left: 2px;
149 | }
150 |
151 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This Source Code is licensed under the MIT license. If a copy of the
3 | * MIT-license was not distributed with this file, You can obtain one at:
4 | * http://opensource.org/licenses/mit-license.html.
5 | *
6 | * @author: Hein Rutjes (IjzerenHein)
7 | * @license MIT
8 | * @copyright Gloey Apps, 2014
9 | */
10 |
11 | /*global define, Please, console*/
12 | /*eslint no-console:0 no-use-before-define:0*/
13 |
14 | define(function(require) {
15 |
16 | //
17 | require('famous-polyfills');
18 | require('famous/core/famous.css');
19 | require('./styles.css');
20 | require('./index.html');
21 | //
22 |
23 | // Fast-click
24 | var FastClick = require('fastclick/lib/fastclick');
25 | FastClick.attach(document.body);
26 |
27 | // import dependencies
28 | var Firebase = require('firebase/lib/firebase-web');
29 | var Engine = require('famous/core/Engine');
30 | var LinkedListViewSequence = require('famous-flex/LinkedListViewSequence');
31 | var Surface = require('famous/core/Surface');
32 | //var Modifier = require('famous/core/Modifier');
33 | //var Transform = require('famous/core/Transform');
34 | //var Lagometer = require('famous-lagometer/Lagometer');
35 | var FlexScrollView = require('famous-flex/FlexScrollView');
36 | var LayoutController = require('famous-flex/LayoutController');
37 | var vflToLayout = require('famous-autolayout/src/vflToLayoutv3');
38 | var AutosizeTextareaSurface = require('famous-autosizetextarea/AutosizeTextareaSurface');
39 | var Timer = require('famous/utilities/Timer');
40 | var InputSurface = require('famous/surfaces/InputSurface');
41 | var RefreshLoader = require('famous-refresh-loader/RefreshLoader');
42 | var moment = require('moment/moment');
43 | var cuid = require('cuid');
44 | // templates
45 | var chatBubbleTemplate = require('./chat-bubble.handlebars');
46 | var daySectionTemplate = require('./day-section.handlebars');
47 |
48 | // Initialize
49 | var mainContext = Engine.createContext();
50 | var viewSequence = new LinkedListViewSequence();
51 | _createPullToRefreshCell();
52 | _setupFirebase();
53 | mainContext.add(_createMainLayout());
54 |
55 | // When position:absolute is used, the size of the root context
56 | // is not initialized properly until the browser is resized.
57 | // Force the context to initialize its size by emulating an initial
58 | // resize event.
59 | Engine.nextTick(function() {
60 | mainContext.emit('resize', {});
61 | });
62 | //_createLagometer();
63 |
64 | //
65 | // Main layout, bottom text input, top chat messages
66 | //
67 | var mainLayout;
68 | function _createMainLayout() {
69 | mainLayout = new LayoutController({
70 | layout: vflToLayout([
71 | '//spacing:100',
72 | '//heights footer:50',
73 | 'V:|[col:[header(34)][content][footer]]|',
74 | 'H:|[col]|'
75 | ]),
76 | layoutOptions: {
77 | heights: {
78 | footer: 50
79 | }
80 | },
81 | dataSource: {
82 | header: _createNameBar(),
83 | content: _createScrollView(),
84 | footer: _createMessageBar()
85 | }
86 | });
87 | return mainLayout;
88 | }
89 |
90 | //
91 | // Creates the top input field for the name
92 | //
93 | var nameBar;
94 | function _createNameBar() {
95 | nameBar = new InputSurface({
96 | classes: ['name-input'],
97 | placeholder: 'Your name...',
98 | value: localStorage.name
99 | });
100 | nameBar.on('change', function() {
101 | localStorage.name = nameBar.getValue();
102 | });
103 | return nameBar;
104 | }
105 |
106 | //
107 | // Message-bar holding textarea input and send button
108 | //
109 | var messageBar;
110 | function _createMessageBar() {
111 | var back = new Surface({
112 | classes: ['message-back']
113 | });
114 | messageBar = new LayoutController({
115 | layout: {dock: [
116 | ['fill', 'back'],
117 | ['left', undefined, 8],
118 | ['top', undefined, 8],
119 | ['right', undefined, 8],
120 | ['bottom', undefined, 8],
121 | ['right', 'send', undefined, 1],
122 | ['fill', 'input', 1]
123 | ]},
124 | dataSource: {
125 | back: back,
126 | input: _createMessageInput(),
127 | send: _createSendButton()
128 | }
129 | });
130 | return messageBar;
131 | }
132 |
133 | //
134 | // Message-input textarea
135 | //
136 | var messageInputTextArea;
137 | function _createMessageInput() {
138 | messageInputTextArea = new AutosizeTextareaSurface({
139 | classes: ['message-input'],
140 | placeholder: 'famous-flex-chat...',
141 | properties: {
142 | resize: 'none'
143 | }
144 | });
145 | messageInputTextArea.on('scrollHeightChanged', _updateMessageBarHeight);
146 | messageInputTextArea.on('keydown', function(e) {
147 | if (e.keyCode === 13) {
148 | e.preventDefault();
149 | _sendMessage();
150 | }
151 | });
152 | return messageInputTextArea;
153 | }
154 |
155 | //
156 | // Updates the message-bar height to accomate for the text that
157 | // was entered in the message text-area.
158 | //
159 | function _updateMessageBarHeight() {
160 | var height = Math.max(Math.min(messageInputTextArea.getScrollHeight() + 16, 200), 50);
161 | if (mainLayout.getLayoutOptions().heights.footer !== height) {
162 | mainLayout.setLayoutOptions({
163 | heights: {
164 | footer: height
165 | }
166 | });
167 | return true;
168 | }
169 | return false;
170 | }
171 |
172 | //
173 | // Create send button
174 | //
175 | function _createSendButton() {
176 | var button = new Surface({
177 | classes: ['message-send'],
178 | content: 'Send',
179 | size: [60, undefined]
180 | });
181 | button.on('click', _sendMessage);
182 | return button;
183 | }
184 |
185 | //
186 | // Create scrollview
187 | //
188 | var scrollView;
189 | function _createScrollView() {
190 | scrollView = new FlexScrollView({
191 | layoutOptions: {
192 | // callback that is called by the layout-function to check
193 | // whether a node is a section
194 | isSectionCallback: function(renderNode) {
195 | return renderNode.properties.isSection;
196 | },
197 | margins: [5, 0, 0, 0]
198 | },
199 | dataSource: viewSequence,
200 | autoPipeEvents: true,
201 | flow: true,
202 | alignment: 1,
203 | mouseMove: true,
204 | debug: true,
205 | pullToRefreshHeader: pullToRefreshHeader
206 | });
207 | return scrollView;
208 | }
209 |
210 | //
211 | // Adds a message to the scrollview
212 | //
213 | var afterInitialRefreshTimerId;
214 | var afterInitialRefresh;
215 | var firstKey;
216 | function _addMessage(data, top, key) {
217 | var time = moment(data.timeStamp || new Date());
218 | data.time = time.format('LT');
219 | if (!data.author || (data.author === '')) {
220 | data.author = 'Anonymous coward';
221 | }
222 |
223 | // Store first key
224 | firstKey = firstKey || key;
225 | if (top && key) {
226 | firstKey = key;
227 | }
228 |
229 | // Insert section
230 | var day = time.format('LL');
231 | if (!top && (day !== lastSectionDay)) {
232 | lastSectionDay = day;
233 | firstSectionDay = firstSectionDay || day;
234 | scrollView.push(_createDaySection(day));
235 | }
236 | else if (top && (day !== firstSectionDay)) {
237 | firstSectionDay = day;
238 | scrollView.insert(0, _createDaySection(day));
239 | }
240 |
241 | //console.log('adding message: ' + JSON.stringify(data));
242 | var chatBubble = _createChatBubble(data);
243 | if (top) {
244 | scrollView.insert(1, chatBubble);
245 | }
246 | else {
247 | scrollView.push(chatBubble);
248 | }
249 | if (!top) {
250 |
251 | // Scroll the latest (newest) chat message
252 | if (afterInitialRefresh) {
253 | scrollView.goToLastPage();
254 | scrollView.reflowLayout();
255 | }
256 | else {
257 |
258 | // On startup, set datasource to the last page immediately
259 | // so it doesn't scroll from top to bottom all the way
260 | viewSequence = viewSequence.getNext() || viewSequence;
261 | scrollView.setDataSource(viewSequence);
262 | scrollView.goToLastPage();
263 | if (afterInitialRefreshTimerId === undefined) {
264 | afterInitialRefreshTimerId = Timer.setTimeout(function() {
265 | afterInitialRefresh = true;
266 | }, 100);
267 | }
268 | }
269 | }
270 | }
271 |
272 | //
273 | // setup firebase
274 | //
275 | var fbMessages;
276 | var firstSectionDay;
277 | var lastSectionDay;
278 | function _setupFirebase() {
279 | fbMessages = new Firebase('https://famous-flex-chat.firebaseio.com/messages');
280 | fbMessages.limitToLast(30).on('child_added', function(snapshot) {
281 | _addMessage(snapshot.val(), false, snapshot.key());
282 | });
283 | }
284 |
285 | //
286 | // Create a chat-bubble
287 | //
288 | function _createChatBubble(data) {
289 | var surface = new Surface({
290 | size: [undefined, true],
291 | classes: ['message-bubble', (data.userId === _getUserId()) ? 'send' : 'received'],
292 | content: chatBubbleTemplate(data),
293 | properties: {
294 | message: data.message
295 | }
296 | });
297 | return surface;
298 | }
299 |
300 | //
301 | // Create a day section
302 | //
303 | function _createDaySection(day) {
304 | return new Surface({
305 | size: [undefined, 42],
306 | classes: ['message-day'],
307 | content: daySectionTemplate({text: day}),
308 | properties: {
309 | isSection: true
310 | }
311 | });
312 | }
313 |
314 | //
315 | // Generates a unique id for every user so that received messages
316 | // can be distinguished comming from this user or another user.
317 | //
318 | var userId;
319 | function _getUserId() {
320 | if (!userId) {
321 | userId = localStorage.userId;
322 | if (!userId) {
323 | userId = cuid();
324 | localStorage.userId = userId;
325 | }
326 | }
327 | return userId;
328 | }
329 |
330 | //
331 | // Sends a new message
332 | //
333 | function _sendMessage() {
334 | var value = messageInputTextArea.getValue();
335 | if (!value || (value === '')) {
336 | return;
337 | }
338 | messageInputTextArea.setValue('');
339 | fbMessages.push({
340 | author: nameBar.getValue(),
341 | userId: _getUserId(),
342 | message: value,
343 | timeStamp: new Date().getTime()
344 | });
345 | messageInputTextArea.focus();
346 | }
347 |
348 | /**
349 | * Create pull to refresh header
350 | */
351 | var pullToRefreshHeader;
352 | function _createPullToRefreshCell() {
353 | pullToRefreshHeader = new RefreshLoader({
354 | size: [undefined, 60],
355 | pullToRefresh: true,
356 | pullToRefreshBackgroundColor: 'white'
357 | });
358 | }
359 | scrollView.on('refresh', function(event) {
360 | var queryKey = firstKey;
361 | fbMessages.endAt(null, firstKey).limitToLast(2).once('value', function(snapshot) {
362 | var val = snapshot.val();
363 | for (var key in val) {
364 | if (key !== queryKey) {
365 | _addMessage(val[key], true, key);
366 | }
367 | }
368 | Timer.setTimeout(function() {
369 | scrollView.hidePullToRefresh(event.footer);
370 | }, 200);
371 | });
372 | });
373 |
374 | //
375 | // Shows the lagometer
376 | //
377 | /*function _createLagometer() {
378 | var lagometerMod = new Modifier({
379 | size: [100, 100],
380 | align: [1.0, 0.0],
381 | origin: [1.0, 0.0],
382 | transform: Transform.translate(-10, 10, 1000)
383 | });
384 | var lagometer = new Lagometer({
385 | size: lagometerMod.getSize()
386 | });
387 | mainContext.add(lagometerMod).add(lagometer);
388 | }*/
389 | });
390 |
--------------------------------------------------------------------------------