├── .gitignore ├── code ├── .meteor │ ├── cordova-plugins │ ├── .gitignore │ ├── release │ ├── platforms │ ├── .id │ ├── .finished-upgraders │ ├── packages │ └── versions ├── private │ └── email-templates │ │ └── .gitkeep ├── client │ ├── startup.js │ ├── stylesheets │ │ ├── module │ │ │ ├── _login.scss │ │ │ ├── _header.scss │ │ │ ├── _loading.scss │ │ │ └── _channel.scss │ │ ├── base │ │ │ ├── _forms.scss │ │ │ └── _animations.scss │ │ ├── _extends.scss │ │ └── application.scss │ ├── templates │ │ ├── globals │ │ │ ├── not-found.js │ │ │ ├── not-found.html │ │ │ ├── public-navigation.html │ │ │ ├── authenticated-navigation.html │ │ │ ├── header.html │ │ │ ├── header.js │ │ │ └── loading.html │ │ ├── public │ │ │ ├── login.js │ │ │ ├── reset-password.js │ │ │ ├── recover-password.js │ │ │ ├── signup.js │ │ │ ├── recover-password.html │ │ │ ├── login.html │ │ │ ├── reset-password.html │ │ │ └── signup.html │ │ ├── authenticated │ │ │ ├── message.html │ │ │ ├── message.js │ │ │ ├── sidebar.html │ │ │ ├── channel.html │ │ │ ├── sidebar.js │ │ │ └── channel.js │ │ └── layouts │ │ │ ├── default.html │ │ │ └── default.js │ ├── modules │ │ ├── sanitize-username.js │ │ ├── set-scroll.js │ │ ├── handle-channel-switch.js │ │ ├── recover-password.js │ │ ├── sort-messages.js │ │ ├── handle-message-insert.js │ │ ├── login.js │ │ ├── reset-password.js │ │ └── signup.js │ └── helpers │ │ ├── template │ │ ├── forms.js │ │ ├── logic.js │ │ ├── strings.js │ │ └── date-time.js │ │ └── flow-router.js ├── both │ ├── startup.js │ ├── routes │ │ ├── configure.js │ │ ├── authenticated.js │ │ └── public.js │ └── methods │ │ ├── insert │ │ └── collection-name.js │ │ ├── remove │ │ └── collection-name.js │ │ └── update │ │ └── collection-name.js ├── public │ └── favicon.ico ├── .gitignore ├── server │ ├── modules │ │ ├── set-browser-policies.js │ │ ├── seed-database.js │ │ └── insert-message.js │ ├── startup.js │ ├── publications │ │ ├── sidebar.js │ │ └── channel.js │ ├── methods │ │ ├── remove │ │ │ └── collection-name.js │ │ └── insert │ │ │ └── messages.js │ └── accounts │ │ └── email-templates.js ├── application.html ├── .editorconfig ├── collections │ ├── users.js │ ├── channels.js │ └── messages.js ├── README.md ├── package.json └── .eslintrc.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /code/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /code/private/email-templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3-beta.12 2 | -------------------------------------------------------------------------------- /code/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /code/client/startup.js: -------------------------------------------------------------------------------- 1 | Meteor.startup( () => { 2 | Bert.defaults.style = 'growl-bottom-right'; 3 | }); 4 | -------------------------------------------------------------------------------- /code/both/startup.js: -------------------------------------------------------------------------------- 1 | Meteor.startup( () => { 2 | // Code to run on client *and* server startup. 3 | }); 4 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_login.scss: -------------------------------------------------------------------------------- 1 | .login label { 2 | display: block; 3 | @extend %clearfix; 4 | } 5 | -------------------------------------------------------------------------------- /code/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeteorchef/building-a-chat-application/HEAD/code/public/favicon.ico -------------------------------------------------------------------------------- /code/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | settings-development.json 3 | settings-production.json 4 | npm-debug.log 5 | 6 | node_modules 7 | -------------------------------------------------------------------------------- /code/server/modules/set-browser-policies.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | BrowserPolicy.content.allowOriginForAll( '*' ); 3 | } 4 | -------------------------------------------------------------------------------- /code/both/routes/configure.js: -------------------------------------------------------------------------------- 1 | FlowRouter.notFound = { 2 | action() { 3 | BlazeLayout.render( 'default', { yield: 'notFound' } ); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /code/client/templates/globals/not-found.js: -------------------------------------------------------------------------------- 1 | Template.notFound.helpers({ 2 | currentPath() { 3 | return FlowRouter.current().path; 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /code/client/modules/sanitize-username.js: -------------------------------------------------------------------------------- 1 | export default function( value ) { 2 | return value.replace( /[^A-Za-z0-9\s]/g, '' ).toLowerCase().trim(); 3 | } 4 | -------------------------------------------------------------------------------- /code/client/stylesheets/base/_forms.scss: -------------------------------------------------------------------------------- 1 | form label.error { 2 | display: block; 3 | margin-top: 10px; 4 | font-weight: normal; 5 | color: lighten( red, 20% ); 6 | } 7 | -------------------------------------------------------------------------------- /code/client/templates/globals/not-found.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /code/application.html: -------------------------------------------------------------------------------- 1 | 2 | MegaCorp Intranet 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /code/client/modules/set-scroll.js: -------------------------------------------------------------------------------- 1 | export default function( containerId ) { 2 | let messages = document.getElementById( containerId ); 3 | setTimeout( () => { messages.scrollTop = messages.scrollHeight; }, 300 ); 4 | } 5 | -------------------------------------------------------------------------------- /code/client/stylesheets/_extends.scss: -------------------------------------------------------------------------------- 1 | %clearfix { 2 | *zoom: 1; 3 | 4 | &:before, 5 | &:after { 6 | display: table; 7 | content: ""; 8 | } 9 | 10 | &:after { 11 | clear: both; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /code/server/startup.js: -------------------------------------------------------------------------------- 1 | import setBrowserPolicies from './modules/set-browser-policies'; 2 | import seedDatabase from './modules/seed-database'; 3 | 4 | Meteor.startup( () => { 5 | setBrowserPolicies(); 6 | seedDatabase(); 7 | }); 8 | -------------------------------------------------------------------------------- /code/server/publications/sidebar.js: -------------------------------------------------------------------------------- 1 | Meteor.publish( 'sidebar', function() { 2 | return [ 3 | Channels.find(), 4 | Meteor.users.find( { _id: { $ne: this.userId } }, { fields: { username: 1, 'profile.name': 1 } } ) 5 | ]; 6 | }); 7 | -------------------------------------------------------------------------------- /code/client/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import "extends"; 2 | 3 | @import "base/animations"; 4 | @import "base/forms"; 5 | 6 | @import "module/channel"; 7 | @import "module/header"; 8 | @import "module/loading"; 9 | @import "module/login"; 10 | -------------------------------------------------------------------------------- /code/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /code/collections/users.js: -------------------------------------------------------------------------------- 1 | Meteor.users.allow({ 2 | insert: () => false, 3 | update: () => false, 4 | remove: () => false 5 | }); 6 | 7 | Meteor.users.deny({ 8 | insert: () => true, 9 | update: () => true, 10 | remove: () => true 11 | }); 12 | -------------------------------------------------------------------------------- /code/both/routes/authenticated.js: -------------------------------------------------------------------------------- 1 | const authenticatedRoutes = FlowRouter.group( { name: 'authenticated' } ); 2 | 3 | authenticatedRoutes.route( '/messages/:channel', { 4 | name: 'channel', 5 | action() { 6 | BlazeLayout.render( 'default', { yield: 'channel' } ); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /code/client/helpers/template/forms.js: -------------------------------------------------------------------------------- 1 | Template.registerHelper( 'selected', ( valueOne, valueTwo ) => { 2 | return valueOne === valueTwo ? 'selected' : ''; 3 | }); 4 | 5 | Template.registerHelper( 'checked', ( valueOne, valueTwo ) => { 6 | return valueOne === valueTwo ? 'checked' : ''; 7 | }); 8 | -------------------------------------------------------------------------------- /code/client/templates/public/login.js: -------------------------------------------------------------------------------- 1 | import login from '../../modules/login'; 2 | 3 | Template.login.onRendered( () => { 4 | login( { form: '#login', template: Template.instance() } ); 5 | }); 6 | 7 | Template.login.events({ 8 | 'submit form': ( event ) => event.preventDefault() 9 | }); 10 | -------------------------------------------------------------------------------- /code/client/stylesheets/base/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes rotate { 2 | from { transform: rotate( 0deg ); } 3 | to { transform: rotate( 360deg ); } 4 | } 5 | 6 | @-webkit-keyframes rotate { 7 | from { -webkit-transform: rotate( 0deg ); } 8 | to { -webkit-transform: rotate( 360deg ); } 9 | } 10 | -------------------------------------------------------------------------------- /code/both/methods/insert/collection-name.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | insertBoth( object ) { 3 | check( object, Object ); 4 | 5 | try { 6 | return Documents.insert( object ); 7 | } catch ( exception ) { 8 | throw new Meteor.Error( '500', `${ exception }` ); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /code/both/methods/remove/collection-name.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | removeBoth( documentId ) { 3 | check( documentId, String ); 4 | 5 | try { 6 | return Documents.remove( documentId ); 7 | } catch ( exception ) { 8 | throw new Meteor.Error( '500', `${ exception }` ); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /code/both/methods/update/collection-name.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | updateBoth( update ) { 3 | check( update, Object ); 4 | 5 | try { 6 | return Documents.update( update._id, { $set: update } ); 7 | } catch ( exception ) { 8 | throw new Meteor.Error( '500', `${ exception }` ); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /code/server/methods/remove/collection-name.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | removeServerOnly( documentId ) { 3 | check( documentId, String ); 4 | 5 | try { 6 | return Documents.remove( documentId ); 7 | } catch ( exception ) { 8 | throw new Meteor.Error( '500', `${ exception }` ); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /code/client/templates/globals/public-navigation.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /code/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1ipv1tp12xzfzhba088c 8 | -------------------------------------------------------------------------------- /code/client/templates/public/reset-password.js: -------------------------------------------------------------------------------- 1 | import resetPassword from '../../modules/reset-password'; 2 | 3 | Template.resetPassword.onRendered( () => { 4 | resetPassword( { form: '#reset-password', template: Template.instance() } ); 5 | }); 6 | 7 | Template.resetPassword.events({ 8 | 'submit form': ( event ) => event.preventDefault() 9 | }); 10 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_header.scss: -------------------------------------------------------------------------------- 1 | .navbar.navbar-default { 2 | border-radius: 0px; 3 | background: #fff; 4 | 5 | .navbar-brand > span { 6 | position: relative; 7 | top: -1px; 8 | left: 10px; 9 | font-size: 14px; 10 | color: #aaa; 11 | } 12 | } 13 | 14 | body.is-channel .navbar.navbar-default { 15 | border-bottom: none; 16 | } 17 | -------------------------------------------------------------------------------- /code/client/templates/public/recover-password.js: -------------------------------------------------------------------------------- 1 | import recoverPassword from '../../modules/recover-password'; 2 | 3 | Template.recoverPassword.onRendered( () => { 4 | recoverPassword( { form: '#recover-password', template: Template.instance() } ); 5 | }); 6 | 7 | Template.recoverPassword.events({ 8 | 'submit form': ( event ) => event.preventDefault() 9 | }); 10 | -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | # The Meteor Chef - Base 2 | A starting point for Meteor apps. 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Base Versionv4.0.0
Meteor Versionv1.3.0
16 | 17 | [Read the Documentation](http://themeteorchef.com/base) 18 | -------------------------------------------------------------------------------- /code/client/templates/authenticated/message.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_loading.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | -webkit-animation-name: rotate; 3 | -webkit-animation-duration: 0.5s; 4 | -webkit-animation-iteration-count: infinite; 5 | -webkit-animation-timing-function: linear; 6 | animation-name: rotate; 7 | animation-duration: 0.5s; 8 | animation-iteration-count: infinite; 9 | animation-timing-function: linear; 10 | } 11 | -------------------------------------------------------------------------------- /code/server/methods/insert/messages.js: -------------------------------------------------------------------------------- 1 | import insertMessage from '../../modules/insert-message'; 2 | 3 | Meteor.methods({ 4 | insertMessage( message ) { 5 | check( message, { 6 | destination: String, 7 | isDirect: Boolean, 8 | message: String 9 | }); 10 | 11 | try { 12 | insertMessage( message ); 13 | } catch ( exception ) { 14 | throw new Meteor.Error( '500', `${ exception }` ); 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /code/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /code/client/templates/globals/authenticated-navigation.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /code/client/templates/authenticated/message.js: -------------------------------------------------------------------------------- 1 | Template.message.helpers({ 2 | name( userId ) { 3 | if ( userId ) { 4 | let user = Meteor.users.findOne( userId, { fields: { 'profile.name': 1 } } ); 5 | return user ? `${ user.profile.name.first } ${ user.profile.name.last }` : ''; 6 | } 7 | } 8 | }); 9 | 10 | Template.message.events({ 11 | 'click a' ( event ) { 12 | event.preventDefault(); 13 | window.open( event.target.href, '_blank' ); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /code/client/helpers/template/logic.js: -------------------------------------------------------------------------------- 1 | Template.registerHelper( 'equals', ( valueOne, valueTwo ) => { 2 | return valueOne === valueTwo; 3 | }); 4 | 5 | Template.registerHelper( 'notEqual', ( valueOne, valueTwo ) => { 6 | return valueOne !== valueTwo; 7 | }); 8 | 9 | Template.registerHelper( 'or', ( valueOne, valueTwo ) => { 10 | return valueOne || valueTwo; 11 | }); 12 | 13 | Template.registerHelper( 'and', function( valueOne, valueTwo ) { 14 | return valueOne && valueTwo; 15 | }); 16 | -------------------------------------------------------------------------------- /code/client/helpers/template/strings.js: -------------------------------------------------------------------------------- 1 | Template.registerHelper( 'capitalize', ( string ) => { 2 | if ( string ) { 3 | return string.charAt( 0 ).toUpperCase() + string.slice( 1 ); 4 | } 5 | }); 6 | 7 | Template.registerHelper( 'lowercase', ( string ) => { 8 | if ( string ) { 9 | return string.toLowerCase(); 10 | } 11 | }); 12 | 13 | Template.registerHelper( 'parseMarkdown', ( string ) => { 14 | if ( string && parseMarkdown ) { 15 | return parseMarkdown( string ); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /code/collections/channels.js: -------------------------------------------------------------------------------- 1 | Channels = new Mongo.Collection( 'channels' ); 2 | 3 | Channels.allow({ 4 | insert: () => false, 5 | update: () => false, 6 | remove: () => false 7 | }); 8 | 9 | Channels.deny({ 10 | insert: () => true, 11 | update: () => true, 12 | remove: () => true 13 | }); 14 | 15 | let ChannelsSchema = new SimpleSchema({ 16 | 'name': { 17 | type: String, 18 | label: 'The name of the channel.' 19 | } 20 | }); 21 | 22 | Channels.attachSchema( ChannelsSchema ); 23 | -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "1.0.0", 4 | "description": "Application description.", 5 | "scripts": { 6 | "start": "meteor --settings settings-development.json", 7 | "staging": "meteor deploy staging.meteor.com --settings settings-development.json", 8 | "production": "meteor deploy production.meteor.com --settings settings-production.json" 9 | }, 10 | "devDependencies": {}, 11 | "dependencies": { 12 | "lodash": "^4.5.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /code/client/templates/layouts/default.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /code/client/templates/globals/header.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /code/client/templates/public/signup.js: -------------------------------------------------------------------------------- 1 | import signup from '../../modules/signup'; 2 | import sanitizeUsername from '../../modules/sanitize-username'; 3 | 4 | Template.signup.onRendered( () => { 5 | signup({ form: '#signup', template: Template.instance() }); 6 | }); 7 | 8 | Template.signup.events({ 9 | 'submit form': ( event ) => event.preventDefault(), 10 | 'keyup [name="username"]' ( event ) { 11 | let value = event.target.value, 12 | formatted = sanitizeUsername( value ); 13 | event.target.value = formatted; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /code/server/publications/channel.js: -------------------------------------------------------------------------------- 1 | Meteor.publish( 'channel', function( isDirect, channel ) { 2 | check( isDirect, Boolean ); 3 | check( channel, String ); 4 | 5 | if ( isDirect ) { 6 | let user = Meteor.users.findOne( { username: channel.replace( '@', '' ) } ); 7 | return Messages.find({ 8 | $or: [ { owner: this.userId, to: user._id }, { owner: user._id, to: this.userId } ] 9 | });; 10 | } else { 11 | let selectedChannel = Channels.findOne( { name: channel } ); 12 | return Messages.find( { channel: selectedChannel._id } ); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### The Meteor Chef 2 | ###### Building a Chat Application 3 | 4 | In this recipe, we'll learn how to build a simple chat application. We'll learn how to create public channels, private channels, and see how to handle things like formatting text with Markdown. 5 | 6 | [Read on The Meteor Chef](http://themeteorchef.com/recipes/building-a-chat-application) 7 | 8 | [Demo the Recipe](http://tmc-building-a-chat-application-demo.meteor.com) 9 | 10 | [Download the Source](https://github.com/themeteorchef/building-a-chat-application/archive/master.zip) 11 | 12 | The code for this recipe is licensed under the [MIT License](http://opensource.org/licenses/MIT). 13 | -------------------------------------------------------------------------------- /code/client/helpers/flow-router.js: -------------------------------------------------------------------------------- 1 | const pathFor = ( path, view ) => { 2 | if ( path.hash ) { 3 | view = path; 4 | path = view.hash.route; 5 | delete view.hash.route; 6 | } 7 | 8 | let query = view.hash.query ? FlowRouter._qs.parse( view.hash.query ) : {}; 9 | return FlowRouter.path( path, view.hash, query ); 10 | }; 11 | 12 | Template.registerHelper( 'pathFor', pathFor ); 13 | 14 | Template.registerHelper( 'urlFor', ( path, view ) => { 15 | return Meteor.absoluteUrl( pathFor( path, view ).substr( 1 ) ); 16 | }); 17 | 18 | Template.registerHelper( 'currentRoute', ( route ) => { 19 | FlowRouter.watchPathChange(); 20 | return FlowRouter.current().route.name === route ? 'active' : ''; 21 | }); 22 | -------------------------------------------------------------------------------- /code/client/templates/authenticated/sidebar.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /code/client/templates/globals/header.js: -------------------------------------------------------------------------------- 1 | Template.header.helpers({ 2 | reminder() { 3 | let now = new Date(), 4 | days = { 5 | 0: 'Sunday', 6 | 1: 'Monday', 7 | 2: 'Tuesday', 8 | 3: 'Wednesday', 9 | 4: 'Thursday', 10 | 5: 'Friday', 11 | 6: 'Saturday' 12 | }; 13 | 14 | return `It's ${ days[ now.getDay() ] }, we still own you!`; 15 | } 16 | }); 17 | 18 | Template.header.events({ 19 | 'click .logout' ( event ) { 20 | event.preventDefault(); 21 | 22 | Meteor.logout( ( error ) => { 23 | if ( error ) { 24 | Bert.alert( error.reason, 'warning' ); 25 | } else { 26 | Bert.alert( 'Logged out!', 'success' ); 27 | } 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /code/client/templates/public/recover-password.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /code/client/templates/authenticated/channel.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /code/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | accounts-password 7 | accounts-base 8 | jquery 9 | check 10 | audit-argument-checks 11 | themeteorchef:jquery-validation 12 | browser-policy 13 | 14 | 15 | themeteorchef:bert 16 | meteorhacks:ssr 17 | 18 | 19 | ecmascript 20 | kadira:flow-router 21 | kadira:blaze-layout 22 | meteorhacks:fast-render 23 | meteor-base 24 | session 25 | templating 26 | fourseven:scss 27 | reactive-var 28 | reactive-dict 29 | aldeed:collection2 30 | tracker 31 | twbs:bootstrap 32 | momentjs:moment 33 | alanning:roles 34 | themeteorchef:seeder 35 | themeteorchef:commonmark 36 | risul:moment-timezone 37 | standard-minifier-css 38 | standard-minifier-js 39 | -------------------------------------------------------------------------------- /code/client/templates/authenticated/sidebar.js: -------------------------------------------------------------------------------- 1 | Template.sidebar.onCreated( () => { 2 | let template = Template.instance(); 3 | template.subscribe( 'sidebar' ); 4 | }); 5 | 6 | Template.sidebar.helpers({ 7 | currentChannel( name ) { 8 | let current = FlowRouter.getParam( 'channel' ); 9 | if ( current ) { 10 | return current === name || current === `@${ name }` ? 'active' : false; 11 | } 12 | }, 13 | channels() { 14 | let channels = Channels.find(); 15 | if ( channels ) { 16 | return channels; 17 | } 18 | }, 19 | users() { 20 | let users = Meteor.users.find( { _id: { $ne: Meteor.userId() } } ); 21 | if ( users ) { 22 | return users; 23 | } 24 | }, 25 | fullName( name ) { 26 | if ( name ) { 27 | return `${ name.first } ${ name.last }`; 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /code/client/helpers/template/date-time.js: -------------------------------------------------------------------------------- 1 | Template.registerHelper( 'formatDateTime', ( timestamp, format ) => { 2 | if ( timestamp && format ) { 3 | return moment( timestamp ).format( format ); 4 | } 5 | }); 6 | 7 | Template.registerHelper( 'formatDateTimeLocal', ( timestamp, timezone, format ) => { 8 | if ( timestamp && timezone && format ) { 9 | return moment( timestamp ).tz( timezone ).format( format ); 10 | } 11 | }); 12 | 13 | Template.registerHelper( 'messageTimestamp', ( timestamp ) => { 14 | if ( timestamp ) { 15 | let today = moment().format( 'YYYY-MM-DD' ), 16 | datestamp = moment( timestamp ).format( 'YYYY-MM-DD' ), 17 | isBeforeToday = moment( today ).isAfter( datestamp ), 18 | format = isBeforeToday ? 'MMMM Do, YYYY hh:mm a' : 'hh:mm a'; 19 | return moment( timestamp ).format( format ); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /code/server/accounts/email-templates.js: -------------------------------------------------------------------------------- 1 | let appName = 'Application Name', 2 | appEmail = `${ appName } `, 3 | emailTemplates = Accounts.emailTemplates; 4 | 5 | emailTemplates.siteName = appName; 6 | emailTemplates.from = appEmail; 7 | 8 | emailTemplates.resetPassword = { 9 | subject() { 10 | return `[${ appName }] Reset Your Password`; 11 | }, 12 | text( user, url ) { 13 | let emailAddress = user.emails[ 0 ].address, 14 | urlWithoutHash = url.replace( '#/', '' ); 15 | 16 | return `A password reset has been requested for the account related to this address (${ emailAddress }). To reset the password, visit the following link:\n\n${ urlWithoutHash }\n\n If you did not request this reset, please ignore this email. If you feel something is wrong, please contact our support team: ${ appEmail }.`; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /code/both/routes/public.js: -------------------------------------------------------------------------------- 1 | const publicRoutes = FlowRouter.group( { name: 'public' } ); 2 | 3 | publicRoutes.route( '/', { 4 | action() { 5 | FlowRouter.go( '/login' ); 6 | } 7 | }); 8 | 9 | publicRoutes.route( '/signup', { 10 | name: 'signup', 11 | action() { 12 | BlazeLayout.render( 'default', { yield: 'signup' } ); 13 | } 14 | }); 15 | 16 | publicRoutes.route( '/login', { 17 | name: 'login', 18 | action() { 19 | BlazeLayout.render( 'default', { yield: 'login' } ); 20 | } 21 | }); 22 | 23 | publicRoutes.route( '/recover-password', { 24 | name: 'recover-password', 25 | action() { 26 | BlazeLayout.render( 'default', { yield: 'recoverPassword' } ); 27 | } 28 | }); 29 | 30 | publicRoutes.route( '/reset-password/:token', { 31 | name: 'reset-password', 32 | action() { 33 | BlazeLayout.render( 'default', { yield: 'resetPassword' } ); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /code/client/templates/globals/loading.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /code/server/modules/seed-database.js: -------------------------------------------------------------------------------- 1 | import seed from 'meteor/themeteorchef:seeder'; 2 | 3 | let _seedUsers = () => { 4 | Seed( 'users', { 5 | environments: [ 'development', 'staging', 'production' ], 6 | data: [{ 7 | username: 'bigguy1991', 8 | email: 'admin@admin.com', 9 | password: 'password', 10 | profile: { 11 | name: { first: 'Carl', last: 'Winslow' } 12 | }, 13 | roles: [ 'admin' ] 14 | },{ 15 | username: 'beetsfan123', 16 | email: 'doug@admin.com', 17 | password: 'password', 18 | profile: { 19 | name: { first: 'Doug', last: 'Funnie' } 20 | }, 21 | roles: [ 'admin' ] 22 | }] 23 | }); 24 | }; 25 | 26 | let _seedChannels = () => { 27 | Seed( 'channels', { 28 | environments: [ 'development', 'staging', 'production' ], 29 | data: [ { name: 'general' } ] 30 | }); 31 | }; 32 | 33 | export default function() { 34 | _seedUsers(); 35 | _seedChannels(); 36 | } 37 | -------------------------------------------------------------------------------- /code/client/modules/handle-channel-switch.js: -------------------------------------------------------------------------------- 1 | import setScroll from './set-scroll'; 2 | 3 | let _establishSubscription = ( template, isDirect, channel ) => { 4 | template.subscribe( 'channel', isDirect, channel, () => { 5 | setScroll( 'messages' ); 6 | setTimeout( () => { template.loading.set( false ); }, 300 ); 7 | }); 8 | }; 9 | 10 | let _handleSwitch = ( template ) => { 11 | let channel = FlowRouter.getParam( 'channel' ); 12 | 13 | if ( channel ) { 14 | let isDirect = channel.includes( '@' ); 15 | template.isDirect.set( isDirect ); 16 | template.loading.set( true ); 17 | _establishSubscription( template, isDirect, channel ); 18 | } 19 | }; 20 | 21 | let _setupReactiveVariables = ( template ) => { 22 | template.isDirect = new ReactiveVar(); 23 | template.loading = new ReactiveVar( true ); 24 | }; 25 | 26 | export default function( template ) { 27 | _setupReactiveVariables( template ); 28 | Tracker.autorun( () => { _handleSwitch( template ); } ); 29 | } 30 | -------------------------------------------------------------------------------- /code/client/templates/authenticated/channel.js: -------------------------------------------------------------------------------- 1 | import handleChannelSwitch from '../../modules/handle-channel-switch'; 2 | import sortMessages from '../../modules/sort-messages'; 3 | import handleMessageInsert from '../../modules/handle-message-insert'; 4 | 5 | Template.channel.onCreated( () => { 6 | let template = Template.instance(); 7 | handleChannelSwitch( template ); 8 | }); 9 | 10 | Template.channel.helpers({ 11 | isLoading() { 12 | return Template.instance().loading.get(); 13 | }, 14 | isDirect() { 15 | return Template.instance().isDirect.get(); 16 | }, 17 | username() { 18 | return FlowRouter.getParam( 'channel' ); 19 | }, 20 | messages() { 21 | let messages = Messages.find( {}, { sort: { timestamp: 1 } } ); 22 | if ( messages ) { 23 | return sortMessages( messages ); 24 | } 25 | } 26 | }); 27 | 28 | Template.channel.events({ 29 | 'keyup [name="message"]' ( event, template ) { 30 | handleMessageInsert( event, template ); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /code/collections/messages.js: -------------------------------------------------------------------------------- 1 | Messages = new Mongo.Collection( 'messages' ); 2 | 3 | Messages.allow({ 4 | insert: () => false, 5 | update: () => false, 6 | remove: () => false 7 | }); 8 | 9 | Messages.deny({ 10 | insert: () => true, 11 | update: () => true, 12 | remove: () => true 13 | }); 14 | 15 | let MessagesSchema = new SimpleSchema({ 16 | 'channel': { 17 | type: String, 18 | label: 'The ID of the channel this message belongs to.', 19 | optional: true 20 | }, 21 | 'to': { 22 | type: String, 23 | label: 'The ID of the user this message was sent directly to.', 24 | optional: true 25 | }, 26 | 'owner': { 27 | type: String, 28 | label: 'The ID of the user that created this message.' 29 | }, 30 | 'timestamp': { 31 | type: Date, 32 | label: 'The date and time this message was created.' 33 | }, 34 | 'message': { 35 | type: String, 36 | label: 'The content of this message.' 37 | } 38 | }); 39 | 40 | Messages.attachSchema( MessagesSchema ); 41 | -------------------------------------------------------------------------------- /code/client/templates/public/login.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /code/client/modules/recover-password.js: -------------------------------------------------------------------------------- 1 | let template; 2 | 3 | let _handleRecovery = () => { 4 | let email = template.find( '[name="emailAddress"]' ).value; 5 | 6 | Accounts.forgotPassword( { email: email }, ( error ) => { 7 | if ( error ) { 8 | Bert.alert( error.reason, 'warning' ); 9 | } else { 10 | Bert.alert( 'Check your inbox for a reset link!', 'success' ); 11 | } 12 | }); 13 | }; 14 | 15 | let validation = () => { 16 | return { 17 | rules: { 18 | emailAddress: { 19 | required: true, 20 | email: true 21 | } 22 | }, 23 | messages: { 24 | emailAddress: { 25 | required: 'Need an email address here.', 26 | email: 'Is this email address legit?' 27 | } 28 | }, 29 | submitHandler() { _handleRecovery(); } 30 | }; 31 | }; 32 | 33 | let _validate = ( form ) => { 34 | $( form ).validate( validation() ); 35 | }; 36 | 37 | export default function( options ) { 38 | template = options.template; 39 | _validate( options.form ); 40 | } 41 | -------------------------------------------------------------------------------- /code/client/modules/sort-messages.js: -------------------------------------------------------------------------------- 1 | let _getTimeDifference = ( previousTime, currentTime ) => { 2 | let previous = moment( previousTime ), 3 | current = moment( currentTime ); 4 | return moment( current ).diff( previous, 'minutes' ); 5 | } 6 | 7 | let _checkIfOwner = ( previousMessage, message ) => { 8 | return typeof previousMessage !== 'undefined' && previousMessage.owner === message.owner; 9 | }; 10 | 11 | let _decideIfShowHeader = ( previousMessage, message ) => { 12 | if ( _checkIfOwner( previousMessage, message ) ) { 13 | message.showHeader = _getTimeDifference( previousMessage.timestamp, message.timestamp ) >= 5; 14 | } else { 15 | message.showHeader = true; 16 | } 17 | }; 18 | 19 | let _mapMessages = ( messages ) => { 20 | let previousMessage; 21 | return messages.map( ( message ) => { 22 | _decideIfShowHeader( previousMessage, message ); 23 | previousMessage = message; 24 | return message; 25 | }); 26 | }; 27 | 28 | export default function( messages ) { 29 | return _mapMessages( messages ); 30 | } 31 | -------------------------------------------------------------------------------- /code/client/templates/public/reset-password.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /code/client/templates/layouts/default.js: -------------------------------------------------------------------------------- 1 | const handleRedirect = ( routes, redirect ) => { 2 | let currentRoute = FlowRouter.getRouteName(); 3 | if ( routes.indexOf( currentRoute ) > -1 ) { 4 | FlowRouter.go( redirect ); 5 | return true; 6 | } 7 | }; 8 | 9 | Template.default.onRendered( () => { 10 | Tracker.autorun( () => { 11 | let isChannel = FlowRouter.getParam( 'channel' ), 12 | bodyClasses = document.body.classList; 13 | 14 | return isChannel ? bodyClasses.add( 'is-channel' ) : bodyClasses.remove( 'is-channel' ); 15 | }); 16 | }); 17 | 18 | Template.default.helpers({ 19 | loggingIn() { 20 | return Meteor.loggingIn(); 21 | }, 22 | authenticated() { 23 | return !Meteor.loggingIn() && Meteor.user(); 24 | }, 25 | redirectAuthenticated() { 26 | return handleRedirect([ 27 | 'login', 28 | 'signup', 29 | 'recover-password', 30 | 'reset-password' 31 | ], '/messages/general' ); 32 | }, 33 | redirectPublic() { 34 | return handleRedirect( [ 'channel' ], '/login' ); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es6': true, 4 | 'meteor': true, 5 | 'node': true 6 | }, 7 | 'extends': 'eslint:recommended', 8 | 'ecmaFeatures': { 9 | 'jsx': true, 10 | 'modules': true, 11 | 'experimentalObjectRestSpread': true 12 | }, 13 | 'plugins': [], 14 | 'globals': { 15 | 'Bert': true, 16 | 'BlazeLayout': true, 17 | 'Channels': true, 18 | 'Counts': true, 19 | 'document': true, 20 | 'FlowRouter': true, 21 | 'Messages': true, 22 | 'moment': true, 23 | 'parseMarkdown': true, 24 | 'ReadLog': true, 25 | 'Roles': true, 26 | 'SimpleSchema': true 27 | }, 28 | 'rules': { 29 | 'comma-dangle': [ 2, 'never' ], 30 | 'computed-property-spacing': [ 2, 'always' ], 31 | 'eqeqeq': [ 2, 'smart' ], 32 | 'indent': [ 2, 2, { 'VariableDeclarator': 2 } ], 33 | 'linebreak-style': [ 2, 'unix' ], 34 | 'no-console': [ 0 ], 35 | 'no-unneeded-ternary': [ 2 ], 36 | 'object-curly-spacing': [ 2, 'always' ], 37 | 'quotes': [ 2, 'single' ], 38 | 'semi': [ 2, 'always' ], 39 | 'space-infix-ops': [ 2 ] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /code/client/modules/handle-message-insert.js: -------------------------------------------------------------------------------- 1 | import setScroll from './set-scroll'; 2 | 3 | let _handleInsert = ( message, event, template ) => { 4 | Meteor.call( 'insertMessage', message, ( error ) => { 5 | if ( error ) { 6 | Bert.alert( error.reason, 'danger' ); 7 | } else { 8 | event.target.value = ''; 9 | } 10 | }); 11 | }; 12 | 13 | let _buildMessage = ( template, text ) => { 14 | return { 15 | destination: FlowRouter.getParam( 'channel' ).replace( '@', '' ), 16 | isDirect: template.isDirect.get(), 17 | message: text 18 | }; 19 | }; 20 | 21 | let _checkIfCanInsert = ( message, event ) => { 22 | return message !== '' && event.keyCode === 13; 23 | }; 24 | 25 | let _getMessage = ( template ) => { 26 | let message = template.find( '[name="message"]' ).value; 27 | return message.trim(); 28 | }; 29 | 30 | export default function( event, template ) { 31 | let text = _getMessage( template ), 32 | canInsert = _checkIfCanInsert( text, event ); 33 | 34 | if ( canInsert ) { 35 | setScroll( 'messages' ); 36 | _handleInsert( _buildMessage( template, text ), event, template ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/client/modules/login.js: -------------------------------------------------------------------------------- 1 | let template; 2 | 3 | let _handleLogin = () => { 4 | let email = template.find( '[name="emailAddress"]' ).value, 5 | password = template.find( '[name="password"]' ).value; 6 | 7 | Meteor.loginWithPassword( email, password, ( error ) => { 8 | if ( error ) { 9 | Bert.alert( error.reason, 'warning' ); 10 | } else { 11 | Bert.alert( 'Logged in!', 'success' ); 12 | } 13 | }); 14 | }; 15 | 16 | let validation = () => { 17 | return { 18 | rules: { 19 | emailAddress: { 20 | required: true, 21 | email: true 22 | }, 23 | password: { 24 | required: true 25 | } 26 | }, 27 | messages: { 28 | emailAddress: { 29 | required: 'Need an email address here.', 30 | email: 'Is this email address legit?' 31 | }, 32 | password: { 33 | required: 'Need a password here.' 34 | } 35 | }, 36 | submitHandler() { _handleLogin(); } 37 | }; 38 | }; 39 | 40 | let _validate = ( form ) => { 41 | $( form ).validate( validation() ); 42 | }; 43 | 44 | export default function( options ) { 45 | template = options.template; 46 | _validate( options.form ); 47 | } 48 | -------------------------------------------------------------------------------- /code/client/modules/reset-password.js: -------------------------------------------------------------------------------- 1 | let template; 2 | 3 | let _handleReset = () => { 4 | var token = FlowRouter.getParam( 'token' ), 5 | password = template.find( '[name="newPassword"]' ).value; 6 | 7 | Accounts.resetPassword( token, password, ( error ) => { 8 | if ( error ) { 9 | Bert.alert( error.reason, 'danger' ); 10 | } else { 11 | Bert.alert( 'Password reset!', 'success' ); 12 | } 13 | }); 14 | }; 15 | 16 | let validation = () => { 17 | return { 18 | rules: { 19 | newPassword: { 20 | required: true, 21 | minlength: 6 22 | }, 23 | repeatNewPassword: { 24 | required: true, 25 | minlength: 6, 26 | equalTo: '[name="newPassword"]' 27 | } 28 | }, 29 | messages: { 30 | newPassword: { 31 | required: 'Enter a new password, please.', 32 | minlength: 'Use at least six characters, please.' 33 | }, 34 | repeatNewPassword: { 35 | required: 'Repeat your new password, please.', 36 | equalTo: 'Hmm, your passwords don\'t match. Try again?' 37 | } 38 | }, 39 | submitHandler() { _handleReset(); } 40 | }; 41 | }; 42 | 43 | let _validate = ( form ) => { 44 | $( form ).validate( validation() ); 45 | }; 46 | 47 | export default function ( options ) { 48 | template = options.template; 49 | _validate( options.form ); 50 | } 51 | -------------------------------------------------------------------------------- /code/server/modules/insert-message.js: -------------------------------------------------------------------------------- 1 | let _insertMessage = ( message ) => { 2 | return Messages.insert( message ); 3 | }; 4 | 5 | let _escapeUnwantedMarkdown = ( message ) => { 6 | // Escape h1-h6 tags and inline images ![]() in Markdown. 7 | return message 8 | .replace( /#/g, '#' ) 9 | .replace( /(!\[.*?\]\()(.*?)(\))+/g, '![]()' ); 10 | }; 11 | 12 | let _cleanUpMessageBeforeInsert = ( message ) => { 13 | delete message.destination; 14 | delete message.isDirect; 15 | message.message = _escapeUnwantedMarkdown( message.message ); 16 | }; 17 | 18 | let _getChannelId = ( channelName ) => { 19 | let channel = Channels.findOne( { name: channelName } ); 20 | if ( channel ) { 21 | return channel._id; 22 | } 23 | }; 24 | 25 | let _getUserId = ( username ) => { 26 | let user = Meteor.users.findOne( { username: username } ); 27 | if ( user ) { 28 | return user._id; 29 | } 30 | }; 31 | 32 | let _assignDestination = ( message ) => { 33 | if ( message.isDirect ) { 34 | message.to = _getUserId( message.destination ); 35 | } else { 36 | let channelId = _getChannelId( message.destination ); 37 | message.channel = channelId; 38 | } 39 | }; 40 | 41 | let _checkIfSelf = ( { destination, owner } ) => { 42 | return destination === owner; 43 | }; 44 | 45 | let _assignOwnerAndTimestamp = ( message ) => { 46 | message.owner = Meteor.userId(); 47 | message.timestamp = new Date(); 48 | }; 49 | 50 | export default function( message ) { 51 | _assignOwnerAndTimestamp( message ); 52 | 53 | if ( !_checkIfSelf( message ) ) { 54 | _assignDestination( message ); 55 | _cleanUpMessageBeforeInsert( message ); 56 | _insertMessage( message ); 57 | } else { 58 | throw new Meteor.Error( '500', 'Can\'t send messages to yourself.' ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /code/client/templates/public/signup.html: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /code/client/modules/signup.js: -------------------------------------------------------------------------------- 1 | let template; 2 | 3 | let _handleSignup = () => { 4 | let user = { 5 | username: template.find( '[name="username"]').value, 6 | email: template.find( '[name="emailAddress"]' ).value, 7 | password: template.find( '[name="password"]' ).value, 8 | profile: { 9 | name: { 10 | first: template.find( '[name="firstName"]' ).value, 11 | last: template.find( '[name="lastName"]' ).value 12 | } 13 | } 14 | }; 15 | 16 | Accounts.createUser( user, ( error ) => { 17 | if ( error ) { 18 | Bert.alert( error.reason, 'danger' ); 19 | } else { 20 | Bert.alert( 'Welcome!', 'success' ); 21 | } 22 | }); 23 | }; 24 | 25 | let validation = () => { 26 | return { 27 | rules: { 28 | firstName: { 29 | required: true 30 | }, 31 | lastName: { 32 | required: true 33 | }, 34 | username: { 35 | required: true, 36 | minlength: 6, 37 | maxlength: 20 38 | }, 39 | emailAddress: { 40 | required: true, 41 | email: true 42 | }, 43 | password: { 44 | required: true, 45 | minlength: 6 46 | } 47 | }, 48 | messages: { 49 | firstName: { 50 | required: 'What is your first name?' 51 | }, 52 | lastName: { 53 | required: 'How about a second name?' 54 | }, 55 | username: { 56 | required: 'What username would you like?' 57 | }, 58 | emailAddress: { 59 | required: 'Need an email address here.', 60 | email: 'Is this email address legit?' 61 | }, 62 | password: { 63 | required: 'Need a password here.', 64 | minlength: 'Use at least six characters, please.' 65 | } 66 | }, 67 | errorPlacement( error, element ) { 68 | if ( element.attr( 'name' ) === 'username' ) { 69 | error.insertAfter( '.input-group.username' ); 70 | } 71 | }, 72 | submitHandler() { _handleSignup(); } 73 | }; 74 | }; 75 | 76 | let _validate = ( form ) => { 77 | $( form ).validate( validation() ); 78 | }; 79 | 80 | export default function( options ) { 81 | template = options.template; 82 | _validate( options.form ); 83 | } 84 | -------------------------------------------------------------------------------- /code/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.3-beta.12 2 | accounts-password@1.1.5-beta.12 3 | alanning:roles@1.2.14 4 | aldeed:collection2@2.6.1 5 | aldeed:simple-schema@1.3.3 6 | allow-deny@1.0.1-beta.12 7 | audit-argument-checks@1.0.4 8 | autoupdate@1.2.5-beta.12 9 | babel-compiler@6.5.0-beta.12 10 | babel-runtime@0.1.5-beta.12 11 | base64@1.0.5-beta.12 12 | binary-heap@1.0.5-beta.12 13 | blaze@2.1.4-beta.12 14 | blaze-tools@1.0.5-beta.12 15 | boilerplate-generator@1.0.5-beta.12 16 | browser-policy@1.0.6-beta.12 17 | browser-policy-common@1.0.5-beta.12 18 | browser-policy-content@1.0.7-beta.12 19 | browser-policy-framing@1.0.7-beta.12 20 | caching-compiler@1.0.1-beta.12 21 | caching-html-compiler@1.0.3-beta.12 22 | callback-hook@1.0.5-beta.12 23 | check@1.1.1-beta.12 24 | chuangbo:cookie@1.1.0 25 | coffeescript@1.0.12-beta.12 26 | cosmos:browserify@0.9.2 27 | ddp@1.2.2 28 | ddp-client@1.2.2-beta.12 29 | ddp-common@1.2.2 30 | ddp-rate-limiter@1.0.1-beta.12 31 | ddp-server@1.2.3-beta.12 32 | deps@1.0.9 33 | diff-sequence@1.0.2-beta.12 34 | ecmascript@0.4.0-beta.12 35 | ecmascript-runtime@0.2.7-beta.12 36 | ejson@1.0.8-beta.12 37 | email@1.0.9-beta.12 38 | fortawesome:fontawesome@4.4.0_1 39 | fourseven:scss@3.4.1 40 | geojson-utils@1.0.5-beta.12 41 | hot-code-push@1.0.1-beta.12 42 | html-tools@1.0.6-beta.12 43 | htmljs@1.0.6-beta.12 44 | http@1.1.2-beta.12 45 | id-map@1.0.4 46 | jquery@1.11.5-beta.12 47 | kadira:blaze-layout@2.3.0 48 | kadira:flow-router@2.10.0 49 | livedata@1.0.15 50 | localstorage@1.0.6-beta.12 51 | logging@1.0.9-beta.12 52 | meteor@1.1.11-beta.12 53 | meteor-base@1.0.1 54 | meteor-env-dev@0.0.2-beta.12 55 | meteor-env-prod@0.0.2-beta.12 56 | meteorhacks:fast-render@2.11.0 57 | meteorhacks:inject-data@1.4.1 58 | meteorhacks:picker@1.0.3 59 | meteorhacks:ssr@2.2.0 60 | minifier-css@1.1.8-beta.12 61 | minifier-js@1.1.8-beta.12 62 | minimongo@1.0.11-beta.12 63 | modules@0.5.0-beta.12 64 | modules-runtime@0.5.0-beta.12 65 | momentjs:moment@2.11.2 66 | mongo@1.1.4-beta.12 67 | mongo-id@1.0.1 68 | npm-bcrypt@0.7.8_2 69 | npm-mongo@1.4.40-beta.12 70 | observe-sequence@1.0.8-beta.12 71 | ordered-dict@1.0.4 72 | promise@0.5.2-beta.12 73 | random@1.0.6-beta.12 74 | rate-limit@1.0.1-beta.12 75 | reactive-dict@1.1.4-beta.12 76 | reactive-var@1.0.6 77 | reload@1.1.5-beta.12 78 | retry@1.0.4 79 | risul:moment-timezone@0.5.0_5 80 | routepolicy@1.0.7-beta.12 81 | service-configuration@1.0.6-beta.12 82 | session@1.1.2-beta.12 83 | sha@1.0.4 84 | spacebars@1.0.8-beta.12 85 | spacebars-compiler@1.0.8-beta.12 86 | srp@1.0.5-beta.12 87 | standard-minifier-css@1.0.3-beta.12 88 | standard-minifier-js@1.0.3-beta.12 89 | templating@1.1.6-beta.12 90 | templating-tools@1.0.1-beta.12 91 | themeteorchef:bert@2.1.0 92 | themeteorchef:commonmark@1.1.0 93 | themeteorchef:jquery-validation@1.14.0 94 | themeteorchef:seeder@0.2.0 95 | tracker@1.0.10-beta.12 96 | twbs:bootstrap@3.3.6 97 | ui@1.0.8 98 | underscore@1.0.5-beta.12 99 | url@1.0.6-beta.12 100 | webapp@1.2.5-beta.12 101 | webapp-hashing@1.0.6-beta.12 102 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_channel.scss: -------------------------------------------------------------------------------- 1 | $red: #DA5347; 2 | $blue: #1B9EDB; 3 | $green: #75BA50; 4 | 5 | body.is-channel .container { 6 | width: 100%; 7 | } 8 | 9 | .sidebar, 10 | .conversation { 11 | position: fixed; 12 | top: 52px; 13 | bottom: 0; 14 | height: 100%; 15 | } 16 | 17 | .sidebar { 18 | left: 0; 19 | width: 250px; 20 | background: $blue; 21 | padding: 20px; 22 | 23 | h5 { 24 | text-transform: uppercase; 25 | font-size: 12px; 26 | color: darken( $blue, 20% ); 27 | margin: 0; 28 | } 29 | 30 | ul { 31 | margin: 10px 0 0 -20px; 32 | padding: 0; 33 | list-style: none; 34 | } 35 | 36 | ul:not( :last-child ) { 37 | margin-bottom: 20px; 38 | } 39 | 40 | li { 41 | font-size: 15px; 42 | } 43 | 44 | a { 45 | display: block; 46 | padding: 5px 10px 5px 20px; 47 | border-radius: 0 3px 3px 0; 48 | color: lighten( $blue, 40% ); 49 | text-decoration: none; 50 | border-left: none; 51 | } 52 | 53 | a:hover { 54 | background: darken( $blue, 10% ); 55 | } 56 | 57 | .active a, 58 | .active a:hover { 59 | background: #fff; 60 | color: $blue; 61 | border-left: none; 62 | 63 | .unread { 64 | background: $blue; 65 | color: #fff; 66 | } 67 | } 68 | 69 | a .unread { 70 | position: relative; 71 | top: 2px; 72 | float: right; 73 | background: #fff; 74 | color: $blue; 75 | font-size: 11px; 76 | font-weight: bold; 77 | width: 25px; 78 | text-align: center; 79 | padding: 1px 0 2px; 80 | border-radius: 30px; 81 | } 82 | } 83 | 84 | .conversation { 85 | left: 250px; 86 | width: calc( 100% - 250px ); 87 | background: #fff; 88 | border-top: 1px solid #eee; 89 | 90 | .message-input { 91 | background: #fff; 92 | position: absolute; 93 | bottom: 52px; 94 | left: 0; 95 | right: 0; 96 | padding: 15px; 97 | border-top: 1px solid #eee; 98 | } 99 | 100 | .message-input input { 101 | display: block; 102 | width: 100%; 103 | border: 2px solid #eee; 104 | border-radius: 3px; 105 | height: 40px; 106 | padding: 5px 10px; 107 | } 108 | 109 | .message-input input:focus { 110 | outline: 0; 111 | border-color: #ddd; 112 | } 113 | 114 | #messages { 115 | display: flex; 116 | overflow: auto; 117 | height: calc( 100% - 122px ); 118 | background: #fff; 119 | padding: 20px 20px 0; 120 | } 121 | 122 | .messages-list { 123 | width: 100%; 124 | max-height: 100%; 125 | align-self: flex-end; 126 | opacity: 1; 127 | 128 | &.loading-list { 129 | opacity: 0; 130 | } 131 | } 132 | 133 | .messages-list .message:not( :last-child ), 134 | .messages-list svg { 135 | margin-bottom: 15px; 136 | } 137 | 138 | .messages-list .message:last-child { 139 | margin-bottom: 20px; 140 | } 141 | 142 | .message header h4 { 143 | font-size: 14px; 144 | } 145 | 146 | .message header h4 span { 147 | display: inline-block; 148 | margin-left: 5px; 149 | font-size: 13px; 150 | font-weight: normal; 151 | color: #888; 152 | } 153 | 154 | .message > *:first-child, 155 | .message > header > *:first-child { 156 | margin-top: 0px; 157 | } 158 | 159 | .message > *:last-child, 160 | .message > .body > *:last-child { 161 | margin-bottom: 0px; 162 | } 163 | } 164 | --------------------------------------------------------------------------------