├── .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 | ![Screenshot](screenshot.gif) 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 | --------------------------------------------------------------------------------