├── .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 |
2 | Error [404]: {{currentPath}} does not exist.
3 |
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 |
2 |
6 |
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 | | Base Version |
8 | v4.0.0 |
9 |
10 |
11 | | Meteor Version |
12 | v1.3.0 |
13 |
14 |
15 |
16 |
17 | [Read the Documentation](http://themeteorchef.com/base)
18 |
--------------------------------------------------------------------------------
/code/client/templates/authenticated/message.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#if showHeader}}
4 |
5 | {{name owner}} {{messageTimestamp timestamp}}
6 |
7 | {{/if}}
8 |
9 |
10 | {{{parseMarkdown message}}}
11 |
12 |
13 |
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 |
2 |
10 |
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 |
2 | {{>bertAlert}}
3 | {{>header}}
4 |
5 | {{#if loggingIn}}
6 | {{> loading}}
7 | {{else}}
8 | {{#if authenticated}}
9 | {{#unless redirectAuthenticated}}
10 | {{> Template.dynamic template=yield}}
11 | {{/unless}}
12 | {{else}}
13 | {{#unless redirectPublic}}
14 | {{> Template.dynamic template=yield}}
15 | {{/unless}}
16 | {{/if}}
17 | {{/if}}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/code/client/templates/globals/header.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
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 |
2 |
25 |
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 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/code/client/templates/authenticated/channel.html:
--------------------------------------------------------------------------------
1 |
2 | {{> sidebar}}
3 |
4 |
5 |
6 |
7 | {{#each messages}}
8 | {{> message}}
9 | {{else}}
10 | {{#if isDirect}}
11 |
You haven't said anything to {{username}} yet. Say hello!
12 | {{else}}
13 |
This channel is pretty quiet. You should say something!
14 | {{/if}}
15 | {{/each}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
2 |
11 |
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 |
2 |
3 |
4 |
5 |
18 |
Don't have an account? Sign Up.
19 |
20 |
21 |
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 |
2 |
21 |
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 |
2 |
3 |
4 |
5 |
39 |
Already have an account? Log In.
40 |
41 |
42 |
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 |
--------------------------------------------------------------------------------