├── .gitignore ├── .meteor ├── .gitignore ├── release ├── platforms ├── .id ├── cordova-plugins ├── .finished-upgraders ├── packages └── versions ├── manuals ├── templates │ ├── footer.md.tmpl │ ├── step14.md.tmpl │ ├── step13.md.tmpl │ ├── step3.md.tmpl │ ├── step4.md.tmpl │ ├── step10.md.tmpl │ ├── root.md.tmpl │ ├── step8.md.tmpl │ ├── step11.md.tmpl │ ├── step9.md.tmpl │ ├── step2.md.tmpl │ ├── step6.md.tmpl │ ├── step12.md.tmpl │ ├── step7.md.tmpl │ ├── step1.md.tmpl │ └── step5.md.tmpl └── views │ ├── root.md │ ├── step14.md │ ├── step3.md │ ├── step13.md │ ├── step4.md │ ├── step5.md │ └── step10.md ├── client ├── imports │ ├── app │ │ ├── app.html │ │ ├── app.component.ts │ │ ├── app.scss │ │ └── app.module.ts │ ├── pages │ │ ├── login │ │ │ ├── login.scss │ │ │ ├── login.html │ │ │ └── login.ts │ │ ├── chats │ │ │ ├── chats-options.scss │ │ │ ├── new-chat.scss │ │ │ ├── chats.scss │ │ │ ├── chats-options.html │ │ │ ├── new-chat.html │ │ │ ├── chats.html │ │ │ ├── chats-options.ts │ │ │ ├── new-chat.ts │ │ │ └── chats.ts │ │ ├── messages │ │ │ ├── show-picture.scss │ │ │ ├── messages-options.scss │ │ │ ├── location-message.scss │ │ │ ├── messages-options.html │ │ │ ├── show-picture.html │ │ │ ├── show-picture.ts │ │ │ ├── messages-attachments.html │ │ │ ├── location-message.html │ │ │ ├── messages-attachments.scss │ │ │ ├── messages-attachments.ts │ │ │ ├── messages-options.ts │ │ │ ├── location-message.ts │ │ │ ├── messages.html │ │ │ ├── messages.scss │ │ │ └── messages.ts │ │ ├── verification │ │ │ ├── verification.scss │ │ │ ├── verification.html │ │ │ └── verification.ts │ │ └── profile │ │ │ ├── profile.scss │ │ │ ├── profile.html │ │ │ └── profile.ts │ ├── theme │ │ ├── variables.scss │ │ ├── ionicons.scss │ │ └── components.scss │ └── services │ │ ├── phone.ts │ │ └── picture.ts ├── main.html ├── main.scss └── main.ts ├── public └── assets │ ├── message-mine.png │ ├── chat-background.jpg │ ├── message-other.png │ └── default-profile-pic.svg ├── imports ├── collections │ ├── index.ts │ ├── messages.ts │ ├── chats.ts │ ├── users.ts │ └── pictures.ts └── models.ts ├── declarations.d.ts ├── private └── settings.json ├── tsconfig.json ├── package.json ├── fonts.json ├── server ├── main.ts ├── publications.ts └── methods.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /manuals/templates/footer.md.tmpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.4.2.3 2 | -------------------------------------------------------------------------------- /manuals/views/root.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /client/imports/app/app.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/message-mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urigo/Ionic2-MeteorCLI-WhatsApp/HEAD/public/assets/message-mine.png -------------------------------------------------------------------------------- /public/assets/chat-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urigo/Ionic2-MeteorCLI-WhatsApp/HEAD/public/assets/chat-background.jpg -------------------------------------------------------------------------------- /public/assets/message-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Urigo/Ionic2-MeteorCLI-WhatsApp/HEAD/public/assets/message-other.png -------------------------------------------------------------------------------- /imports/collections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chats'; 2 | export * from './messages'; 3 | export * from './pictures'; 4 | export * from './users'; 5 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | A wildcard module is declared below to allow third party libraries to be used in an app even if they don't 3 | provide their own type declarations. 4 | */ 5 | declare module '*'; -------------------------------------------------------------------------------- /imports/collections/messages.ts: -------------------------------------------------------------------------------- 1 | import { MongoObservable } from 'meteor-rxjs'; 2 | import { Message } from '../models'; 3 | 4 | export const Messages = new MongoObservable.Collection('messages'); -------------------------------------------------------------------------------- /client/imports/pages/login/login.scss: -------------------------------------------------------------------------------- 1 | .login-page-content { 2 | .instructions { 3 | text-align: center; 4 | font-size: medium; 5 | margin: 50px; 6 | } 7 | 8 | .text-input { 9 | text-align: center; 10 | } 11 | } -------------------------------------------------------------------------------- /client/imports/pages/chats/chats-options.scss: -------------------------------------------------------------------------------- 1 | .chats-options-page-content { 2 | .options { 3 | margin: 0; 4 | } 5 | 6 | .option-name { 7 | float: left; 8 | } 9 | 10 | .option-icon { 11 | float: right; 12 | } 13 | } -------------------------------------------------------------------------------- /client/imports/pages/messages/show-picture.scss: -------------------------------------------------------------------------------- 1 | .show-picture { 2 | background-color: black; 3 | 4 | .picture { 5 | position: absolute; 6 | top: 50%; 7 | left: 50%; 8 | transform: translate(-50%, -50%); 9 | } 10 | } -------------------------------------------------------------------------------- /client/imports/pages/messages/messages-options.scss: -------------------------------------------------------------------------------- 1 | .chats-options-page-content { 2 | .options { 3 | margin: 0; 4 | } 5 | 6 | .option-name { 7 | float: left; 8 | } 9 | 10 | .option-icon { 11 | float: right; 12 | } 13 | } -------------------------------------------------------------------------------- /private/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "accounts-phone": { 3 | "verificationWaitTime": 0, 4 | "verificationRetriesWaitTime": 0, 5 | "adminPhoneNumbers": ["+9721234567", "+97212345678", "+97212345679"], 6 | "phoneVerificationMasterCode": "1234" 7 | } 8 | } -------------------------------------------------------------------------------- /client/imports/pages/verification/verification.scss: -------------------------------------------------------------------------------- 1 | .verification-page-content { 2 | .instructions { 3 | text-align: center; 4 | font-size: medium; 5 | margin: 50px; 6 | } 7 | 8 | .text-input { 9 | text-align: center; 10 | } 11 | } -------------------------------------------------------------------------------- /client/imports/pages/chats/new-chat.scss: -------------------------------------------------------------------------------- 1 | .new-chat { 2 | .user-picture { 3 | border-radius: 50%; 4 | width: 50px; 5 | float: left; 6 | } 7 | 8 | .user-name { 9 | margin-left: 20px; 10 | margin-top: 25px; 11 | transform: translate(0, -50%); 12 | float: left; 13 | } 14 | } -------------------------------------------------------------------------------- /client/imports/pages/messages/location-message.scss: -------------------------------------------------------------------------------- 1 | .location-message-content { 2 | .scroll-content { 3 | margin-top: 44px; 4 | } 5 | 6 | sebm-google-map { 7 | padding: 0; 8 | } 9 | 10 | .sebm-google-map-container { 11 | height: 300px; 12 | margin-top: -15px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.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 | 1h3nx061w8uib2b7353t 8 | -------------------------------------------------------------------------------- /.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | cordova-plugin-camera@2.3.1 2 | cordova-plugin-console@1.0.5 3 | cordova-plugin-device@1.1.4 4 | cordova-plugin-geolocation@2.4.1 5 | cordova-plugin-image-picker@1.1.3 6 | cordova-plugin-sim@1.2.1 7 | cordova-plugin-splashscreen@4.0.1 8 | cordova-plugin-statusbar@2.2.1 9 | cordova-plugin-whitelist@1.3.1 10 | ionic-plugin-keyboard@1.1.4 11 | -------------------------------------------------------------------------------- /client/imports/pages/messages/messages-options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /imports/collections/chats.ts: -------------------------------------------------------------------------------- 1 | import { MongoObservable } from 'meteor-rxjs'; 2 | import { Chat } from '../models'; 3 | import { Messages } from './messages'; 4 | 5 | export const Chats = new MongoObservable.Collection('chats'); 6 | 7 | // Dispose unused messages 8 | Chats.collection.after.remove(function (userId, doc) { 9 | Messages.collection.remove({ chatId: doc._id }); 10 | }); 11 | -------------------------------------------------------------------------------- /client/imports/pages/chats/chats.scss: -------------------------------------------------------------------------------- 1 | .chats-page-content { 2 | .chat-picture { 3 | border-radius: 50%; 4 | width: 50px; 5 | float: left; 6 | } 7 | 8 | .chat-info { 9 | float: left; 10 | margin: 10px 0 0 20px; 11 | 12 | .last-message-timestamp { 13 | position: absolute; 14 | top: 10px; 15 | right: 10px; 16 | font-size: 14px; 17 | color: #9A9898; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | Ionic2-MeteorCLI-WhatsApp 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/imports/pages/profile/profile.scss: -------------------------------------------------------------------------------- 1 | .profile-page-content { 2 | .profile-picture { 3 | max-width: 300px; 4 | display: block; 5 | margin: auto; 6 | 7 | img { 8 | margin-bottom: -33px; 9 | width: 100%; 10 | } 11 | 12 | ion-icon { 13 | float: right; 14 | font-size: 30px; 15 | opacity: 0.5; 16 | border-left: black solid 1px; 17 | padding-left: 5px; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /client/imports/pages/messages/show-picture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Show Picture 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/imports/pages/messages/show-picture.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavParams, ViewController } from 'ionic-angular'; 3 | import template from './show-picture.html'; 4 | 5 | @Component({ 6 | template 7 | }) 8 | export class ShowPictureComponent { 9 | pictureSrc: string; 10 | 11 | constructor(private navParams: NavParams, private viewCtrl: ViewController) { 12 | this.pictureSrc = navParams.get('pictureSrc'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/main.scss: -------------------------------------------------------------------------------- 1 | // Theme 2 | @import "imports/theme/variables"; 3 | 4 | // App 5 | @import "imports/app/app"; 6 | 7 | // Pages 8 | @import "imports/pages/chats/chats"; 9 | @import "imports/pages/chats/new-chat"; 10 | @import "imports/pages/chats/chats-options"; 11 | @import "imports/pages/login/login"; 12 | @import "imports/pages/messages/messages"; 13 | @import "imports/pages/messages/messages-options"; 14 | @import "imports/pages/profile/profile"; 15 | @import "imports/pages/verification/verification"; 16 | -------------------------------------------------------------------------------- /.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 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | -------------------------------------------------------------------------------- /client/imports/pages/chats/chats-options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /client/main.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import 'reflect-metadata'; 3 | 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | import { MeteorObservable } from 'meteor-rxjs'; 6 | import { Meteor } from 'meteor/meteor'; 7 | import { AppModule } from './imports/app/app.module'; 8 | 9 | Meteor.startup(() => { 10 | const subscription = MeteorObservable.autorun().subscribe(() => { 11 | if (Meteor.loggingIn()) { 12 | return; 13 | } 14 | 15 | setTimeout(() => subscription.unsubscribe()); 16 | platformBrowserDynamic().bootstrapModule(AppModule); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /imports/collections/users.ts: -------------------------------------------------------------------------------- 1 | import { MongoObservable } from 'meteor-rxjs'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { User } from '../models'; 4 | import { Pictures } from './pictures'; 5 | 6 | export const Users = MongoObservable.fromExisting(Meteor.users); 7 | 8 | // Dispose unused profile pictures 9 | Meteor.users.after.update(function (userId, doc, fieldNames, modifier, options) { 10 | if (!doc.profile) return; 11 | if (!this.previous.profile) return; 12 | if (doc.profile.pictureId == this.previous.profile.pictureId) return; 13 | 14 | Pictures.collection.remove({ _id: doc.profile.pictureId }); 15 | }, { fetchPrevious: true }); 16 | -------------------------------------------------------------------------------- /client/imports/pages/profile/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Profile 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 | 17 | 18 | Name 19 | 20 | 21 |
-------------------------------------------------------------------------------- /public/assets/default-profile-pic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/imports/pages/login/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/imports/pages/verification/verification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Verification 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | An SMS message with the verification code has been sent to {{phone}}. 15 |
16 |
17 |
18 | To proceed, please enter the 4-digit verification code below. 19 |
20 |
21 | 22 | 23 | 24 | 25 |
-------------------------------------------------------------------------------- /client/imports/pages/messages/messages-attachments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/imports/pages/messages/location-message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Send Location 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

Send your current location

19 |

Accurate to {{accuracy}} meters

20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /client/imports/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Platform } from 'ionic-angular'; 3 | import { StatusBar, Splashscreen } from 'ionic-native'; 4 | import { Meteor } from 'meteor/meteor'; 5 | import { ChatsPage } from '../pages/chats/chats'; 6 | import { LoginPage } from '../pages/login/login'; 7 | import template from "./app.html"; 8 | 9 | @Component({ 10 | template 11 | }) 12 | export class MyApp { 13 | rootPage: any; 14 | 15 | constructor(platform: Platform) { 16 | this.rootPage = Meteor.user() ? ChatsPage : LoginPage; 17 | 18 | platform.ready().then(() => { 19 | // Okay, so the platform is ready and our plugins are available. 20 | // Here you can do any higher level native things you might need. 21 | if (platform.is('cordova')) { 22 | StatusBar.styleDefault(); 23 | Splashscreen.hide(); 24 | } 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /manuals/templates/step14.md.tmpl: -------------------------------------------------------------------------------- 1 | Congratulations! You've just mastered `Ionic`'s framework combined with `Meteor` as a back-end. Creating a full stack real time mobile app has never been so fast and easy. Be sure to use the skills you've just granted wisely. 2 | 3 | For feature suggestions and issues submission please visit the issues page: https://github.com/Urigo/Ionic2-MeteorCLI-WhatsApp/issues 4 | 5 | Anyone whose willing to make his own additions in this tutorial is welcome to open a new pull request: https://github.com/Urigo/Ionic2-MeteorCLI-WhatsApp/pulls 6 | 7 | For more amazing tutorials be sure to visit: https://angular-meteor.com 8 | 9 | Follow us on twitter at: https://twitter.com/UriGoldshtein 10 | 11 | Special thanks to [Eytan Manor](https://github.com/DAB0mB) and [Dotan Simha](https://github.com/dotansimha) who did an amazing job writing this tutorial. 12 | 13 | {{{nav_step ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/native-mobile"}}} 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "declaration": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "sourceMap": true, 15 | "target": "es5", 16 | "skipLibCheck": true, 17 | "stripInternal": true, 18 | "noImplicitAny": false, 19 | "types": [ 20 | "meteor-typings", 21 | "@types/underscore", 22 | "@types/meteor-accounts-phone", 23 | "@types/meteor-publish-composite", 24 | "@types/meteor-collection-hooks" 25 | ] 26 | }, 27 | "include": [ 28 | "client/**/*.ts", 29 | "server/**/*.ts", 30 | "imports/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "compileOnSave": false, 36 | "atom": { 37 | "rewriteTsconfig": false 38 | } 39 | } -------------------------------------------------------------------------------- /client/imports/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Named Color Variables 2 | // -------------------------------------------------- 3 | // Named colors makes it easy to reuse colors on various components. 4 | // It's highly recommended to change the default colors 5 | // to match your app's branding. Ionic uses a Sass map of 6 | // colors so you can add, rename and remove colors as needed. 7 | // The "primary" color is the only required color in the map. 8 | 9 | $colors: ( 10 | primary: #387ef5, 11 | secondary: #32db64, 12 | danger: #f53d3d, 13 | light: #f4f4f4, 14 | dark: #222, 15 | whatsapp: #075E54 16 | ); 17 | 18 | // Components 19 | // -------------------------------------------------- 20 | 21 | @import "components"; 22 | 23 | 24 | // App Theme 25 | // -------------------------------------------------- 26 | // Ionic apps can have different themes applied, which can 27 | // then be future customized. This import comes last 28 | // so that the above variables are used and Ionic's 29 | // default are overridden. 30 | 31 | @import "{}/node_modules/ionic-angular/themes/ionic.theme.default"; 32 | -------------------------------------------------------------------------------- /manuals/views/step14.md: -------------------------------------------------------------------------------- 1 | # Step 14: Summary 2 | 3 | Congratulations! You've just mastered `Ionic`'s framework combined with `Meteor` as a back-end. Creating a full stack real time mobile app has never been so fast and easy. Be sure to use the skills you've just granted wisely. 4 | 5 | For feature suggestions and issues submission please visit the issues page: https://github.com/Urigo/Ionic2-MeteorCLI-WhatsApp/issues 6 | 7 | Anyone whose willing to make his own additions in this tutorial is welcome to open a new pull request: https://github.com/Urigo/Ionic2-MeteorCLI-WhatsApp/pulls 8 | 9 | For more amazing tutorials be sure to visit: https://angular-meteor.com 10 | 11 | Follow us on twitter at: https://twitter.com/UriGoldshtein 12 | 13 | Special thanks to [Eytan Manor](https://github.com/DAB0mB) and [Dotan Simha](https://github.com/dotansimha) who did an amazing job writing this tutorial. 14 | 15 | [{]: (nav_step ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/native-mobile") 16 | | [< Previous Step](https://angular-meteor.com/tutorials/whatsapp2/meteor/native-mobile) | 17 | |:----------------------| 18 | [}]: # 19 | 20 | -------------------------------------------------------------------------------- /client/imports/pages/messages/messages-attachments.scss: -------------------------------------------------------------------------------- 1 | .messages-attachments-page-content { 2 | $icon-background-size: 60px; 3 | $icon-font-size: 20pt; 4 | 5 | .attachments { 6 | width: 100%; 7 | margin: 0; 8 | display: inline-flex; 9 | } 10 | 11 | .attachment { 12 | text-align: center; 13 | margin: 0; 14 | padding: 0; 15 | 16 | .item-inner { 17 | padding: 0 18 | } 19 | 20 | .attachment-icon { 21 | width: $icon-background-size; 22 | height: $icon-background-size; 23 | line-height: $icon-background-size; 24 | font-size: $icon-font-size; 25 | border-radius: 50%; 26 | color: white; 27 | margin-bottom: 10px 28 | } 29 | 30 | .attachment-name { 31 | color: gray; 32 | } 33 | } 34 | 35 | .attachment-gallery .attachment-icon { 36 | background: linear-gradient(#e13838 50%, #f53d3d 50%); 37 | } 38 | 39 | .attachment-camera .attachment-icon { 40 | background: linear-gradient(#3474e1 50%, #387ef5 50%); 41 | } 42 | 43 | .attachment-location .attachment-icon { 44 | background: linear-gradient(#2ec95c 50%, #32db64 50%); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/imports/theme/ionicons.scss: -------------------------------------------------------------------------------- 1 | // Ionicons Icon Font CSS 2 | // -------------------------- 3 | // Ionicons CSS for Ionic's element 4 | // ionicons-icons.scss has the icons and their unicode characters 5 | 6 | $ionicons-font-path: $font-path !default; 7 | 8 | @import "{}/node_modules/ionicons/dist/scss/ionicons-icons"; 9 | @import "{}/node_modules/ionicons/dist/scss/ionicons-variables"; 10 | 11 | 12 | @font-face { 13 | font-family: "Ionicons"; 14 | src: url("#{$ionicons-font-path}/ionicons.woff2?v=#{$ionicons-version}") format("woff2"), 15 | url("#{$ionicons-font-path}/ionicons.woff?v=#{$ionicons-version}") format("woff"), 16 | url("#{$ionicons-font-path}/ionicons.ttf?v=#{$ionicons-version}") format("truetype"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | ion-icon { 22 | display: inline-block; 23 | 24 | font-family: "Ionicons"; 25 | -moz-osx-font-smoothing: grayscale; 26 | -webkit-font-smoothing: antialiased; 27 | font-style: normal; 28 | font-variant: normal; 29 | font-weight: normal; 30 | line-height: 1; 31 | text-rendering: auto; 32 | text-transform: none; 33 | speak: none; 34 | } 35 | -------------------------------------------------------------------------------- /client/imports/app/app.scss: -------------------------------------------------------------------------------- 1 | // App Global Sass 2 | // -------------------------------------------------- 3 | // Put style rules here that you want to apply globally. These 4 | // styles are for the entire app and not just one component. 5 | // Additionally, this file can be also used as an entry point 6 | // to import other Sass files to be included in the output CSS. 7 | 8 | 9 | // Options Popover Component 10 | // -------------------------------------------------- 11 | 12 | $options-popover-width: 200px; 13 | $options-popover-margin: 5px; 14 | 15 | .options-popover .popover-content { 16 | width: $options-popover-width; 17 | transform-origin: right top 0px !important; 18 | left: calc(100% - #{$options-popover-width} - #{$options-popover-margin}) !important; 19 | top: $options-popover-margin !important; 20 | } 21 | 22 | // Attachments Popover Component 23 | // -------------------------------------------------- 24 | 25 | $attachments-popover-width: 100%; 26 | 27 | .attachments-popover .popover-content { 28 | width: $attachments-popover-width; 29 | transform-origin: 300px 30px !important; 30 | left: calc(100% - #{$attachments-popover-width}) !important; 31 | top: 58px !important; 32 | } 33 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.0.4 # Packages every Meteor app needs to have 8 | mongo@1.1.14 # The database Meteor supports right now 9 | reactive-var@1.0.11 # Reactive variable for tracker 10 | jquery@1.11.10 # Helpful client-side library 11 | tracker@1.1.1 # Meteor's client-side reactive programming library 12 | 13 | standard-minifier-css@1.3.2 # CSS minifier run for production mode 14 | standard-minifier-js@1.2.1 # JS minifier run for production mode 15 | es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. 16 | ecmascript@0.6.1 # Enable ECMAScript2015+ syntax in app code 17 | shell-server@0.2.1 # Server-side component of the `meteor shell` command 18 | 19 | angular2-compilers 20 | mys:fonts 21 | mobile-status-bar 22 | launch-screen 23 | check 24 | npm-bcrypt 25 | accounts-base 26 | mys:accounts-phone 27 | reywood:publish-composite 28 | jalik:ufs 29 | jalik:ufs-gridfs 30 | -------------------------------------------------------------------------------- /client/imports/pages/chats/new-chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | New Chat 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /imports/models.ts: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | 3 | export const DEFAULT_PICTURE_URL = '/assets/default-profile-pic.svg'; 4 | 5 | export interface Profile { 6 | name?: string; 7 | picture?: string; 8 | pictureId?: string; 9 | } 10 | 11 | export enum MessageType { 12 | TEXT = 'text', 13 | LOCATION = 'location', 14 | PICTURE = 'picture' 15 | } 16 | 17 | export interface Chat { 18 | _id?: string; 19 | title?: string; 20 | picture?: string; 21 | lastMessage?: Message; 22 | memberIds?: string[]; 23 | } 24 | 25 | export interface Message { 26 | _id?: string; 27 | chatId?: string; 28 | senderId?: string; 29 | content?: string; 30 | createdAt?: Date; 31 | ownership?: string; 32 | type?: MessageType; 33 | } 34 | 35 | export interface User extends Meteor.User { 36 | profile?: Profile; 37 | } 38 | 39 | export interface Location { 40 | lat: number; 41 | lng: number; 42 | zoom: number; 43 | } 44 | 45 | export interface Picture { 46 | _id?: string; 47 | complete?: boolean; 48 | extension?: string; 49 | name?: string; 50 | progress?: number; 51 | size?: number; 52 | store?: string; 53 | token?: string; 54 | type?: string; 55 | uploadedAt?: Date; 56 | uploading?: boolean; 57 | url?: string; 58 | userId?: string; 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic2-MeteorCLI-WhatsApp", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run --settings private/settings.json" 6 | }, 7 | "dependencies": { 8 | "@angular/common": "2.2.1", 9 | "@angular/compiler": "2.2.1", 10 | "@angular/compiler-cli": "2.2.1", 11 | "@angular/core": "2.2.1", 12 | "@angular/forms": "2.2.1", 13 | "@angular/http": "2.2.1", 14 | "@angular/platform-browser": "2.2.1", 15 | "@angular/platform-browser-dynamic": "2.2.1", 16 | "@angular/platform-server": "2.2.1", 17 | "@ionic/storage": "1.1.7", 18 | "angular2-google-maps": "^0.17.0", 19 | "angular2-moment": "^1.1.0", 20 | "babel-runtime": "6.18.0", 21 | "ionic-angular": "2.0.0-rc.5", 22 | "ionic-native": "2.2.11", 23 | "ionicons": "3.0.0", 24 | "meteor-node-stubs": "~0.2.0", 25 | "meteor-rxjs": "^0.4.7", 26 | "moment": "^2.17.1", 27 | "reflect-metadata": "^0.1.9", 28 | "rxjs": "5.0.0-beta.12", 29 | "sharp": "^0.17.1", 30 | "zone.js": "0.6.26" 31 | }, 32 | "devDependencies": { 33 | "@types/meteor": "^1.3.31", 34 | "@types/meteor-accounts-phone": "0.0.5", 35 | "@types/meteor-collection-hooks": "^0.8.0", 36 | "@types/meteor-publish-composite": "0.0.32", 37 | "@types/underscore": "^1.7.36", 38 | "meteor-typings": "^1.3.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/imports/pages/verification/verification.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AlertController, NavController, NavParams } from 'ionic-angular'; 3 | import { ProfilePage } from '../profile/profile'; 4 | import { PhoneService } from '../../services/phone'; 5 | import template from './verification.html'; 6 | 7 | @Component({ 8 | template 9 | }) 10 | export class VerificationPage implements OnInit { 11 | code: string = ''; 12 | phone: string; 13 | 14 | constructor( 15 | private alertCtrl: AlertController, 16 | private navCtrl: NavController, 17 | private navParams: NavParams, 18 | private phoneService: PhoneService 19 | ) {} 20 | 21 | ngOnInit() { 22 | this.phone = this.navParams.get('phone'); 23 | } 24 | 25 | onInputKeypress({keyCode}: KeyboardEvent): void { 26 | if (keyCode === 13) { 27 | this.verify(); 28 | } 29 | } 30 | 31 | verify(): void { 32 | this.phoneService.login(this.phone, this.code).then(() => { 33 | this.navCtrl.setRoot(ProfilePage, {}, { 34 | animate: true 35 | }); 36 | }); 37 | } 38 | 39 | handleError(e: Error): void { 40 | console.error(e); 41 | 42 | const alert = this.alertCtrl.create({ 43 | title: 'Oops!', 44 | message: e.message, 45 | buttons: ['OK'] 46 | }); 47 | 48 | alert.present(); 49 | } 50 | } -------------------------------------------------------------------------------- /fonts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": ["eot", "ttf", "woff", "woff2"], 3 | "map": { 4 | "node_modules/ionic-angular/fonts/ionicons.eot": "fonts/ionicons.eot", 5 | "node_modules/ionic-angular/fonts/ionicons.ttf": "fonts/ionicons.ttf", 6 | "node_modules/ionic-angular/fonts/ionicons.woff": "fonts/ionicons.woff", 7 | "node_modules/ionic-angular/fonts/ionicons.woff2": "fonts/ionicons.woff2", 8 | "node_modules/ionic-angular/fonts/noto-sans-bold.ttf": "fonts/noto-sans-bold.ttf", 9 | "node_modules/ionic-angular/fonts/noto-sans-regular.ttf": "fonts/noto-sans-regular.ttf", 10 | "node_modules/ionic-angular/fonts/noto-sans.scss": "fonts/noto-sans.scss", 11 | "node_modules/ionic-angular/fonts/roboto-bold.ttf": "fonts/roboto-bold.ttf", 12 | "node_modules/ionic-angular/fonts/roboto-bold.woff": "fonts/roboto-bold.woff", 13 | "node_modules/ionic-angular/fonts/roboto-light.ttf": "fonts/roboto-light.ttf", 14 | "node_modules/ionic-angular/fonts/roboto-light.woff": "fonts/roboto-light.woff", 15 | "node_modules/ionic-angular/fonts/roboto-medium.ttf": "fonts/roboto-medium.ttf", 16 | "node_modules/ionic-angular/fonts/roboto-medium.woff": "fonts/roboto-medium.woff", 17 | "node_modules/ionic-angular/fonts/roboto-regular.ttf": "fonts/roboto-regular.ttf", 18 | "node_modules/ionic-angular/fonts/roboto-regular.woff": "fonts/roboto-regular.woff" 19 | } 20 | } -------------------------------------------------------------------------------- /client/imports/pages/chats/chats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chats 5 | 6 | 7 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/imports/services/phone.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Platform } from 'ionic-angular'; 3 | import { Sim } from 'ionic-native'; 4 | import { Accounts } from 'meteor/accounts-base'; 5 | import { Meteor } from 'meteor/meteor'; 6 | 7 | @Injectable() 8 | export class PhoneService { 9 | constructor(private platform: Platform) { 10 | 11 | } 12 | 13 | getNumber(): Promise { 14 | if (!this.platform.is('cordova') || 15 | !this.platform.is('mobile')) { 16 | return Promise.resolve(''); 17 | } 18 | 19 | return Sim.getSimInfo().then((info) => { 20 | return '+' + info.phoneNumber; 21 | }); 22 | } 23 | 24 | verify(phoneNumber: string): Promise { 25 | return new Promise((resolve, reject) => { 26 | Accounts.requestPhoneVerification(phoneNumber, (e: Error) => { 27 | if (e) { 28 | return reject(e); 29 | } 30 | 31 | resolve(); 32 | }); 33 | }); 34 | } 35 | 36 | login(phoneNumber: string, code: string): Promise { 37 | return new Promise((resolve, reject) => { 38 | Accounts.verifyPhone(phoneNumber, code, (e: Error) => { 39 | if (e) { 40 | return reject(e); 41 | } 42 | 43 | resolve(); 44 | }); 45 | }); 46 | } 47 | 48 | logout(): Promise { 49 | return new Promise((resolve, reject) => { 50 | Meteor.logout((e: Error) => { 51 | if (e) { 52 | return reject(e); 53 | } 54 | 55 | resolve(); 56 | }); 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /imports/collections/pictures.ts: -------------------------------------------------------------------------------- 1 | import { MongoObservable } from 'meteor-rxjs'; 2 | import { UploadFS } from 'meteor/jalik:ufs'; 3 | import { Meteor } from 'meteor/meteor'; 4 | import { Picture, DEFAULT_PICTURE_URL } from '../models'; 5 | 6 | export interface PicturesCollection extends MongoObservable.Collection { 7 | getPictureUrl(selector?: Object | string): string; 8 | } 9 | 10 | export const Pictures = 11 | new MongoObservable.Collection('pictures') as PicturesCollection; 12 | 13 | export const PicturesStore = new UploadFS.store.GridFS({ 14 | collection: Pictures.collection, 15 | name: 'pictures', 16 | filter: new UploadFS.Filter({ 17 | contentTypes: ['image/*'] 18 | }), 19 | permissions: new UploadFS.StorePermissions({ 20 | insert: picturesPermissions, 21 | update: picturesPermissions, 22 | remove: picturesPermissions 23 | }), 24 | transformWrite(from, to) { 25 | // The transformation function will only be invoked on the server. Accordingly, 26 | // the 'sharp' library is a server-only library which will cause an error to be 27 | // thrown when loaded on the global scope 28 | const Sharp = Npm.require('sharp'); 29 | // Compress picture to 75% from its original quality 30 | const transform = Sharp().png({ quality: 75 }); 31 | from.pipe(transform).pipe(to); 32 | } 33 | }); 34 | 35 | // Gets picture's url by a given selector 36 | Pictures.getPictureUrl = function (selector) { 37 | const picture = this.findOne(selector) || {}; 38 | return picture.url || DEFAULT_PICTURE_URL; 39 | }; 40 | 41 | function picturesPermissions(userId: string): boolean { 42 | return Meteor.isServer || !!userId; 43 | } 44 | -------------------------------------------------------------------------------- /manuals/templates/step13.md.tmpl: -------------------------------------------------------------------------------- 1 | In this step, we will be implementing additional native features, to enhance the user experience. 2 | 3 | ## Automatic phone number detection 4 | 5 | `Ionic 2` is provided by default with a `Cordova` plug-in called `cordova-plugin-sim`, which allows us to retrieve some data from the current device's SIM card, if even exists. We will use the SIM card to automatically detect the current device's phone number, so this way the user won't need to manually fill-in his phone number whenever he tries to login. We will start by adding the appropriate handler in the `PhoneService`: 6 | 7 | {{{diff_step 13.1}}} 8 | 9 | And we will use it inside the `LoginPage`: 10 | 11 | {{{diff_step 13.2}}} 12 | 13 | In-order for it to work, be sure to install the following `Cordova` plug-in: 14 | 15 | $ meteor add cordova:cordova-plugin-sim@1.2.1 16 | 17 | ## Camera 18 | 19 | Next - we will grant access to the device's camera so we can send pictures which are yet to exist in the gallery. 20 | 21 | We will start by adding the appropriate `Cordova` plug-in: 22 | 23 | $ meteor add cordova:cordova-plugin-camera@2.3.1 24 | 25 | We will bind the `click` event in the `MessagesAttachmentsComponent`: 26 | 27 | {{{diff_step 13.5}}} 28 | 29 | And we will use the recently installed `Cordova` plug-in in the event handler to take some pictures: 30 | 31 | {{{diff_step 13.6}}} 32 | 33 | Note that take pictures are retrieved as relative paths in the device, but we use some existing methods in the `PictureService` to convert these paths into the desired format. 34 | 35 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/file-upload" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/summary"}}} 36 | -------------------------------------------------------------------------------- /client/imports/pages/messages/messages-attachments.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AlertController, Platform, ModalController, ViewController } from 'ionic-angular'; 3 | import { Camera } from 'ionic-native'; 4 | import { PictureService } from '../../services/picture'; 5 | import { MessageType } from '../../../../imports/models'; 6 | import { NewLocationMessageComponent } from './location-message'; 7 | import template from './messages-attachments.html'; 8 | 9 | @Component({ 10 | template 11 | }) 12 | export class MessagesAttachmentsComponent { 13 | constructor( 14 | private alertCtrl: AlertController, 15 | private platform: Platform, 16 | private viewCtrl: ViewController, 17 | private modelCtrl: ModalController, 18 | private pictureService: PictureService 19 | ) {} 20 | 21 | sendPicture(): void { 22 | this.pictureService.select().then((file: File) => { 23 | this.viewCtrl.dismiss({ 24 | messageType: MessageType.PICTURE, 25 | selectedPicture: file 26 | }); 27 | }); 28 | } 29 | 30 | takePicture(): void { 31 | if (!this.platform.is('cordova')) { 32 | return console.warn('Device must run cordova in order to take pictures'); 33 | } 34 | 35 | Camera.getPicture().then((dataURI) => { 36 | const blob = this.pictureService.convertDataURIToBlob(dataURI); 37 | 38 | this.viewCtrl.dismiss({ 39 | messageType: MessageType.PICTURE, 40 | selectedPicture: blob 41 | }); 42 | }); 43 | } 44 | 45 | sendLocation(): void { 46 | const locationModal = this.modelCtrl.create(NewLocationMessageComponent); 47 | locationModal.onDidDismiss((location) => { 48 | if (!location) { 49 | this.viewCtrl.dismiss(); 50 | 51 | return; 52 | } 53 | 54 | this.viewCtrl.dismiss({ 55 | messageType: MessageType.LOCATION, 56 | selectedLocation: location 57 | }); 58 | }); 59 | 60 | locationModal.present(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/imports/pages/chats/chats-options.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable } from '@angular/core'; 2 | import { Alert, AlertController, NavController, ViewController } from 'ionic-angular'; 3 | import { PhoneService } from '../../services/phone'; 4 | import { LoginPage } from '../login/login'; 5 | import { ProfilePage } from '../profile/profile'; 6 | import template from './chats-options.html'; 7 | 8 | @Component({ 9 | template 10 | }) 11 | @Injectable() 12 | export class ChatsOptionsComponent { 13 | constructor( 14 | private alertCtrl: AlertController, 15 | private navCtrl: NavController, 16 | private phoneService: PhoneService, 17 | private viewCtrl: ViewController 18 | ) {} 19 | 20 | editProfile(): void { 21 | this.viewCtrl.dismiss().then(() => { 22 | this.navCtrl.push(ProfilePage); 23 | }); 24 | } 25 | 26 | logout(): void { 27 | const alert = this.alertCtrl.create({ 28 | title: 'Logout', 29 | message: 'Are you sure you would like to proceed?', 30 | buttons: [ 31 | { 32 | text: 'Cancel', 33 | role: 'cancel' 34 | }, 35 | { 36 | text: 'Yes', 37 | handler: () => { 38 | this.handleLogout(alert); 39 | return false; 40 | } 41 | } 42 | ] 43 | }); 44 | 45 | this.viewCtrl.dismiss().then(() => { 46 | alert.present(); 47 | }); 48 | } 49 | 50 | handleLogout(alert: Alert): void { 51 | alert.dismiss().then(() => { 52 | return this.phoneService.logout(); 53 | }) 54 | .then(() => { 55 | this.navCtrl.setRoot(LoginPage, {}, { 56 | animate: true 57 | }); 58 | }) 59 | .catch((e) => { 60 | this.handleError(e); 61 | }); 62 | } 63 | 64 | handleError(e: Error): void { 65 | console.error(e); 66 | 67 | const alert = this.alertCtrl.create({ 68 | title: 'Oops!', 69 | message: e.message, 70 | buttons: ['OK'] 71 | }); 72 | 73 | alert.present(); 74 | } 75 | } -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.14 2 | allow-deny@1.0.5 3 | angular2-compilers@0.6.6 4 | autoupdate@1.2.11 5 | babel-compiler@6.13.0 6 | babel-runtime@1.0.1 7 | barbatus:caching-compiler@1.1.9 8 | barbatus:css-compiler@0.3.6 9 | barbatus:scss-compiler@3.8.3 10 | barbatus:typescript@0.5.2 11 | barbatus:typescript-compiler@0.8.7 12 | barbatus:typescript-runtime@0.1.2 13 | base64@1.0.10 14 | binary-heap@1.0.10 15 | blaze@2.3.0 16 | blaze-tools@1.0.10 17 | boilerplate-generator@1.0.11 18 | caching-compiler@1.0.6 19 | callback-hook@1.0.10 20 | check@1.2.4 21 | ddp@1.2.5 22 | ddp-client@1.2.9 23 | ddp-common@1.2.8 24 | ddp-rate-limiter@1.0.6 25 | ddp-server@1.2.10 26 | deps@1.0.12 27 | diff-sequence@1.0.7 28 | ecmascript@0.6.1 29 | ecmascript-runtime@0.3.15 30 | ejson@1.0.13 31 | email@1.0.16 32 | es5-shim@4.6.15 33 | geojson-utils@1.0.10 34 | hot-code-push@1.0.4 35 | html-tools@1.0.11 36 | htmljs@1.0.11 37 | http@1.1.8 38 | id-map@1.0.9 39 | jalik:ufs@0.7.1_1 40 | jalik:ufs-gridfs@0.1.4 41 | jquery@1.11.10 42 | launch-screen@1.1.0 43 | livedata@1.0.18 44 | localstorage@1.0.12 45 | logging@1.1.16 46 | matb33:collection-hooks@0.8.4 47 | meteor@1.6.0 48 | meteor-base@1.0.4 49 | minifier-css@1.2.15 50 | minifier-js@1.2.15 51 | minimongo@1.0.19 52 | mobile-status-bar@1.0.13 53 | modules@0.7.7 54 | modules-runtime@0.7.8 55 | mongo@1.1.14 56 | mongo-id@1.0.6 57 | mys:accounts-phone@0.0.21 58 | mys:fonts@0.0.2 59 | npm-bcrypt@0.9.2 60 | npm-mongo@2.2.16_1 61 | observe-sequence@1.0.14 62 | ordered-dict@1.0.9 63 | promise@0.8.8 64 | random@1.0.10 65 | rate-limit@1.0.6 66 | reactive-var@1.0.11 67 | reload@1.1.11 68 | retry@1.0.9 69 | reywood:publish-composite@1.4.2 70 | routepolicy@1.0.12 71 | service-configuration@1.0.11 72 | sha@1.0.9 73 | shell-server@0.2.1 74 | spacebars@1.0.13 75 | spacebars-compiler@1.1.0 76 | srp@1.0.10 77 | standard-minifier-css@1.3.2 78 | standard-minifier-js@1.2.1 79 | tracker@1.1.1 80 | ui@1.0.12 81 | underscore@1.0.10 82 | urigo:static-html-compiler@0.1.8 83 | url@1.0.11 84 | webapp@1.3.12 85 | webapp-hashing@1.0.9 86 | -------------------------------------------------------------------------------- /client/imports/pages/messages/messages-options.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AlertController, NavController, NavParams, ViewController } from 'ionic-angular'; 3 | import { MeteorObservable } from 'meteor-rxjs'; 4 | import { ChatsPage } from '../chats/chats'; 5 | import template from './messages-options.html'; 6 | 7 | @Component({ 8 | template 9 | }) 10 | export class MessagesOptionsComponent { 11 | constructor( 12 | public alertCtrl: AlertController, 13 | public navCtrl: NavController, 14 | public params: NavParams, 15 | public viewCtrl: ViewController 16 | ) {} 17 | 18 | remove(): void { 19 | const alert = this.alertCtrl.create({ 20 | title: 'Remove', 21 | message: 'Are you sure you would like to proceed?', 22 | buttons: [ 23 | { 24 | text: 'Cancel', 25 | role: 'cancel' 26 | }, 27 | { 28 | text: 'Yes', 29 | handler: () => { 30 | this.handleRemove(alert); 31 | return false; 32 | } 33 | } 34 | ] 35 | }); 36 | 37 | this.viewCtrl.dismiss().then(() => { 38 | alert.present(); 39 | }); 40 | } 41 | 42 | handleRemove(alert): void { 43 | MeteorObservable.call('removeChat', this.params.get('chat')._id).subscribe({ 44 | next: () => { 45 | alert.dismiss().then(() => { 46 | this.navCtrl.setRoot(ChatsPage, {}, { 47 | animate: true 48 | }); 49 | }); 50 | }, 51 | error: (e: Error) => { 52 | alert.dismiss().then(() => { 53 | if (e) { 54 | return this.handleError(e); 55 | } 56 | 57 | this.navCtrl.setRoot(ChatsPage, {}, { 58 | animate: true 59 | }); 60 | }); 61 | } 62 | }); 63 | } 64 | 65 | handleError(e: Error): void { 66 | console.error(e); 67 | 68 | const alert = this.alertCtrl.create({ 69 | title: 'Oops!', 70 | message: e.message, 71 | buttons: ['OK'] 72 | }); 73 | 74 | alert.present(); 75 | } 76 | } -------------------------------------------------------------------------------- /client/imports/pages/login/login.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterContentInit } from '@angular/core'; 2 | import { Alert, AlertController, NavController } from 'ionic-angular'; 3 | import { PhoneService } from '../../services/phone'; 4 | import { VerificationPage } from '../verification/verification'; 5 | import template from './login.html'; 6 | 7 | @Component({ 8 | template 9 | }) 10 | export class LoginPage implements AfterContentInit { 11 | phone = ''; 12 | 13 | constructor( 14 | private alertCtrl: AlertController, 15 | private phoneService: PhoneService, 16 | private navCtrl: NavController 17 | ) {} 18 | 19 | ngAfterContentInit() { 20 | this.phoneService.getNumber().then((phone) => { 21 | if (phone) { 22 | this.login(phone); 23 | } 24 | }); 25 | } 26 | 27 | onInputKeypress({keyCode}: KeyboardEvent): void { 28 | if (keyCode === 13) { 29 | this.login(); 30 | } 31 | } 32 | 33 | login(phone: string = this.phone): void { 34 | const alert = this.alertCtrl.create({ 35 | title: 'Confirm', 36 | message: `Would you like to proceed with the phone number ${phone}?`, 37 | buttons: [ 38 | { 39 | text: 'Cancel', 40 | role: 'cancel' 41 | }, 42 | { 43 | text: 'Yes', 44 | handler: () => { 45 | this.handleLogin(alert); 46 | return false; 47 | } 48 | } 49 | ] 50 | }); 51 | 52 | alert.present(); 53 | } 54 | 55 | handleLogin(alert: Alert): void { 56 | alert.dismiss().then(() => { 57 | return this.phoneService.verify(this.phone); 58 | }) 59 | .then(() => { 60 | this.navCtrl.push(VerificationPage, { 61 | phone: this.phone 62 | }); 63 | }) 64 | .catch((e) => { 65 | this.handleError(e); 66 | }); 67 | } 68 | 69 | handleError(e: Error): void { 70 | console.error(e); 71 | 72 | const alert = this.alertCtrl.create({ 73 | title: 'Oops!', 74 | message: e.message, 75 | buttons: ['OK'] 76 | }); 77 | 78 | alert.present(); 79 | } 80 | } -------------------------------------------------------------------------------- /client/imports/pages/profile/profile.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AlertController, NavController } from 'ionic-angular'; 3 | import { MeteorObservable } from 'meteor-rxjs'; 4 | import { Pictures } from '../../../../imports/collections'; 5 | import { Profile } from '../../../../imports/models'; 6 | import { PictureService } from '../../services/picture'; 7 | import { ChatsPage } from '../chats/chats'; 8 | import template from './profile.html'; 9 | 10 | @Component({ 11 | template 12 | }) 13 | export class ProfilePage implements OnInit { 14 | picture: string; 15 | profile: Profile; 16 | 17 | constructor( 18 | private alertCtrl: AlertController, 19 | private navCtrl: NavController, 20 | private pictureService: PictureService 21 | ) {} 22 | 23 | ngOnInit(): void { 24 | this.profile = Meteor.user().profile || { 25 | name: '' 26 | }; 27 | 28 | MeteorObservable.subscribe('user').subscribe(() => { 29 | this.picture = Pictures.getPictureUrl(this.profile.pictureId); 30 | }); 31 | } 32 | 33 | selectProfilePicture(): void { 34 | this.pictureService.select().then((blob) => { 35 | this.uploadProfilePicture(blob); 36 | }) 37 | .catch((e) => { 38 | this.handleError(e); 39 | }); 40 | } 41 | 42 | uploadProfilePicture(blob: Blob): void { 43 | this.pictureService.upload(blob).then((picture) => { 44 | this.profile.pictureId = picture._id; 45 | this.picture = picture.url; 46 | }) 47 | .catch((e) => { 48 | this.handleError(e); 49 | }); 50 | } 51 | 52 | updateProfile(): void { 53 | MeteorObservable.call('updateProfile', this.profile).subscribe({ 54 | next: () => { 55 | this.navCtrl.push(ChatsPage); 56 | }, 57 | error: (e: Error) => { 58 | this.handleError(e); 59 | } 60 | }); 61 | } 62 | 63 | handleError(e: Error): void { 64 | console.error(e); 65 | 66 | const alert = this.alertCtrl.create({ 67 | title: 'Oops!', 68 | message: e.message, 69 | buttons: ['OK'] 70 | }); 71 | 72 | alert.present(); 73 | } 74 | } -------------------------------------------------------------------------------- /manuals/templates/step3.md.tmpl: -------------------------------------------------------------------------------- 1 | To help you get started with `RxJS`, we recommend you to read [this post](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/). 2 | 3 | ## TL;DR 4 | 5 | `RxJS` is a library that allows us to easily create and manipulate streams of events and data. This makes complex asynchronous development much easier to handle and understand. `Angular 2` adopted `RxJS` as a dependency, and uses it to manage its stream of data and flow of actions. 6 | 7 | ## Quick Reference 8 | 9 | In this tutorial, we will be using fundamental `RxJS` operators, as listed below: 10 | 11 | - **map** - Transform values of the observable (into another observable). Common use cases are converting, parsing, adding new fields etc. 12 | 13 | - **filter** - Filters values emitted by the observable, and continue the flow only with the values which passed the filter handler. 14 | 15 | - **startWith** - Sets the initial value for the previous operation before proceeding. 16 | 17 | - **flatMap** - Useful when we want to resolve a set of observables. 18 | 19 | `RxJS` offers plenty of operators, which can ease the development process. A more detailed explanation can be found in the [RxJS book](http://xgrommx.github.io/rx-book/index.html). 20 | 21 | ## Meteor-RxJS 22 | 23 | `Angular 2` uses [ngrx](https://github.com/ngrx) package, and supports `Observable` data sources (For example, using `ngFor` directive). 24 | 25 | `meteor-rxjs` wraps `Meteor`'s basic functionality and exposes `RxJS` interfaces which can be used to manipulate reactive data sources; The `meteor-rxjs` package will be used vastly further in this tutorial, so as we go further, you'll probably get a better perception for it. 26 | 27 | ## More References 28 | 29 | - [RxJS Book](http://xgrommx.github.io/rx-book/index.html) 30 | - [Meteor-RxJS API Documentation](api/meteor-rxjs/latest/MeteorObservable) 31 | - [RxJS API Documentation](http://reactivex.io/rxjs/) 32 | - [meteor-rxjs @ GitHub](https://github.com/Urigo/meteor-rxjs) 33 | 34 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/meteor-server-side"}}} 35 | -------------------------------------------------------------------------------- /client/imports/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ErrorHandler } from '@angular/core'; 2 | import { AgmCoreModule } from 'angular2-google-maps/core'; 3 | import { MomentModule } from 'angular2-moment'; 4 | import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 5 | import { ChatsPage } from '../pages/chats/chats' 6 | import { ChatsOptionsComponent } from '../pages/chats/chats-options'; 7 | import { NewChatComponent } from '../pages/chats/new-chat'; 8 | import { LoginPage } from '../pages/login/login'; 9 | import { MessagesPage } from '../pages/messages/messages'; 10 | import { MessagesAttachmentsComponent } from '../pages/messages/messages-attachments'; 11 | import { MessagesOptionsComponent } from '../pages/messages/messages-options'; 12 | import { NewLocationMessageComponent } from '../pages/messages/location-message'; 13 | import { ShowPictureComponent } from '../pages/messages/show-picture'; 14 | import { ProfilePage } from '../pages/profile/profile'; 15 | import { VerificationPage } from '../pages/verification/verification'; 16 | import { PhoneService } from '../services/phone'; 17 | import { PictureService } from '../services/picture'; 18 | import { MyApp } from './app.component'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | MyApp, 23 | ChatsPage, 24 | MessagesPage, 25 | LoginPage, 26 | VerificationPage, 27 | ProfilePage, 28 | ChatsOptionsComponent, 29 | NewChatComponent, 30 | MessagesOptionsComponent, 31 | MessagesAttachmentsComponent, 32 | NewLocationMessageComponent, 33 | ShowPictureComponent 34 | ], 35 | imports: [ 36 | IonicModule.forRoot(MyApp), 37 | MomentModule, 38 | AgmCoreModule.forRoot({ 39 | apiKey: 'AIzaSyAWoBdZHCNh5R-hB5S5ZZ2oeoYyfdDgniA' 40 | }) 41 | ], 42 | bootstrap: [IonicApp], 43 | entryComponents: [ 44 | MyApp, 45 | ChatsPage, 46 | MessagesPage, 47 | LoginPage, 48 | VerificationPage, 49 | ProfilePage, 50 | ChatsOptionsComponent, 51 | NewChatComponent, 52 | MessagesOptionsComponent, 53 | MessagesAttachmentsComponent, 54 | NewLocationMessageComponent, 55 | ShowPictureComponent 56 | ], 57 | providers: [ 58 | { provide: ErrorHandler, useClass: IonicErrorHandler }, 59 | PhoneService, 60 | PictureService 61 | ] 62 | }) 63 | export class AppModule {} -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { Chats, Messages, Users } from '../imports/collections'; 4 | import { MessageType, Picture } from '../imports/models'; 5 | 6 | Meteor.startup(() => { 7 | if (Meteor.settings) { 8 | Object.assign(Accounts._options, Meteor.settings['accounts-phone']); 9 | SMS.twilio = Meteor.settings['twilio']; 10 | } 11 | 12 | if (Users.collection.find().count() > 0) { 13 | return; 14 | } 15 | 16 | let picture = importPictureFromUrl({ 17 | name: 'man1.jpg', 18 | url: 'https://randomuser.me/api/portraits/men/1.jpg' 19 | }); 20 | 21 | Accounts.createUserWithPhone({ 22 | phone: '+972540000001', 23 | profile: { 24 | name: 'Ethan Gonzalez', 25 | pictureId: picture._id 26 | } 27 | }); 28 | 29 | picture = importPictureFromUrl({ 30 | name: 'lego1.jpg', 31 | url: 'https://randomuser.me/api/portraits/lego/1.jpg' 32 | }); 33 | 34 | Accounts.createUserWithPhone({ 35 | phone: '+972540000002', 36 | profile: { 37 | name: 'Bryan Wallace', 38 | pictureId: picture._id 39 | } 40 | }); 41 | 42 | picture = importPictureFromUrl({ 43 | name: 'woman1.jpg', 44 | url: 'https://randomuser.me/api/portraits/women/1.jpg' 45 | }); 46 | 47 | Accounts.createUserWithPhone({ 48 | phone: '+972540000003', 49 | profile: { 50 | name: 'Avery Stewart', 51 | pictureId: picture._id 52 | } 53 | }); 54 | 55 | picture = importPictureFromUrl({ 56 | name: 'woman2.jpg', 57 | url: 'https://randomuser.me/api/portraits/women/2.jpg' 58 | }); 59 | 60 | Accounts.createUserWithPhone({ 61 | phone: '+972540000004', 62 | profile: { 63 | name: 'Katie Peterson', 64 | pictureId: picture._id 65 | } 66 | }); 67 | 68 | picture = importPictureFromUrl({ 69 | name: 'man2.jpg', 70 | url: 'https://randomuser.me/api/portraits/men/2.jpg' 71 | }); 72 | 73 | Accounts.createUserWithPhone({ 74 | phone: '+972540000005', 75 | profile: { 76 | name: 'Ray Edwards', 77 | pictureId: picture._id 78 | } 79 | }); 80 | }); 81 | 82 | function importPictureFromUrl(options: { name: string, url: string }): Picture { 83 | const description = { name: options.name }; 84 | 85 | return Meteor.call('ufsImportURL', options.url, description, 'pictures'); 86 | } 87 | -------------------------------------------------------------------------------- /manuals/templates/step4.md.tmpl: -------------------------------------------------------------------------------- 1 | Now that we have the initial chats layout and its component, we will take it a step further by providing the chats data from a server instead of having it locally. 2 | 3 | ## Collections 4 | 5 | Collections in `Meteor` are actually references to [MongoDB](http://mongodb.com) collections. This functionality is provided to us by a `Meteor` package called [Minimongo](https://guide.meteor.com/collections.html), and it shares almost the same API as a native `MongoDB` collection. In this tutorial we will be wrapping our collections using `RxJS`'s `Observables`, which is available for us thanks to [meteor-rxjs](http://npmjs.com/package/meteor-rxjs). 6 | 7 | Our initial collections are gonna be the chats and messages collections; One is going to store chats-models, and the other is going to store messages-models: 8 | 9 | {{{diff_step 4.1}}} 10 | 11 | We chose to create a dedicated module for each collection, because in the near future there might be more logic added into each one of them. To make importation convenient, we will export all collections from a single file: 12 | 13 | {{{diff_step 4.2}}} 14 | 15 | Now instead of requiring each collection individually, we can just require them from the `index.ts` file. 16 | 17 | ## Data fixtures 18 | 19 | Since we have real collections now, and not dummy ones, we will need to fill them up with some data in case they are empty, so we can test our application properly. Let's create our data fixtures in the server: 20 | 21 | {{{diff_step 4.3}}} 22 | 23 | > This behavior is **not** recommended and should be removed once we're ready for production. A conditioned environment variable might be an appropriate solution. 24 | 25 | Note how we use the `.collection` property to get the actual `Mongo.Collection` instance. In the `Meteor` server we want to avoid the usage of observables since it uses `fibers`. More information about fibers can be fond [here](https://www.npmjs.com/package/fibers). 26 | 27 | ## Preparing Our Client 28 | 29 | Now that we have some real data stored in our database, we can replace it with the data fabricated in the `ChatsPage`. This way the client can stay correlated with the server: 30 | 31 | {{{diff_step 4.4}}} 32 | 33 | We will also re-implement the `removeChat` method using an actual `Meteor` collection: 34 | 35 | {{{diff_step 4.5}}} 36 | 37 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/rxjs" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/folder-structure"}}} 38 | -------------------------------------------------------------------------------- /client/imports/pages/messages/location-message.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Platform, ViewController } from 'ionic-angular'; 3 | import { Geolocation } from 'ionic-native'; 4 | import { Observable, Subscription } from 'rxjs'; 5 | import { Location } from '../../../../imports/models'; 6 | import template from './location-message.html'; 7 | 8 | const DEFAULT_ZOOM = 8; 9 | const EQUATOR = 40075004; 10 | const DEFAULT_LAT = 51.678418; 11 | const DEFAULT_LNG = 7.809007; 12 | const LOCATION_REFRESH_INTERVAL = 500; 13 | 14 | @Component({ 15 | template 16 | }) 17 | export class NewLocationMessageComponent implements OnInit, OnDestroy { 18 | lat: number = DEFAULT_LAT; 19 | lng: number = DEFAULT_LNG; 20 | zoom: number = DEFAULT_ZOOM; 21 | accuracy: number = -1; 22 | intervalObs: Subscription; 23 | 24 | constructor(private platform: Platform, private viewCtrl: ViewController) { 25 | } 26 | 27 | ngOnInit() { 28 | // Refresh location at a specific refresh rate 29 | this.intervalObs = this.reloadLocation() 30 | .flatMapTo(Observable 31 | .interval(LOCATION_REFRESH_INTERVAL) 32 | .timeInterval()) 33 | .subscribe(() => { 34 | this.reloadLocation(); 35 | }); 36 | } 37 | 38 | ngOnDestroy() { 39 | // Dispose subscription 40 | if (this.intervalObs) { 41 | this.intervalObs.unsubscribe(); 42 | } 43 | } 44 | 45 | calculateZoomByAccureacy(accuracy: number): number { 46 | // Source: http://stackoverflow.com/a/25143326 47 | const deviceHeight = this.platform.height(); 48 | const deviceWidth = this.platform.width(); 49 | const screenSize = Math.min(deviceWidth, deviceHeight); 50 | const requiredMpp = accuracy / screenSize; 51 | 52 | return ((Math.log(EQUATOR / (256 * requiredMpp))) / Math.log(2)) + 1; 53 | } 54 | 55 | reloadLocation() { 56 | return Observable.fromPromise(Geolocation.getCurrentPosition().then((position) => { 57 | if (this.lat && this.lng) { 58 | // Update view-models to represent the current geo-location 59 | this.accuracy = position.coords.accuracy; 60 | this.lat = position.coords.latitude; 61 | this.lng = position.coords.longitude; 62 | this.zoom = this.calculateZoomByAccureacy(this.accuracy); 63 | } 64 | })); 65 | } 66 | 67 | sendLocation() { 68 | this.viewCtrl.dismiss({ 69 | lat: this.lat, 70 | lng: this.lng, 71 | zoom: this.zoom 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /manuals/views/step3.md: -------------------------------------------------------------------------------- 1 | # Step 3: RxJS 2 | 3 | To help you get started with `RxJS`, we recommend you to read [this post](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/). 4 | 5 | ## TL;DR 6 | 7 | `RxJS` is a library that allows us to easily create and manipulate streams of events and data. This makes complex asynchronous development much easier to handle and understand. `Angular 2` adopted `RxJS` as a dependency, and uses it to manage its stream of data and flow of actions. 8 | 9 | ## Quick Reference 10 | 11 | In this tutorial, we will be using fundamental `RxJS` operators, as listed below: 12 | 13 | - **map** - Transform values of the observable (into another observable). Common use cases are converting, parsing, adding new fields etc. 14 | 15 | - **filter** - Filters values emitted by the observable, and continue the flow only with the values which passed the filter handler. 16 | 17 | - **startWith** - Sets the initial value for the previous operation before proceeding. 18 | 19 | - **flatMap** - Useful when we want to resolve a set of observables. 20 | 21 | `RxJS` offers plenty of operators, which can ease the development process. A more detailed explanation can be found in the [RxJS book](http://xgrommx.github.io/rx-book/index.html). 22 | 23 | ## Meteor-RxJS 24 | 25 | `Angular 2` uses [ngrx](https://github.com/ngrx) package, and supports `Observable` data sources (For example, using `ngFor` directive). 26 | 27 | `meteor-rxjs` wraps `Meteor`'s basic functionality and exposes `RxJS` interfaces which can be used to manipulate reactive data sources; The `meteor-rxjs` package will be used vastly further in this tutorial, so as we go further, you'll probably get a better perception for it. 28 | 29 | ## More References 30 | 31 | - [RxJS Book](http://xgrommx.github.io/rx-book/index.html) 32 | - [Meteor-RxJS API Documentation](api/meteor-rxjs/latest/MeteorObservable) 33 | - [RxJS API Documentation](http://reactivex.io/rxjs/) 34 | - [meteor-rxjs @ GitHub](https://github.com/Urigo/meteor-rxjs) 35 | 36 | [{]: (nav_step next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/meteor-server-side" prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-page") 37 | | [< Previous Step](https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-page) | [Next Step >](https://angular-meteor.com/tutorials/whatsapp2/meteor/meteor-server-side) | 38 | |:--------------------------------|--------------------------------:| 39 | [}]: # 40 | 41 | -------------------------------------------------------------------------------- /client/imports/pages/messages/messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{title}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
{{message.content}}
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | {{ message.createdAt | amDateFormat: 'HH:mm' }} 30 |
31 |
32 | 33 |
{{day.timestamp}}
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /manuals/templates/step10.md.tmpl: -------------------------------------------------------------------------------- 1 | ## Lazy-Loading 2 | 3 | In this step, we will implement a lazy-loading mechanism in the `MessagesPage`. Lazy loading means that only the necessary data will be loaded once we're promoted to the corresponding view, and it will keep loading, but gradually. In the `MessagesPage` case, we will only be provided with several messages once we enter the view, enough messages to fill all of it, and as we scroll up, we will provided with more messages. This way we can have a smooth experience, without the cost of fetching the entire messages collection. We will start by limiting our `messages` subscription into 30 documents: 4 | 5 | {{{diff_step 10.1}}} 6 | 7 | As we said, we will be fetching more and more messages gradually, so we will need to have a counter in the component which will tell us the number of the batch we would like to fetch in our next scroll: 8 | 9 | {{{diff_step 10.2}}} 10 | 11 | By now, whether you noticed or not, we have some sort of a limitation which we have to solve. Let's say we've fetched all the messages available for the current chat, and we keep scrolling up, the component will keep attempting to fetch more messages, but it doesn't know that it reached the limit. Because of that, we will need to know the total number of messages so we will know when to stop the lazy-loading mechanism. To solve this issue, we will begin with implementing a method which will retrieve the number of total messages for a provided chat: 12 | 13 | {{{diff_step 10.3}}} 14 | 15 | Now, whenever we fetch a new messages-batch we will check if we reached the total messages limit, and if so, we will stop listening to the scroll event: 16 | 17 | {{{diff_step 10.4}}} 18 | 19 | ## Filter 20 | 21 | Now we're gonna implement the a search-bar, in the `NewChatComponent`. 22 | 23 | Let's start by implementing the logic using `RxJS`. We will use a `BehaviorSubject` which will store the search pattern entered in the search bar, and we will be able to detect changes in its value using the `Observable` API; So whenever the search pattern is being changed, we will update the users list by re-subscribing to the `users` subscription: 24 | 25 | {{{diff_step 10.5}}} 26 | 27 | Note how we used the `debounce` method to prevent subscription spamming. Let's add the template for the search-bar in the `NewChat` view, and bind it to the corresponding data-models and methods in the component: 28 | 29 | {{{diff_step 10.6}}} 30 | 31 | Now we will modify the `users` subscription to accept the search-pattern, which will be used as a filter for the result-set; 32 | 33 | {{{diff_step 10.7}}} 34 | 35 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/privacy" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/google-maps"}}} 36 | -------------------------------------------------------------------------------- /server/publications.ts: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Mongo } from 'meteor/mongo'; 3 | import { Chats, Messages, Pictures, Users } from '../imports/collections'; 4 | import { Chat, Message, Picture, User } from '../imports/models'; 5 | 6 | Meteor.publishComposite('users', function( 7 | pattern: string 8 | ): PublishCompositeConfig { 9 | if (!this.userId) { 10 | return; 11 | } 12 | 13 | let selector = {}; 14 | 15 | if (pattern) { 16 | selector = { 17 | 'profile.name': { $regex: pattern, $options: 'i' } 18 | }; 19 | } 20 | 21 | return { 22 | find: () => { 23 | return Users.collection.find(selector, { 24 | fields: { profile: 1 }, 25 | limit: 15 26 | }); 27 | }, 28 | 29 | children: [ 30 | > { 31 | find: (user) => { 32 | return Pictures.collection.find(user.profile.pictureId, { 33 | fields: { url: 1 } 34 | }); 35 | } 36 | } 37 | ] 38 | }; 39 | }); 40 | 41 | Meteor.publish('messages', function( 42 | chatId: string, 43 | messagesBatchCounter: number): Mongo.Cursor { 44 | if (!this.userId || !chatId) { 45 | return; 46 | } 47 | 48 | return Messages.collection.find({ 49 | chatId 50 | }, { 51 | sort: { createdAt: -1 }, 52 | limit: 30 * messagesBatchCounter 53 | }); 54 | }); 55 | 56 | Meteor.publishComposite('chats', function(): PublishCompositeConfig { 57 | if (!this.userId) { 58 | return; 59 | } 60 | 61 | return { 62 | find: () => { 63 | return Chats.collection.find({ memberIds: this.userId }); 64 | }, 65 | 66 | children: [ 67 | > { 68 | find: (chat) => { 69 | return Messages.collection.find({ chatId: chat._id }, { 70 | sort: { createdAt: -1 }, 71 | limit: 1 72 | }); 73 | } 74 | }, 75 | > { 76 | find: (chat) => { 77 | return Users.collection.find({ 78 | _id: { $in: chat.memberIds } 79 | }, { 80 | fields: { profile: 1 } 81 | }); 82 | }, 83 | children: [ 84 | > { 85 | find: (user, chat) => { 86 | return Pictures.collection.find(user.profile.pictureId, { 87 | fields: { url: 1 } 88 | }); 89 | } 90 | } 91 | ] 92 | } 93 | ] 94 | }; 95 | }); 96 | 97 | Meteor.publish('user', function () { 98 | if (!this.userId) { 99 | return; 100 | } 101 | 102 | const profile = Users.findOne(this.userId).profile || {}; 103 | 104 | return Pictures.collection.find({ 105 | _id: profile.pictureId 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /manuals/templates/root.md.tmpl: -------------------------------------------------------------------------------- 1 | Facing your next mobile app project, you want to choose the best solutions to start fast while also solutions that will stay relevant when your project grows and scales. 2 | 3 | The two companies that took the concept of creating a platform with a complete set of tools for your development needs and not just another framework to the furthest are `Meteor` and `Ionic`. 4 | 5 | **Ionic** - Ionic has become one of the most popular solutions to develop hybrid mobile apps fast across different platform. 6 | 7 | The `Ionic` platform includes solutions for prototyping, build, testing, deploying apps, a market of starter apps, plugins and themes, CLI integration and push notifications service. (Further writing by an Ionic person) 8 | 9 | **Meteor** - But your app needs a full stack solution. 10 | 11 | `Meteor` has become the only open source JavaScript platform that supply the complete set of solutions you need to create a real time mobile connected apps. 12 | 13 | The `Meteor` platform is reliable, fast and easy to develop and deploy and it will also handle all the complexities of your app when it grows and scales with time. 14 | 15 | ### So which one should you choose? 16 | 17 | Your best option is to use them both! 18 | 19 | Both companies made the steps needed to support each other: 20 | 21 | - [Meteor now has official support for Angular with it's 3rd party libraries!](http://info.meteor.com/blog/official-angular-support-with-angular-meteor-1.0.0?__hstc=219992390.d5a12b08bbf681831d288088f2c1b55f.1476117688291.1482430169317.1482433129287.88&__hssc=219992390.2.1482433129287&__hsfp=2355228760) 22 | - [Ionic added official support for Meteor's packaging system](https://github.com/driftyco/ionic/pull/3133) 23 | 24 | So now we can use the strengthnesses of each of those platform combined to create the ultimate stack for your mobile apps. 25 | 26 | In this tutorial, we will create a full `WhatsApp` clone using Angular and `Ionic` framework as a client platform and `Meteor`'s reactive collections and authentication packages as our back-end. 27 | 28 | #### Chapters 29 | 30 | - **[Step 1](manuals/views/step1.md)** - Bootstrapping 31 | - **[Step 2](manuals/views/step2.md)** - Chats Page 32 | - **[Step 3](manuals/views/step3.md)** - RxJS 33 | - **[Step 4](manuals/views/step4.md)** - Realtime Meteor Server 34 | - **[Step 5](manuals/views/step5.md)** - Folder Structure 35 | - **[Step 6](manuals/views/step6.md)** - Messages Page 36 | - **[Step 7](manuals/views/step7.md)** - Users & Authentication 37 | - **[Step 8](manuals/views/step8.md)** - Chats Creation & Removal 38 | - **[Step 9](manuals/views/step9.md)** - Privacy & Subscriptions 39 | - **[Step 10](manuals/views/step10.md)** - Filter & Pagination 40 | - **[Step 11](manuals/views/step11.md)** - Google Maps & Geolocation 41 | - **[Step 12](manuals/views/step12.md)** - File Upload & Images 42 | - **[Step 13](manuals/views/step13.md)** - Native Mobile 43 | 44 | {{{nav_step ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/setup"}}} 45 | -------------------------------------------------------------------------------- /manuals/templates/step8.md.tmpl: -------------------------------------------------------------------------------- 1 | Our next step is about adding the ability to create new chats. We have the `ChatsPage` and the authentication system, but we need to hook them up some how. Let's define the initial `User` schema which will be used to retrieve its relevant information in our application: 2 | 3 | {{{diff_step 8.1}}} 4 | 5 | `Meteor` comes with a built-in users collection, defined as `Meteor.users`, but since we're using `Observables` vastly, we will wrap our collection with one: 6 | 7 | {{{diff_step 8.2}}} 8 | 9 | For accessibility, we're gonna export the collection from the `index` file as well: 10 | 11 | {{{diff_step 8.3}}} 12 | 13 | ## Chats Creation 14 | 15 | We will be using `Ionic`'s modal dialog to show the chat creation view. The first thing we're gonna do would be implementing the component itself, along with its view and stylesheet: 16 | 17 | {{{diff_step 8.4}}} 18 | 19 | {{{diff_step 8.5}}} 20 | 21 | {{{diff_step 8.6}}} 22 | 23 | The dialog should contain a list of all the users whose chat does not exist yet. Once we click on one of these users we should be demoted to the chats view with the new chat we've just created. 24 | 25 | The dialog should be revealed whenever we click on one of the options in the options pop-over, therefore, we will implement the necessary handler: 26 | 27 | {{{diff_step 8.7}}} 28 | 29 | And bind it to the `click` event: 30 | 31 | {{{diff_step 8.8}}} 32 | 33 | We will import the newly created component in the app's `NgModule` as well, so it can be recognized properly: 34 | 35 | {{{diff_step 8.9}}} 36 | 37 | We're also required to implement the appropriate `Meteor` method which will be the actually handler for feeding our data-base with newly created chats: 38 | 39 | {{{diff_step 8.10}}} 40 | 41 | As you can see, a chat is inserted with an additional `memberIds` field. Whenever we have such a change we should update the model's schema accordingly, in this case we're talking about adding the `memberIds` field, like so: 42 | 43 | {{{diff_step 8.11}}} 44 | 45 | Thanks to our new-chat dialog, we can create chats dynamically with no need in initial fabrication. Let's replace the chats fabrication with users fabrication in the Meteor server: 46 | 47 | {{{diff_step 8.12}}} 48 | 49 | Since we've changed the data fabrication method, the chat's title and picture are not hard-coded anymore, therefore, any additional data should be fetched in the components themselves: 50 | 51 | {{{diff_step 8.13}}} 52 | 53 | Now we want our changes to take effect. We will reset the database so next time we run our `Meteor` server the users will be fabricated. To reset the database, first make sure the `Meteor` server is stopped , and then type the following command: 54 | 55 | $ meteor reset 56 | 57 | Now, as soon as you start the server, new users should be fabricated and inserted into the database: 58 | 59 | $ npm run start 60 | 61 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/authentication" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/privacy"}}} 62 | -------------------------------------------------------------------------------- /server/methods.ts: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Chats, Messages } from '../imports/collections'; 3 | import { MessageType, Profile } from '../imports/models'; 4 | import { check, Match } from 'meteor/check'; 5 | 6 | const nonEmptyString = Match.Where((str) => { 7 | check(str, String); 8 | return str.length > 0; 9 | }); 10 | 11 | Meteor.methods({ 12 | addChat(receiverId: string): void { 13 | if (!this.userId) { 14 | throw new Meteor.Error('unauthorized', 15 | 'User must be logged-in to create a new chat'); 16 | } 17 | 18 | check(receiverId, nonEmptyString); 19 | 20 | if (receiverId === this.userId) { 21 | throw new Meteor.Error('illegal-receiver', 22 | 'Receiver must be different than the current logged in user'); 23 | } 24 | 25 | const chatExists = !!Chats.collection.find({ 26 | memberIds: { $all: [this.userId, receiverId] } 27 | }).count(); 28 | 29 | if (chatExists) { 30 | throw new Meteor.Error('chat-exists', 31 | 'Chat already exists'); 32 | } 33 | 34 | const chat = { 35 | memberIds: [this.userId, receiverId] 36 | }; 37 | 38 | Chats.insert(chat); 39 | }, 40 | 41 | removeChat(chatId: string): void { 42 | if (!this.userId) { 43 | throw new Meteor.Error('unauthorized', 44 | 'User must be logged-in to remove chat'); 45 | } 46 | 47 | check(chatId, nonEmptyString); 48 | 49 | const chatExists = !!Chats.collection.find(chatId).count(); 50 | 51 | if (!chatExists) { 52 | throw new Meteor.Error('chat-not-exists', 53 | 'Chat doesn\'t exist'); 54 | } 55 | 56 | Chats.remove(chatId); 57 | }, 58 | 59 | updateProfile(profile: Profile): void { 60 | if (!this.userId) throw new Meteor.Error('unauthorized', 61 | 'User must be logged-in to create a new chat'); 62 | 63 | check(profile, { 64 | name: nonEmptyString, 65 | pictureId: Match.Maybe(nonEmptyString) 66 | }); 67 | 68 | Meteor.users.update(this.userId, { 69 | $set: {profile} 70 | }); 71 | }, 72 | 73 | addMessage(type: MessageType, chatId: string, content: string) { 74 | if (!this.userId) throw new Meteor.Error('unauthorized', 75 | 'User must be logged-in to create a new chat'); 76 | 77 | check(type, Match.OneOf(String, [ MessageType.TEXT, MessageType.LOCATION ])); 78 | check(chatId, nonEmptyString); 79 | check(content, nonEmptyString); 80 | 81 | const chatExists = !!Chats.collection.find(chatId).count(); 82 | 83 | if (!chatExists) { 84 | throw new Meteor.Error('chat-not-exists', 85 | 'Chat doesn\'t exist'); 86 | } 87 | 88 | return { 89 | messageId: Messages.collection.insert({ 90 | chatId: chatId, 91 | senderId: this.userId, 92 | content: content, 93 | createdAt: new Date(), 94 | type: type 95 | }) 96 | }; 97 | }, 98 | countMessages(): number { 99 | return Messages.collection.find().count(); 100 | } 101 | }); -------------------------------------------------------------------------------- /client/imports/pages/messages/messages.scss: -------------------------------------------------------------------------------- 1 | .messages-page-navbar { 2 | .chat-picture { 3 | width: 50px; 4 | border-radius: 50%; 5 | float: left; 6 | } 7 | 8 | .chat-title { 9 | line-height: 50px; 10 | float: left; 11 | } 12 | } 13 | 14 | .messages-page-content { 15 | > .scroll-content { 16 | margin: 42px -16px 42px !important; 17 | } 18 | 19 | .day-wrapper .day-timestamp { 20 | margin-left: calc(50% - 64px); 21 | margin-right: calc(50% - 64px); 22 | margin-bottom: 9px; 23 | text-align: center; 24 | line-height: 27px; 25 | height: 27px; 26 | border-radius: 3px; 27 | color: gray; 28 | box-shadow: 0 1px 2px rgba(0, 0, 0, .15); 29 | background: #d9effa; 30 | } 31 | 32 | .messages { 33 | height: 100%; 34 | background-image: url(/assets/chat-background.jpg); 35 | background-color: #E0DAD6; 36 | background-repeat: no-repeat; 37 | background-size: cover; 38 | } 39 | 40 | .message-wrapper { 41 | margin-bottom: 9px; 42 | 43 | &::after { 44 | content: ""; 45 | display: table; 46 | clear: both; 47 | } 48 | } 49 | 50 | .message { 51 | display: inline-block; 52 | position: relative; 53 | max-width: 65vh; 54 | border-radius: 7px; 55 | box-shadow: 0 1px 2px rgba(0, 0, 0, .15); 56 | 57 | &.message-mine { 58 | float: right; 59 | background-color: #DCF8C6; 60 | 61 | &::before { 62 | right: -11px; 63 | background-image: url(/assets/message-mine.png) 64 | } 65 | } 66 | 67 | &.message-other { 68 | float: left; 69 | background-color: #FFF; 70 | 71 | &::before { 72 | left: -11px; 73 | background-image: url(/assets/message-other.png) 74 | } 75 | } 76 | 77 | &.message-other::before, &.message-mine::before { 78 | content: ""; 79 | position: absolute; 80 | bottom: 3px; 81 | width: 12px; 82 | height: 19px; 83 | background-position: 50% 50%; 84 | background-repeat: no-repeat; 85 | background-size: contain; 86 | } 87 | 88 | .message-content { 89 | padding: 5px 7px; 90 | word-wrap: break-word; 91 | 92 | &::after { 93 | content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0"; 94 | display: inline; 95 | } 96 | 97 | .sebm-google-map-container { 98 | height: 25vh; 99 | width: 35vh; 100 | } 101 | } 102 | 103 | .message-timestamp { 104 | position: absolute; 105 | bottom: 2px; 106 | right: 7px; 107 | font-size: 12px; 108 | color: gray; 109 | } 110 | } 111 | } 112 | 113 | .messages-page-footer { 114 | padding-right: 0; 115 | 116 | .message-editor { 117 | margin-left: 2px; 118 | padding-left: 5px; 119 | background: white; 120 | border-radius: 3px; 121 | } 122 | 123 | .message-editor-button { 124 | box-shadow: none; 125 | width: 50px; 126 | height: 50px; 127 | font-size: 17px; 128 | margin: auto; 129 | } 130 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to build a Whatsapp-clone using Ionic 2 and Meteor CLI 2 | 3 | Facing your next mobile app project, you want to choose the best solutions to start fast while also solutions that will stay relevant when your project grows and scales. 4 | 5 | The two companies that took the concept of creating a platform with a complete set of tools for your development needs and not just another framework to the furthest are `Meteor` and `Ionic`. 6 | 7 | **Ionic** - Ionic has become one of the most popular solutions to develop hybrid mobile apps fast across different platform. 8 | 9 | The `Ionic` platform includes solutions for prototyping, build, testing, deploying apps, a market of starter apps, plugins and themes, CLI integration and push notifications service. (Further writing by an Ionic person) 10 | 11 | **Meteor** - But your app needs a full stack solution. 12 | 13 | `Meteor` has become the only open source JavaScript platform that supply the complete set of solutions you need to create a real time mobile connected apps. 14 | 15 | The `Meteor` platform is reliable, fast and easy to develop and deploy and it will also handle all the complexities of your app when it grows and scales with time. 16 | 17 | ### So which one should you choose? 18 | 19 | Your best option is to use them both! 20 | 21 | Both companies made the steps needed to support each other: 22 | 23 | - [Meteor now has official support for Angular with it's 3rd party libraries!](http://info.meteor.com/blog/official-angular-support-with-angular-meteor-1.0.0?__hstc=219992390.d5a12b08bbf681831d288088f2c1b55f.1476117688291.1482430169317.1482433129287.88&__hssc=219992390.2.1482433129287&__hsfp=2355228760) 24 | - [Ionic added official support for Meteor's packaging system](https://github.com/driftyco/ionic/pull/3133) 25 | 26 | So now we can use the strengthnesses of each of those platform combined to create the ultimate stack for your mobile apps. 27 | 28 | In this tutorial, we will create a full `WhatsApp` clone using Angular and `Ionic` framework as a client platform and `Meteor`'s reactive collections and authentication packages as our back-end. 29 | 30 | #### Chapters 31 | 32 | - **[Step 1](manuals/views/step1.md)** - Bootstrapping 33 | - **[Step 2](manuals/views/step2.md)** - Chats Page 34 | - **[Step 3](manuals/views/step3.md)** - RxJS 35 | - **[Step 4](manuals/views/step4.md)** - Realtime Meteor Server 36 | - **[Step 5](manuals/views/step5.md)** - Folder Structure 37 | - **[Step 6](manuals/views/step6.md)** - Messages Page 38 | - **[Step 7](manuals/views/step7.md)** - Users & Authentication 39 | - **[Step 8](manuals/views/step8.md)** - Chats Creation & Removal 40 | - **[Step 9](manuals/views/step9.md)** - Privacy & Subscriptions 41 | - **[Step 10](manuals/views/step10.md)** - Filter & Pagination 42 | - **[Step 11](manuals/views/step11.md)** - Google Maps & Geolocation 43 | - **[Step 12](manuals/views/step12.md)** - File Upload & Images 44 | - **[Step 13](manuals/views/step13.md)** - Native Mobile 45 | 46 | [{]: (nav_step ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/setup") 47 | | [Begin Tutorial >](https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/setup) | 48 | |----------------------:| 49 | [}]: # 50 | 51 | -------------------------------------------------------------------------------- /client/imports/services/picture.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Platform } from 'ionic-angular'; 3 | import { ImagePicker } from 'ionic-native'; 4 | import { UploadFS } from 'meteor/jalik:ufs'; 5 | import { _ } from 'meteor/underscore'; 6 | import { PicturesStore } from '../../../imports/collections'; 7 | import { DEFAULT_PICTURE_URL } from '../../../imports/models'; 8 | 9 | @Injectable() 10 | export class PictureService { 11 | constructor(private platform: Platform) { 12 | } 13 | 14 | select(): Promise { 15 | if (!this.platform.is('cordova') || !this.platform.is('mobile')) { 16 | return new Promise((resolve, reject) => { 17 | try { 18 | UploadFS.selectFile((file: File) => { 19 | resolve(file); 20 | }); 21 | } 22 | catch (e) { 23 | reject(e); 24 | } 25 | }); 26 | } 27 | 28 | return ImagePicker.getPictures({maximumImagesCount: 1}).then((URL: string) => { 29 | return this.convertURLtoBlob(URL); 30 | }); 31 | } 32 | 33 | upload(blob: Blob): Promise { 34 | return new Promise((resolve, reject) => { 35 | const metadata = _.pick(blob, 'name', 'type', 'size'); 36 | 37 | if (!metadata.name) { 38 | metadata.name = DEFAULT_PICTURE_URL; 39 | } 40 | 41 | const upload = new UploadFS.Uploader({ 42 | data: blob, 43 | file: metadata, 44 | store: PicturesStore, 45 | onComplete: resolve, 46 | onError: reject 47 | }); 48 | 49 | upload.start(); 50 | }); 51 | } 52 | 53 | convertURLtoBlob(URL: string): Promise { 54 | return new Promise((resolve, reject) => { 55 | const image = document.createElement('img'); 56 | 57 | image.onload = () => { 58 | try { 59 | const dataURI = this.convertImageToDataURI(image); 60 | const blob = this.convertDataURIToBlob(dataURI); 61 | 62 | resolve(blob); 63 | } 64 | catch (e) { 65 | reject(e); 66 | } 67 | }; 68 | 69 | image.src = URL; 70 | }); 71 | } 72 | 73 | convertImageToDataURI(image: HTMLImageElement): string { 74 | // Create an empty canvas element 75 | const canvas = document.createElement('canvas'); 76 | canvas.width = image.width; 77 | canvas.height = image.height; 78 | 79 | // Copy the image contents to the canvas 80 | const context = canvas.getContext('2d'); 81 | context.drawImage(image, 0, 0); 82 | 83 | // Get the data-URL formatted image 84 | // Firefox supports PNG and JPEG. You could check image.src to 85 | // guess the original format, but be aware the using 'image/jpg' 86 | // will re-encode the image. 87 | const dataURL = canvas.toDataURL('image/png'); 88 | 89 | return dataURL.replace(/^data:image\/(png|jpg);base64,/, ''); 90 | } 91 | 92 | convertDataURIToBlob(dataURI): Blob { 93 | const binary = atob(dataURI); 94 | 95 | // Write the bytes of the string to a typed array 96 | const charCodes = Object.keys(binary) 97 | .map(Number) 98 | .map(binary.charCodeAt.bind(binary)); 99 | 100 | // Build blob with typed array 101 | return new Blob([new Uint8Array(charCodes)], {type: 'image/jpeg'}); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /client/imports/pages/chats/new-chat.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AlertController, ViewController } from 'ionic-angular'; 3 | import { MeteorObservable } from 'meteor-rxjs'; 4 | import { _ } from 'meteor/underscore'; 5 | import { Observable, Subscription, BehaviorSubject } from 'rxjs'; 6 | import { Chats, Pictures, Users } from '../../../../imports/collections'; 7 | import { User } from '../../../../imports/models'; 8 | import template from './new-chat.html'; 9 | 10 | @Component({ 11 | template 12 | }) 13 | export class NewChatComponent implements OnInit { 14 | searchPattern: BehaviorSubject; 15 | senderId: string; 16 | users: Observable; 17 | usersSubscription: Subscription; 18 | 19 | constructor( 20 | private alertCtrl: AlertController, 21 | private viewCtrl: ViewController 22 | ) { 23 | this.senderId = Meteor.userId(); 24 | this.searchPattern = new BehaviorSubject(undefined); 25 | } 26 | 27 | ngOnInit() { 28 | this.observeSearchBar(); 29 | } 30 | 31 | updateSubscription(newValue) { 32 | this.searchPattern.next(newValue); 33 | } 34 | 35 | observeSearchBar(): void { 36 | this.searchPattern.asObservable() 37 | // Prevents the search bar from being spammed 38 | .debounce(() => Observable.timer(1000)) 39 | .forEach(() => { 40 | if (this.usersSubscription) { 41 | this.usersSubscription.unsubscribe(); 42 | } 43 | 44 | this.usersSubscription = this.subscribeUsers(); 45 | }); 46 | } 47 | 48 | addChat(user): void { 49 | MeteorObservable.call('addChat', user._id).subscribe({ 50 | next: () => { 51 | this.viewCtrl.dismiss(); 52 | }, 53 | error: (e: Error) => { 54 | this.viewCtrl.dismiss().then(() => { 55 | this.handleError(e); 56 | }); 57 | } 58 | }); 59 | } 60 | 61 | subscribeUsers(): Subscription { 62 | // Fetch all users matching search pattern 63 | const subscription = MeteorObservable.subscribe('users', this.searchPattern.getValue()); 64 | const autorun = MeteorObservable.autorun(); 65 | 66 | return Observable.merge(subscription, autorun).subscribe(() => { 67 | this.users = this.findUsers(); 68 | }); 69 | } 70 | 71 | findUsers(): Observable { 72 | // Find all belonging chats 73 | return Chats.find({ 74 | memberIds: this.senderId 75 | }, { 76 | fields: { 77 | memberIds: 1 78 | } 79 | }) 80 | // Invoke merge-map with an empty array in case no chat found 81 | .startWith([]) 82 | .mergeMap((chats) => { 83 | // Get all userIDs who we're chatting with 84 | const receiverIds = _.chain(chats) 85 | .pluck('memberIds') 86 | .flatten() 87 | .concat(this.senderId) 88 | .value(); 89 | 90 | // Find all users which are not in belonging chats 91 | return Users.find({ 92 | _id: { $nin: receiverIds } 93 | }) 94 | // Invoke map with an empty array in case no user found 95 | .startWith([]); 96 | }); 97 | } 98 | 99 | handleError(e: Error): void { 100 | console.error(e); 101 | 102 | const alert = this.alertCtrl.create({ 103 | buttons: ['OK'], 104 | message: e.message, 105 | title: 'Oops!' 106 | }); 107 | 108 | alert.present(); 109 | } 110 | 111 | getPic(pictureId): string { 112 | return Pictures.getPictureUrl(pictureId); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /client/imports/pages/chats/chats.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavController, PopoverController, ModalController, AlertController } from 'ionic-angular'; 3 | import { MeteorObservable } from 'meteor-rxjs'; 4 | import * as Moment from 'moment'; 5 | import { Observable, Subscriber } from 'rxjs'; 6 | import { Chats, Messages, Users, Pictures } from '../../../../imports/collections'; 7 | import { Chat, Message } from '../../../../imports/models'; 8 | import { ChatsOptionsComponent } from './chats-options'; 9 | import { MessagesPage } from '../messages/messages'; 10 | import template from './chats.html'; 11 | import { NewChatComponent } from './new-chat'; 12 | 13 | @Component({ 14 | template 15 | }) 16 | export class ChatsPage implements OnInit { 17 | chats; 18 | senderId: string; 19 | 20 | constructor( 21 | private navCtrl: NavController, 22 | private popoverCtrl: PopoverController, 23 | private modalCtrl: ModalController, 24 | private alertCtrl: AlertController) { 25 | this.senderId = Meteor.userId(); 26 | } 27 | 28 | ngOnInit() { 29 | MeteorObservable.subscribe('chats').subscribe(() => { 30 | MeteorObservable.autorun().subscribe(() => { 31 | this.chats = this.findChats(); 32 | }); 33 | }); 34 | } 35 | 36 | addChat(): void { 37 | const modal = this.modalCtrl.create(NewChatComponent); 38 | modal.present(); 39 | } 40 | 41 | findChats(): Observable { 42 | // Find chats and transform them 43 | return Chats.find().map(chats => { 44 | chats.forEach(chat => { 45 | chat.title = ''; 46 | chat.picture = ''; 47 | 48 | const receiverId = chat.memberIds.find(memberId => memberId !== this.senderId); 49 | const receiver = Users.findOne(receiverId); 50 | 51 | if (receiver) { 52 | chat.title = receiver.profile.name; 53 | chat.picture = Pictures.getPictureUrl(receiver.profile.pictureId); 54 | } 55 | 56 | // This will make the last message reactive 57 | this.findLastChatMessage(chat._id).subscribe((message) => { 58 | chat.lastMessage = message; 59 | }); 60 | }); 61 | 62 | return chats; 63 | }); 64 | } 65 | 66 | findLastChatMessage(chatId: string): Observable { 67 | return Observable.create((observer: Subscriber) => { 68 | const chatExists = () => !!Chats.findOne(chatId); 69 | 70 | // Re-compute until chat is removed 71 | MeteorObservable.autorun().takeWhile(chatExists).subscribe(() => { 72 | Messages.find({ chatId }, { 73 | sort: { createdAt: -1 } 74 | }).subscribe({ 75 | next: (messages) => { 76 | // Invoke subscription with the last message found 77 | if (!messages.length) { 78 | return; 79 | } 80 | 81 | const lastMessage = messages[0]; 82 | observer.next(lastMessage); 83 | }, 84 | error: (e) => { 85 | observer.error(e); 86 | }, 87 | complete: () => { 88 | observer.complete(); 89 | } 90 | }); 91 | }); 92 | }); 93 | } 94 | 95 | showMessages(chat): void { 96 | this.navCtrl.push(MessagesPage, {chat}); 97 | } 98 | 99 | removeChat(chat: Chat): void { 100 | MeteorObservable.call('removeChat', chat._id).subscribe({ 101 | error: (e: Error) => { 102 | if (e) { 103 | this.handleError(e); 104 | } 105 | } 106 | }); 107 | } 108 | 109 | handleError(e: Error): void { 110 | console.error(e); 111 | 112 | const alert = this.alertCtrl.create({ 113 | buttons: ['OK'], 114 | message: e.message, 115 | title: 'Oops!' 116 | }); 117 | 118 | alert.present(); 119 | } 120 | 121 | showOptions(): void { 122 | const popover = this.popoverCtrl.create(ChatsOptionsComponent, {}, { 123 | cssClass: 'options-popover chats-options-popover' 124 | }); 125 | 126 | popover.present(); 127 | } 128 | } -------------------------------------------------------------------------------- /manuals/templates/step11.md.tmpl: -------------------------------------------------------------------------------- 1 | In this step we will add the ability to send the current location in [Google Maps](https://www.google.com/maps/). 2 | 3 | {{{diff_step 11.1}}} 4 | 5 | ## Geo Location 6 | 7 | To get the devices location (aka `geo-location`) we will install a `Cordova` plug-in called `cordova-plugin-geolocation` which will provide us with these abilities: 8 | 9 | $ meteor add cordova:cordova-plugin-geolocation@2.4.1 10 | 11 | ## Angular 2 Google Maps 12 | 13 | Since the location is going to be presented with `Google Maps`, we will install a package which will help up interact with it in `Angular 2`: 14 | 15 | $ meteor npm install --save angular2-google-maps 16 | 17 | Before you import the installed package to the app's `NgModule` be sure to generate an API key. An API key is a code passed in by computer programs calling an API to identify the calling program, its developer, or its user to the Web site. To generate an API key go to [Google Maps API documentation page](https://developers.google.com/maps/documentation/javascript/get-api-key) and follow the instructions. **Each app should have it's own API key**, as for now we can just use an API key we generated for the sake of this tutorial, but once you are ready for production, **replace the API key in the script below**: 18 | 19 | {{{diff_step 11.3}}} 20 | 21 | ## Attachments Menu 22 | 23 | Before we proceed any further, we will add a new message type to our schema, so we can differentiate between a text message and a location message: 24 | 25 | {{{diff_step 11.4}}} 26 | 27 | We want the user to be able to send a location message through an attachments menu in the `MessagesPage`, so let's implement the initial `MessagesAttachmentsComponent`, and as we go through, we will start filling it up: 28 | 29 | {{{diff_step 11.5}}} 30 | 31 | {{{diff_step 11.6}}} 32 | 33 | {{{diff_step 11.7}}} 34 | 35 | {{{diff_step 11.8}}} 36 | 37 | We will add a generic style-sheet for the attachments menu since it can also use us in the future: 38 | 39 | {{{diff_step 11.9}}} 40 | 41 | Now we will add a handler in the `MessagesPage` which will open the newly created menu, and we will bind it to the view: 42 | 43 | {{{diff_step 11.10}}} 44 | 45 | {{{diff_step 11.11}}} 46 | 47 | ## Sending Location 48 | 49 | A location is a composition of longitude, latitude and an altitude, or in short: `long, lat, alt`. Let's define a new `Location` model which will represent the mentioned schema: 50 | 51 | {{{diff_step 11.12}}} 52 | 53 | Up next, would be implementing the actual component which will handle geo-location sharing: 54 | 55 | {{{diff_step 11.13}}} 56 | 57 | Basically, what this component does is refreshing the current geo-location at a specific refresh rate. Note that in order to fetch the geo-location we use `Geolocation's` API, but behind the scene it uses ``cordova-plugin-geolocation`. The `sendLocation` method dismisses the view and returns the calculated geo-location. Now let's added the component's corresponding view: 58 | 59 | {{{diff_step 11.14}}} 60 | 61 | The `sebm-google-map` is the component which represents the map itself, and we provide it with `lat`, `lng` and `zoom`, so the map can be focused on the current geo-location. If you'll notice, we also used the `sebm-google-map-marker` component with the same data-models, so the marker will be shown right in the center of the map. 62 | 63 | Now we will add some `CSS` to make sure the map is visible: 64 | 65 | {{{diff_step 11.15}}} 66 | 67 | And we will import the component: 68 | 69 | {{{diff_step 11.16}}} 70 | 71 | The component is ready. The only thing left to do would be revealing it. So we will add the appropriate handler in the `MessagesAttachmentsComponent`: 72 | 73 | {{{diff_step 11.17}}} 74 | 75 | And we will bind it to its view: 76 | 77 | {{{diff_step 11.18}}} 78 | 79 | Now we will implement a new method in the `MessagesPage`, called `sendLocationMessage`, which will create a string representation of the current geo-location and send it to the server: 80 | 81 | {{{diff_step 11.19}}} 82 | 83 | This requires us to update the `addMessage` method in the server so it can support location typed messages: 84 | 85 | {{{diff_step 11.20}}} 86 | 87 | ## Viewing Location Messages 88 | 89 | The infrastructure is ready, but we can't yet see the message, therefore, we will need to add support for location messages in the `MessagesPage` view: 90 | 91 | {{{diff_step 11.21}}} 92 | 93 | These additions looks pretty similar to the `LocationMessage` since they are based on the same core components. 94 | 95 | We will now add a method which can parse a string representation of the location into an actual `JSON`: 96 | 97 | {{{diff_step 11.22}}} 98 | 99 | And we will make some final adjustments for the view so the map can be presented properly: 100 | 101 | {{{diff_step 11.23}}} 102 | 103 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/filter-and-pagination" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/file-upload"}}} 104 | -------------------------------------------------------------------------------- /manuals/templates/step9.md.tmpl: -------------------------------------------------------------------------------- 1 | In this step we gonna take care of the app's security and encapsulation, since we don't want the users to do whatever they want, and we don't want them to be able to see content which is irrelevant for them. 2 | 3 | We gonna start by removing a `Meteor` package named `insecure`. This package provides the client with the ability to run collection mutation methods. This is a behavior we are not interested in since removing data and creating data should be done in the server and only after certain validations. `Meteor` includes this package by default only for development purposes and it should be removed once our app is ready for production. As said, we will remove this package by typing the following command: 4 | 5 | $ meteor remove insecure 6 | 7 | ## Secured Mutations 8 | 9 | Since we enabled restrictions to run certain operations on data-collections directly from the client, we will need to define a method on the server which will handle each of these. By calling these methods, we will be able to manipulate the data the way we want, but not directly. The first method we're going to take care of would be the `removeChat` method, which will handle, obviously, chat removals by given ID: 10 | 11 | {{{diff_step 9.2}}} 12 | 13 | We will carefully replace the removal method invocation in the `ChatsPage` with the method we've just defined: 14 | 15 | {{{diff_step 9.3}}} 16 | 17 | In the `MessagesPage` we have options icon presented as three periods at the right side of the navigation bar. We will now implement this option menu which should pop-over once clicked. We will start by implementing its corresponding component called `MessagesOptionsComponent`, along with its view-template, style-sheet, and necessary importations: 18 | 19 | {{{diff_step 9.4}}} 20 | 21 | {{{diff_step 9.5}}} 22 | 23 | {{{diff_step 9.6}}} 24 | 25 | {{{diff_step 9.7}}} 26 | 27 | Now that the component is ready, we will implement the handler in the `MessagesPage` which will actually show it, using the `PopoverController`: 28 | 29 | {{{diff_step 9.8}}} 30 | 31 | And we will bind the handler for the view so any time we press on the `options` button the event will be trigger the handler: 32 | 33 | {{{diff_step 9.9}}} 34 | 35 | Right now all the chats are published to all the clients which is not very good for privacy, and it's inefficient since the entire data-base is being fetched automatically rather than fetching only the data which is necessary for the current view. This behavior occurs because of a `Meteor` package, which is installed by default for development purposes, called `autopublish`. To get rid of the auto-publishing behavior we will need to get rid of the `autopublish` package as well: 36 | 37 | $ meteor remove autopublish 38 | 39 | This requires us to explicitly define our publications. We will start with the `users` publication which will be used in the `NewChatComponent` to fetch all the users who we can potentially chat with: 40 | 41 | {{{diff_step 9.11}}} 42 | 43 | The second publication we're going to implement would be the `messages` publication which will be used in the `MessagesPage`: 44 | 45 | {{{diff_step 9.12}}} 46 | 47 | As you see, all our publications so far are only focused on fetching data from a single collection. We will now add the [publish-composite](https://atmospherejs.com/reywood/publish-composite) package which will help us implement joined collection publications: 48 | 49 | $ meteor add reywood:publish-composite 50 | 51 | We will install the package's declarations as well so the compiler can recognize the extensions made in `Meteor`'s API: 52 | 53 | $ meteor npm install --save @types/meteor-publish-composite 54 | 55 | And we will import the declarations by adding the following field in the `tsconfig` file: 56 | 57 | {{{diff_step 9.15}}} 58 | 59 | Now we will implement our first composite-publication, called `chats`. Why exactly does the `chats` publication has to count on multiple collections? That's because we're relying on multiple collections when presenting the data in the `ChatsPage`: 60 | 61 | - **ChatsCollection** - Used to retrieve the actual information for each chat. 62 | - **MessagesCollection** - Used to retrieve the last message for the corresponding chat. 63 | - **UsersCollection** - Used to retrieve the receiver's information for the corresponding chat. 64 | 65 | To implement this composite publication we will use the `Meteor.publishComposite` method: 66 | 67 | {{{diff_step 9.16}}} 68 | 69 | The `chats` publication is made out of several nodes, which are structured according to the list above. 70 | 71 | We finished with all the necessary publications for now, all is left to do is using them. The usages of these publications are called `subscriptions`, so whenever we subscribe to a publication, we will fetch the data exported by it, and then we can run queries of this data in our client, as we desire. 72 | 73 | The first subscription we're going to make would be the `users` subscription in the `NewChatComponent`, so whenever we open the dialog a subscription should be made: 74 | 75 | {{{diff_step 9.17}}} 76 | 77 | The second subscription we're going to define would be the `chats` subscription in the `ChatsPage`, this way we will have the necessary data to work with when presenting the users we're chatting with: 78 | 79 | {{{diff_step 9.18}}} 80 | 81 | The `messages` publication is responsible for bringing all the relevant messages for a certain chat. Unlike the other two publications, this publication is actually parameterized and it requires us to pass a chat id during subscription. Let's subscribe to the `messages` publication in the `MessagesPage`, and pass the current active chat ID provided to us by the navigation parameters: 82 | 83 | {{{diff_step 9.19}}} 84 | 85 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-mutations" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/filter-and-pagination"}}} 86 | -------------------------------------------------------------------------------- /manuals/templates/step2.md.tmpl: -------------------------------------------------------------------------------- 1 | ## First Ionic Component 2 | 3 | Now that we're finished with the initial setup, we can start building our app. 4 | 5 | Our app is gonna have a very clear methodology. It's gonna be made out of pages, and each page will be made out of 3 files: 6 | 7 | - `.html` - A view template file written in `HTML` based on `Angular 2`'s new [template engine](https://angular.io/docs/ts/latest/guide/template-syntax.html). 8 | - `.scss` - A stylesheet file written in a `CSS` pre-process language called [SASS](https://sass-lang.com). 9 | - `.ts` - A script file written in `Typescript`. 10 | 11 | Following this pattern, we will create our first page, starting with its component - named `ChatsPage`: 12 | 13 | {{{diff_step 2.1}}} 14 | 15 | `Angular 2` uses decorators to declare `Component`s, and we use `ES2016` classes to create the actual component, and the `template` declares the template file for the component. So now let's create this template file, next to the component file: 16 | 17 | {{{diff_step 2.2}}} 18 | 19 | Once creating an Ionic page it's recommended to use the following layout: 20 | 21 | - <ion-header> - The header of the page. Will usually contain content that should be bounded to the top like navbar. 22 | - <ion-content> - The content of the page. Will usually contain it's actual content like text. 23 | - <ion-footer> - The footer of the page. Will usually contain content that should be bounded to the bottom like toolbars. 24 | 25 | Now, we need to add a declaration for this new `Component` in our `NgModule` definition: 26 | 27 | {{{diff_step 2.3}}} 28 | 29 | > You can read more about [Angular 2 NgModule here](https://angular.io/docs/ts/latest/guide/ngmodule.html). 30 | 31 | We will define the `ChatsPage` as the initial component of our app by setting the `rootPage` property in the main app component: 32 | 33 | {{{diff_step 2.4}}} 34 | 35 | To make the `rootPage` visible, we will need to use the `ion-nav` component in the application's view: 36 | 37 | {{{diff_step 2.5}}} 38 | 39 | Let's add some code to our `Component` with a simple logic; Once the component is created we gonna define some dummy chats, using the `Observable.of`, so we can have some data to test our view against: 40 | 41 | {{{diff_step 2.6}}} 42 | 43 | > Further explanation regards `RxJS` can be found in [step 3](./step3.md) 44 | 45 | `moment` is an essential package for our data fabrication, which requires us to install it using the following command: 46 | 47 | $ meteor npm install --save moment 48 | 49 | ## TypeScript Interfaces 50 | 51 | Now, because we use `TypeScript`, we can define our own data-types and use them in our app, which will give you a better auto-complete and developing experience in most IDEs. In our application, we have 2 models: a `chat` model and a `message` model. Since we will probably be using these models in both client and server, we will create a dir which is common for both, called `imports`: 52 | 53 | $ mkdir imports 54 | 55 | Inside the `imports` dir, we will define our initial `models.ts` file: 56 | 57 | {{{diff_step 2.8}}} 58 | 59 | Now that the models are up and set, we can use apply it to the `ChatsPage`: 60 | 61 | {{{diff_step 2.9}}} 62 | 63 | ## Ionic's Theming System 64 | 65 | `Ionic 2` provides us with a comfortable theming system which is based on `SASS` variables. The theme definition file is located in `client/imports/theme/variable.scss`. Since we want our app to have a "Whatsappish" look, we will define a new `SASS` variable called `whatsapp` in the variables file: 66 | 67 | {{{diff_step 2.10}}} 68 | 69 | The `whatsapp` color can be used by adding an attribute called `color` with a value `whatsapp` to any Ionic component. 70 | 71 | To begin with, we can start by implementing the `ChatsView` and apply our newly defined theme into it. This view will contain a list representing all the available chats in the component's data-set: 72 | 73 | {{{diff_step 2.11}}} 74 | 75 | We use `ion-list` which Ionic translates into a list, and we use `ion-item` to represent a single item in that list. A chat item includes an image, the receiver's name, and its recent message. 76 | 77 | > The `async` pipe is used to iterate through data which should be fetched asynchronously, in this case, observables. 78 | 79 | Now, in order to finish our theming and styling, let's create a stylesheet file for our component: 80 | 81 | {{{diff_step 2.12}}} 82 | 83 | Ionic will load newly defined stylesheet files automatically, so you shouldn't be worried for importations. 84 | 85 | ## External Angular 2 Modules 86 | 87 | Since `Ionic 2` uses `Angular 2` as the layer view, we can load `Angular 2` modules just like any other plain `Angular 2` application. One module that may come in our interest would be the `angular2-moment` module, which will provide us with the ability to use `moment`'s utility functions in the view as pipes. 88 | 89 | It requires us to install `angular2-moment` module using the following command: 90 | 91 | $ meteor npm install --save angular2-moment 92 | 93 | Now we will need to declare this module in the app's main component: 94 | 95 | {{{diff_step 2.14}}} 96 | 97 | Which will make `moment` available as a pack of pipes, as mentioned earlier: 98 | 99 | {{{diff_step 2.15}}} 100 | 101 | ## Ionic Touch Events 102 | 103 | Ionic provides us with components which can handle mobile events like: slide, tap and pinch. Since we're going to take advantage of the "sliding" event in the `chats` view, we used the `ion-item-sliding` component, so any time we will slide our item to the left, we should see a `Remove` button. 104 | 105 | Right now this button is not hooked to anything. It requires us to bind it into the component: 106 | 107 | {{{diff_step 2.16}}} 108 | 109 | And now that it is bound to the component we can safely implement its handler: 110 | 111 | {{{diff_step 2.17}}} 112 | 113 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/setup" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/rxjs"}}} 114 | -------------------------------------------------------------------------------- /manuals/templates/step6.md.tmpl: -------------------------------------------------------------------------------- 1 | In this step we will add the messages view and the ability to send messages. 2 | 3 | Before we implement anything related to the messages pages, we first have to make sure that once we click on a chat item in the chats page, we will be promoted into its corresponding messages view. Let's first implement the `showMessages()` method in the chats component: 4 | 5 | {{{diff_step 6.1}}} 6 | 7 | And let's register the click event in the view: 8 | 9 | {{{diff_step 6.2}}} 10 | 11 | Notice how we used a controller called `NavController`. The `NavController` is `Ionic`'s new method to navigate in our app. We can also use a traditional router, but since in a mobile app we have no access to the url bar, this might come more in handy. You can read more about the `NavController` [here](http://ionicframework.com/docs/v2/api/components/nav/NavController/). 12 | 13 | Let's go ahead and implement the messages component. We'll call it `MessagesPage`: 14 | 15 | {{{diff_step 6.3}}} 16 | 17 | As you can see, in order to get the chat's id we used the `NavParams` service. This is a simple service which gives you access to a key-value storage containing all the parameters we've passed using the `NavController`. 18 | 19 | For more information about the `NavParams` service, see the following [link](http://ionicframework.com/docs/v2/api/components/nav/NavParams). 20 | 21 | Don't forget that any component you create has to be imported in the app's module: 22 | 23 | {{{diff_step 6.4}}} 24 | 25 | Now we can complete our `ChatsPage`'s navigation method by importing the `MessagesPage`: 26 | 27 | {{{diff_step 6.5}}} 28 | 29 | We're missing some important details in the messages page. We don't know who we're chatting with, we don't know how does he look like, and we don't know which message is ours, and which is not. We can add these using the following code snippet: 30 | 31 | {{{diff_step 6.6}}} 32 | 33 | Since now we're not really able to determine the author of a message, we mark every even message as ours; But later on once we have an authentication system and users, we will be filling the missing gap. 34 | 35 | We will also have to update the message model to have an `ownership` property: 36 | 37 | {{{diff_step 6.7}}} 38 | 39 | Now that we have a basic component, let's implement a messages view as well: 40 | 41 | {{{diff_step 6.8}}} 42 | 43 | The template consists of a picture and a title inside the navigation bar. It also has two buttons. The purpose of the first button from the left would be sending attachments, and the second one should show an options pop-over, just like in the chats page. As for the content, we simply used a list of messages to show all available messages in the selected chat. To complete the view, let's write its belonging stylesheet: 44 | 45 | {{{diff_step 6.9}}} 46 | 47 | This style requires us to add some assets. So inside the `public/assets` dir, download the following: 48 | 49 | public/assets$ wget https://github.com/Urigo/Ionic2CLI-Meteor-WhatsApp/raw/master/public/assets/chat-background.jpg 50 | public/assets$ wget https://github.com/Urigo/Ionic2CLI-Meteor-WhatsApp/raw/master/public/assets/message-mine.png 51 | public/assets$ wget https://github.com/Urigo/Ionic2CLI-Meteor-WhatsApp/raw/master/public/assets/message-other.png 52 | 53 | Now we need to take care of the message's timestamp and format it, then again we gonna use `angular2-moment` only this time we gonna use a different format using the `amDateFormat` pipe: 54 | 55 | {{{diff_step 6.11}}} 56 | 57 | Our messages are set, but there is one really important feature missing: sending messages. Let's implement our message editor. We will start with the view itself. We will add an input for editing our messages, a `send` button, and a `record` button whose logic won't be implemented in this tutorial since we only wanna focus on the text messaging system. To fulfill this layout we gonna use a tool-bar (`ion-toolbar`) inside a footer (`ion-footer`) and place it underneath the content of the view: 58 | 59 | {{{diff_step 6.12}}} 60 | 61 | Our stylesheet requires few adjustments as well: 62 | 63 | {{{diff_step 6.13}}} 64 | 65 | Now we can implement the handler for messages sending in the component: 66 | 67 | {{{diff_step 6.14}}} 68 | 69 | As you can see, we've used a `Meteor` method called `sendTextMessage`, which is yet to exist. This method will add messages to our messages collection and run on both client's local cache and server. Now we're going to create a `server/methods.ts` file in our server and implement the method's logic: 70 | 71 | {{{diff_step 6.15}}} 72 | 73 | We would also like to validate some data sent to methods we define. For this we're gonna use a utility package provided to us by `Meteor` and it's called `check`. 74 | 75 | It requires us to add the following package in the server: 76 | 77 | api$ meteor add check 78 | 79 | And we're gonna use it in the `addMessage` method we've just defined: 80 | 81 | {{{diff_step 6.15}}} 82 | 83 | ## Auto Scroll 84 | 85 | In addition, we would like the view to auto-scroll down whenever a new message is added. We can achieve that using a native class called [MutationObserver](https://developer.mozilla.org/en/docs/Web/API/MutationObserver), which can detect changes in the view: 86 | 87 | {{{diff_step 6.18}}} 88 | 89 | So why didn't we update the scrolling position on a `Meteor` computation? - Because we want to initiate the scrolling function once the **view** is ready, not the **data**. They might look similar, but the difference is crucial. 90 | 91 | ## Group Messages 92 | 93 | Our next goal would be grouping our messages on the view according to the day they were sent, with an exception of the current date. So let's say we're in January 2nd 2017; Messages from yesterday will appear above the label `January 1 2017`. 94 | 95 | We can group our messages right after being fetched by the `Observable` using the `map` function: 96 | 97 | {{{diff_step 6.19}}} 98 | 99 | And now we will add a nested iteration in the messages view; The outer loop would iterate through the messages day-groups, and the inner loop would iterate through the messages themselves: 100 | 101 | {{{diff_step 6.20}}} 102 | 103 | {{{nav_step prev_step="https://angular-meteor.com/tutorials/whatsapp2/meteor/folder-structure" next_step="https://angular-meteor.com/tutorials/whatsapp2/meteor/authentication"}}} 104 | -------------------------------------------------------------------------------- /manuals/views/step13.md: -------------------------------------------------------------------------------- 1 | # Step 13: Native Mobile 2 | 3 | In this step, we will be implementing additional native features, to enhance the user experience. 4 | 5 | ## Automatic phone number detection 6 | 7 | `Ionic 2` is provided by default with a `Cordova` plug-in called `cordova-plugin-sim`, which allows us to retrieve some data from the current device's SIM card, if even exists. We will use the SIM card to automatically detect the current device's phone number, so this way the user won't need to manually fill-in his phone number whenever he tries to login. We will start by adding the appropriate handler in the `PhoneService`: 8 | 9 | [{]: (diff_step 13.1) 10 | #### Step 13.1: Implement getNumber with native ionic 11 | 12 | ##### Changed client/imports/services/phone.ts 13 | ```diff 14 | @@ -1,5 +1,6 @@ 15 | ┊1┊1┊import { Injectable } from '@angular/core'; 16 | ┊2┊2┊import { Platform } from 'ionic-angular'; 17 | +┊ ┊3┊import { Sim } from 'ionic-native'; 18 | ┊3┊4┊import { Accounts } from 'meteor/accounts-base'; 19 | ┊4┊5┊import { Meteor } from 'meteor/meteor'; 20 | ┊5┊6┊ 21 | ``` 22 | ```diff 23 | @@ -9,6 +10,17 @@ 24 | ┊ 9┊10┊ 25 | ┊10┊11┊ } 26 | ┊11┊12┊ 27 | +┊ ┊13┊ getNumber(): Promise { 28 | +┊ ┊14┊ if (!this.platform.is('cordova') || 29 | +┊ ┊15┊ !this.platform.is('mobile')) { 30 | +┊ ┊16┊ return Promise.resolve(''); 31 | +┊ ┊17┊ } 32 | +┊ ┊18┊ 33 | +┊ ┊19┊ return Sim.getSimInfo().then((info) => { 34 | +┊ ┊20┊ return '+' + info.phoneNumber; 35 | +┊ ┊21┊ }); 36 | +┊ ┊22┊ } 37 | +┊ ┊23┊ 38 | ┊12┊24┊ verify(phoneNumber: string): Promise { 39 | ┊13┊25┊ return new Promise((resolve, reject) => { 40 | ┊14┊26┊ Accounts.requestPhoneVerification(phoneNumber, (e: Error) => { 41 | ``` 42 | [}]: # 43 | 44 | And we will use it inside the `LoginPage`: 45 | 46 | [{]: (diff_step 13.2) 47 | #### Step 13.2: Use getNumber native method 48 | 49 | ##### Changed client/imports/pages/login/login.ts 50 | ```diff 51 | @@ -1,4 +1,4 @@ 52 | -┊1┊ ┊import { Component } from '@angular/core'; 53 | +┊ ┊1┊import { Component, AfterContentInit } from '@angular/core'; 54 | ┊2┊2┊import { Alert, AlertController, NavController } from 'ionic-angular'; 55 | ┊3┊3┊import { PhoneService } from '../../services/phone'; 56 | ┊4┊4┊import { VerificationPage } from '../verification/verification'; 57 | ``` 58 | ```diff 59 | @@ -7,7 +7,7 @@ 60 | ┊ 7┊ 7┊@Component({ 61 | ┊ 8┊ 8┊ template 62 | ┊ 9┊ 9┊}) 63 | -┊10┊ ┊export class LoginPage { 64 | +┊ ┊10┊export class LoginPage implements AfterContentInit { 65 | ┊11┊11┊ phone = ''; 66 | ┊12┊12┊ 67 | ┊13┊13┊ constructor( 68 | ``` 69 | ```diff 70 | @@ -16,6 +16,14 @@ 71 | ┊16┊16┊ private navCtrl: NavController 72 | ┊17┊17┊ ) {} 73 | ┊18┊18┊ 74 | +┊ ┊19┊ ngAfterContentInit() { 75 | +┊ ┊20┊ this.phoneService.getNumber().then((phone) => { 76 | +┊ ┊21┊ if (phone) { 77 | +┊ ┊22┊ this.login(phone); 78 | +┊ ┊23┊ } 79 | +┊ ┊24┊ }); 80 | +┊ ┊25┊ } 81 | +┊ ┊26┊ 82 | ┊19┊27┊ onInputKeypress({keyCode}: KeyboardEvent): void { 83 | ┊20┊28┊ if (keyCode === 13) { 84 | ┊21┊29┊ this.login(); 85 | ``` 86 | [}]: # 87 | 88 | In-order for it to work, be sure to install the following `Cordova` plug-in: 89 | 90 | $ meteor add cordova:cordova-plugin-sim@1.2.1 91 | 92 | ## Camera 93 | 94 | Next - we will grant access to the device's camera so we can send pictures which are yet to exist in the gallery. 95 | 96 | We will start by adding the appropriate `Cordova` plug-in: 97 | 98 | $ meteor add cordova:cordova-plugin-camera@2.3.1 99 | 100 | We will bind the `click` event in the `MessagesAttachmentsComponent`: 101 | 102 | [{]: (diff_step 13.5) 103 | #### Step 13.5: Added click event for takePicture 104 | 105 | ##### Changed client/imports/pages/messages/messages-attachments.html 106 | ```diff 107 | @@ -5,7 +5,7 @@ 108 | ┊ 5┊ 5┊
Gallery
109 | ┊ 6┊ 6┊ 110 | ┊ 7┊ 7┊ 111 | -┊ 8┊ ┊ 116 | ``` 117 | [}]: # 118 | 119 | And we will use the recently installed `Cordova` plug-in in the event handler to take some pictures: 120 | 121 | [{]: (diff_step 13.6) 122 | #### Step 13.6: Implement takePicture 123 | 124 | ##### Changed client/imports/pages/messages/messages-attachments.ts 125 | ```diff 126 | @@ -1,5 +1,6 @@ 127 | ┊1┊1┊import { Component } from '@angular/core'; 128 | ┊2┊2┊import { AlertController, Platform, ModalController, ViewController } from 'ionic-angular'; 129 | +┊ ┊3┊import { Camera } from 'ionic-native'; 130 | ┊3┊4┊import { PictureService } from '../../services/picture'; 131 | ┊4┊5┊import { MessageType } from '../../../../imports/models'; 132 | ┊5┊6┊import { NewLocationMessageComponent } from './location-message'; 133 | ``` 134 | ```diff 135 | @@ -26,6 +27,21 @@ 136 | ┊26┊27┊ }); 137 | ┊27┊28┊ } 138 | ┊28┊29┊ 139 | +┊ ┊30┊ takePicture(): void { 140 | +┊ ┊31┊ if (!this.platform.is('cordova')) { 141 | +┊ ┊32┊ return console.warn('Device must run cordova in order to take pictures'); 142 | +┊ ┊33┊ } 143 | +┊ ┊34┊ 144 | +┊ ┊35┊ Camera.getPicture().then((dataURI) => { 145 | +┊ ┊36┊ const blob = this.pictureService.convertDataURIToBlob(dataURI); 146 | +┊ ┊37┊ 147 | +┊ ┊38┊ this.viewCtrl.dismiss({ 148 | +┊ ┊39┊ messageType: MessageType.PICTURE, 149 | +┊ ┊40┊ selectedPicture: blob 150 | +┊ ┊41┊ }); 151 | +┊ ┊42┊ }); 152 | +┊ ┊43┊ } 153 | +┊ ┊44┊ 154 | ┊29┊45┊ sendLocation(): void { 155 | ┊30┊46┊ const locationModal = this.modelCtrl.create(NewLocationMessageComponent); 156 | ┊31┊47┊ locationModal.onDidDismiss((location) => { 157 | ``` 158 | [}]: # 159 | 160 | Note that take pictures are retrieved as relative paths in the device, but we use some existing methods in the `PictureService` to convert these paths into the desired format. 161 | 162 | [{]: (nav_step next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/summary" prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/file-upload") 163 | | [< Previous Step](https://angular-meteor.com/tutorials/whatsapp2/meteor/file-upload) | [Next Step >](https://angular-meteor.com/tutorials/whatsapp2/meteor/summary) | 164 | |:--------------------------------|--------------------------------:| 165 | [}]: # 166 | 167 | -------------------------------------------------------------------------------- /manuals/templates/step12.md.tmpl: -------------------------------------------------------------------------------- 1 | In this step, we will be using `Ionic 2` to pick up some images from our device's gallery, and we will use them to send pictures, and to set our profile picture. 2 | 3 | ## Image Picker 4 | 5 | First, we will a `Cordova` plug-in which will give us the ability to access the gallery: 6 | 7 | $ meteor add cordova:cordova-plugin-image-picker@1.1.3 8 | 9 | ## Meteor FS 10 | 11 | Up next, would be adding the ability to store some files in our data-base. This requires us to add 2 `Meteor` packages, called `ufs` and `ufs-gridfs` (Which adds support for `GridFS` operations. See [reference](https://docs.mongodb.com/manual/core/gridfs/)), which will take care of FS operations: 12 | 13 | $ meteor add jalik:ufs 14 | $ meteor add jalik:ufs-gridfs 15 | 16 | ## Client Side 17 | 18 | Before we proceed to the server, we will add the ability to select and upload pictures in the client. All our picture-related operations will be defined in a single service called `PictureService`; The first bit of this service would be picture-selection. The `UploadFS` package already supports that feature, **but only for the browser**, therefore we will be using the `Cordova` plug-in we've just installed to select some pictures from our mobile device: 19 | 20 | {{{diff_step 12.3}}} 21 | 22 | In order to use the service we will need to import it in the app's `NgModule` as a `provider`: 23 | 24 | {{{diff_step 12.4}}} 25 | 26 | Since now we will be sending pictures, we will need to update the message schema to support picture typed messages: 27 | 28 | {{{diff_step 12.5}}} 29 | 30 | In the attachments menu, we will add a new handler for sending pictures, called `sendPicture`: 31 | 32 | {{{diff_step 12.6}}} 33 | 34 | And we will bind that handler to the view, so whenever we press the right button, the handler will be invoked with the selected picture: 35 | 36 | {{{diff_step 12.7}}} 37 | 38 | Now we will be extending the `MessagesPage`, by adding a method which will send the picture selected in the attachments menu: 39 | 40 | {{{diff_step 12.8}}} 41 | 42 | For now, we will add a stub for the `upload` method in the `PictureService` and we will get back to it once we finish implementing the necessary logic in the server for storing a picture: 43 | 44 | {{{diff_step 12.9}}} 45 | 46 | ## Server Side 47 | 48 | So as we said, need to handle storage of pictures that were sent by the client. First, we will create a `Picture` model so the compiler can recognize a picture object: 49 | 50 | {{{diff_step 12.10}}} 51 | 52 | If you're familiar with `Whatsapp`, you'll know that sent pictures are compressed. That's so the data-base can store more pictures, and the traffic in the network will be faster. To compress the sent pictures, we will be using an `NPM` package called [sharp](https://www.npmjs.com/package/sharp), which is a utility library which will help us perform transformations on pictures: 53 | 54 | $ meteor npm install --save sharp 55 | 56 | Now we will create a picture store which will compress pictures using `sharp` right before they are inserted into the data-base: 57 | 58 | {{{diff_step 12.12}}} 59 | 60 | You can look at a store as some sort of a wrapper for a collection, which will run different kind of a operations before it mutates it or fetches data from it. Note that we used `GridFS` because this way an uploaded file is split into multiple packets, which is more efficient for storage. We also defined a small utility function on that store which will retrieve a profile picture. If the ID was not found, it will return a link for the default picture. To make things convenient, we will also export the store from the `index` file: 61 | 62 | {{{diff_step 12.13}}} 63 | 64 | Now that we have the pictures store, and the server knows how to handle uploaded pictures, we will implement the `upload` stub in the `PictureService`: 65 | 66 | {{{diff_step 12.14}}} 67 | 68 | ## View Picture Messages 69 | 70 | We will now add the support for picture typed messages in the `MessagesPage`, so whenever we send a picture, we will be able to see them in the messages list like any other message: 71 | 72 | {{{diff_step 12.15}}} 73 | 74 | As you can see, we also bound the picture message to the `click` event, which means that whenever we click on it, a picture viewer should be opened with the clicked picture. Let's create the component for that picture viewer: 75 | 76 | {{{diff_step 12.16}}} 77 | 78 | {{{diff_step 12.17}}} 79 | 80 | {{{diff_step 12.18}}} 81 | 82 | {{{diff_step 12.19}}} 83 | 84 | And now that we have that component ready, we will implement the `showPicture` method in the `MessagesPage` component, which will create a new instance of the `ShowPictureComponent`: 85 | 86 | {{{diff_step 12.20}}} 87 | 88 | ## Profile Picture 89 | 90 | We have the ability to send picture messages. Now we will add the ability to change the user's profile picture using the infrastructure we've just created. To begin with, we will define a new property to our `User` model called `pictureId`, which will be used to determine the belonging profile picture of the current user: 91 | 92 | {{{diff_step 12.21}}} 93 | 94 | We will bind the editing button in the profile selection page into an event handler: 95 | 96 | {{{diff_step 12.22}}} 97 | 98 | And we will add all the missing logic in the component, so the `pictureId` will be transformed into and actual reference, and so we can have the ability to select a picture from our gallery and upload it: 99 | 100 | {{{diff_step 12.23}}} 101 | 102 | We will also define a new hook in the `Meteor.users` collection so whenever we update the profile picture, the previous one will be removed from the data-base. This way we won't have some unnecessary data in our data-base, which will save us some precious storage: 103 | 104 | {{{diff_step 12.24}}} 105 | 106 | Collection hooks are not part of `Meteor`'s official API and are added through a third-party package called `matb33:collection-hooks`. This requires us to install the necessary type definition: 107 | 108 | $ npm install --save-dev @types/meteor-collection-hooks 109 | 110 | Now we need to import the type definition we've just installed in the `tsconfig.json` file: 111 | 112 | {{{diff_step 12.26}}} 113 | 114 | We now add a `user` publication which should be subscribed whenever we initialize the `ProfilePage`. This subscription should fetch some data from other collections which is related to the user which is currently logged in; And to be more specific, the document associated with the `profileId` defined in the `User` model: 115 | 116 | {{{diff_step 12.27}}} 117 | 118 | We will also modify the `users` and `chats` publication, so each user will contain its corresponding picture document as well: 119 | 120 | {{{diff_step 12.28}}} 121 | 122 | {{{diff_step 12.29}}} 123 | 124 | Since we already set up some collection hooks on the users collection, we can take it a step further by defining collection hooks on the chat collection, so whenever a chat is being removed, all its corresponding messages will be removed as well: 125 | 126 | {{{diff_step 12.30}}} 127 | 128 | We will now update the `updateProfile` method in the server to accept `pictureId`, so whenever we pick up a new profile picture the server won't reject it: 129 | 130 | {{{diff_step 12.31}}} 131 | 132 | Now we will update the users fabrication in our server's initialization, so instead of using hard-coded URLs, we will insert them as new documents to the `PicturesCollection`: 133 | 134 | {{{diff_step 12.32}}} 135 | 136 | To avoid some unexpected behaviors, we will reset our data-base so our server can re-fabricate the data: 137 | 138 | $ meteor reset 139 | 140 | We will now update the `ChatsPage` to add the belonging picture for each chat during transformation: 141 | 142 | {{{diff_step 12.33}}} 143 | 144 | And we will do the same in the `NewChatComponent`: 145 | 146 | {{{diff_step 12.34}}} 147 | 148 | {{{diff_step 12.35}}} 149 | 150 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/google-maps" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/native-mobile"}}} 151 | -------------------------------------------------------------------------------- /manuals/templates/step7.md.tmpl: -------------------------------------------------------------------------------- 1 | In this step we will authenticate and identify users in our app. 2 | 3 | Before we go ahead and start extending our app, we will add a few packages which will make our lives a bit less complex when it comes to authentication and users management. 4 | 5 | First we will update our `Meteor` project and add a couple of packages: `accounts-base` and `accounts-phone`. These will give us the ability to verify a user using an SMS code: 6 | 7 | $ meteor add accounts-base 8 | $ meteor add npm-bcrypt 9 | $ meteor add mys:accounts-phone 10 | 11 | For the sake of debugging we gonna write an authentication settings file (`private/settings.json`) which might make our life easier, but once you're in production mode you *shouldn't* use this configuration: 12 | 13 | {{{diff_step 7.2}}} 14 | 15 | Now anytime we run our app we should provide it with a `settings.json`: 16 | 17 | $ meteor run --settings private/settings.json 18 | 19 | To make it easier, we're gonna edit the `start` script defined in the `package.json` by appending the following: 20 | 21 | {{{diff_step 7.3}}} 22 | 23 | > *NOTE*: If you would like to test the verification with a real phone number, `accounts-phone` provides an easy access for [twilio's API](https://www.twilio.com/), for more information see [accounts-phone's repo](https://github.com/okland/accounts-phone). 24 | 25 | We will now apply the settings file we've just created so it can actually take effect: 26 | 27 | {{{diff_step 7.4}}} 28 | 29 | We also need to make sure we have the necessary declaration files for the package we've just added, so the compiler can recognize the new API: 30 | 31 | $ meteor npm install --save-dev @types/meteor-accounts-phone 32 | 33 | And we will reference from the `tsconfig.json` like so: 34 | 35 | {{{diff_step 7.6}}} 36 | 37 | ## Using Meteor's Accounts System 38 | 39 | Now, we will use the `Meteor`'s accounts system in the client. Our first use case would be delaying our app's bootstrap phase, until `Meteor`'s accounts system has done it's initialization. 40 | 41 | `Meteor`'s accounts API exposes a method called `loggingIn` which indicates if the authentication flow is done, which we gonna use before bootstraping our application, to make sure we provide the client with the necessary views which are right to his current state: 42 | 43 | {{{diff_step 7.7}}} 44 | 45 | To make things easier, we're going to organize all authentication related functions into a single service which we're gonna call `PhoneService`: 46 | 47 | {{{diff_step 7.8}}} 48 | 49 | And we gonna require it in the app's `NgModule` so it can be recognized: 50 | 51 | {{{diff_step 7.9}}} 52 | 53 | The `PhoneService` is not only packed with whatever functionality we need, but it also wraps async callbacks with promises, which has several advantages: 54 | 55 | - A promise is chainable, and provides an easy way to manage an async flow. 56 | - A promise is wrapped with `zone`, which means the view will be updated automatically once the callback has been invoked. 57 | - A promise can interface with an `Observable`. 58 | 59 | ## UI 60 | 61 | For authentication purposes, we gonna create the following flow in our app: 62 | 63 | - login - The initial page in the authentication flow where the user fills up his phone number. 64 | - verification - Verify a user's phone number by an SMS authentication. 65 | - profile - Ask a user to pickup its name. Afterwards he will be promoted to the tabs page. 66 | 67 | Let's start by creating the `LoginComponent`. In this component we will request an SMS verification right after a phone number has been entered: 68 | 69 | {{{diff_step 7.10}}} 70 | 71 | In short, once we press the login button, the `login` method is called and shows an alert dialog to confirm the action (See [reference](http://ionicframework.com/docs/v2/components/#alert)). If an error has occurred, the `handlerError` method is called and shows an alert dialog with the received error. If everything went as expected the `handleLogin` method is invoked, which will call the `login` method in the `PhoneService`. 72 | 73 | Hopefully that the component's logic is clear now, let's move to the template: 74 | 75 | {{{diff_step 7.11}}} 76 | 77 | And add some style into it: 78 | 79 | {{{diff_step 7.12}}} 80 | 81 | And as usual, newly created components should be imported in the app's module: 82 | 83 | {{{diff_step 7.13}}} 84 | 85 | We will also need to identify if the user is logged in or not once the app is launched; If so - the user will be promoted directly to the `ChatsPage`, and if not, he will have to go through the `LoginPage` first: 86 | 87 | {{{diff_step 7.14}}} 88 | 89 | Let's proceed and implement the verification page. We will start by creating its component, called `VerificationPage`. Logic is pretty much the same as in the `LoginComponent`: 90 | 91 | {{{diff_step 7.15}}} 92 | 93 | {{{diff_step 7.16}}} 94 | 95 | {{{diff_step 7.17}}} 96 | 97 | And add it to the `NgModule`: 98 | 99 | {{{diff_step 7.18}}} 100 | 101 | Now we can make sure that anytime we login, we will be promoted to the `VerificationPage` right after: 102 | 103 | {{{diff_step 7.19}}} 104 | 105 | The last step in our authentication pattern is setting our profile. We will create a `Profile` interface so the compiler can recognize profile-data structures: 106 | 107 | {{{diff_step 7.20}}} 108 | 109 | As you can probably notice we also defined a constant for the default profile picture. We will need to make this resource available for use before proceeding. The referenced `svg` file can be copied directly from the `ionicons` NodeJS module using the following command: 110 | 111 | public/assets$ cp ../../node_modules/ionicons/dist/svg/ios-contact.svg default-profile-pic.svg 112 | 113 | Now we can safely proceed to implementing the `ProfileComponent`: 114 | 115 | {{{diff_step 7.22}}} 116 | 117 | {{{diff_step 7.23}}} 118 | 119 | {{{diff_step 7.24}}} 120 | 121 | Let's redirect users who passed the verification stage to the newly created `ProfileComponent` like so: 122 | 123 | {{{diff_step 7.25}}} 124 | 125 | We will also need to import the `ProfileComponent` in the app's `NgModule` so it can be recognized: 126 | 127 | {{{diff_step 7.26}}} 128 | 129 | The core logic behind this component actually lies within the invocation of the `updateProfile`, a Meteor method implemented in our API which looks like so: 130 | 131 | {{{diff_step 7.27}}} 132 | 133 | ## Adjusting the Messaging System 134 | 135 | Now that our authentication flow is complete, we will need to edit the messages, so each user can be identified by each message sent. We will add a restriction in the `addMessage` method to see if a user is logged in, and we will bind its ID to the created message: 136 | 137 | {{{diff_step 7.28}}} 138 | 139 | This requires us to update the `Message` model as well so `TypeScript` will recognize the changes: 140 | 141 | {{{diff_step 7.29}}} 142 | 143 | Now we can determine if a message is ours or not in the `MessagePage` thanks to the `senderId` field we've just added: 144 | 145 | {{{diff_step 7.30}}} 146 | 147 | ## Chat Options Menu 148 | 149 | Now we're going to add the abilities to log-out and edit our profile as well, which are going to be presented to us using a popover. Let's show a [popover](http://ionicframework.com/docs/v2/components/#popovers) any time we press on the options icon in the top right corner of the chats view. 150 | 151 | A popover, just like a page in our app, consists of a component, view, and style: 152 | 153 | {{{diff_step 7.31}}} 154 | 155 | {{{diff_step 7.32}}} 156 | 157 | {{{diff_step 7.33}}} 158 | 159 | It requires us to import it in the `NgModule` as well: 160 | 161 | {{{diff_step 7.34}}} 162 | 163 | Now we will implement the method in the `ChatsPage` which will initialize the `ChatsOptionsComponent` using a popover controller: 164 | 165 | {{{diff_step 7.35}}} 166 | 167 | The method is invoked thanks to the following binding in the chats view: 168 | 169 | {{{diff_step 7.36}}} 170 | 171 | As for now, once you click on the options icon in the chats view, the popover should appear in the middle of the screen. To fix it, we gonna add the extend our app's main stylesheet, since it can be potentially used as a component not just in the `ChatsPage`, but also in other pages as well: 172 | 173 | {{{diff_step 7.37}}} 174 | 175 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/messages-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-mutations"}}} 176 | -------------------------------------------------------------------------------- /manuals/templates/step1.md.tmpl: -------------------------------------------------------------------------------- 1 | > If you got directly into here, please read the whole [intro section](https://angular-meteor.com/tutorials/whatsapp2-tutorial) explaining the goals for this tutorial and project. 2 | 3 | Both [Meteor](https://meteor.com) and [Ionic](https://ionicframework.com) took their platform to the next level in tooling. 4 | Both provide CLI interface instead of bringing bunch of dependencies and configure build tools. 5 | There are also differences between those tools. in this post we will focus on the `Meteor` CLI. 6 | 7 | > If you are interested in the [Ionic CLI](https://angular-meteor.com/tutorials/whatsapp2/ionic/setup), the steps needed to use it with Meteor are almost identical to the steps required by the Meteor CLI. 8 | 9 | Start by installing `Meteor` if you haven't already (See [reference](https://www.meteor.com/install)). 10 | 11 | Now let's create our app - write this in the command line: 12 | 13 | $ meteor create whatsapp 14 | 15 | And let's see what we've got. Go into the new folder: 16 | 17 | $ cd whatsapp 18 | 19 | A `Meteor` project will contain the following dirs by default: 20 | 21 | - client - A dir containing all client scripts. 22 | - server - A dir containing all server scripts. 23 | 24 | These scripts should be loaded automatically by their alphabetic order on their belonging platform, e.g. a script defined under the client dir should be loaded by `Meteor` only on the client. A script defined in neither of these folders should be loaded on both. 25 | 26 | `Meteor` apps use [Blaze](http://blazejs.org/) as its default template engine and [Babel](https://babeljs.io/) as its default script pre-processor. In this tutorial, we're interested in using [Angular 2](https://angular.io/)'s template engine and [Typescript](https://www.typescriptlang.org/) as our script pre-processor; Therefore will remove the `blaze-html-templates` package: 27 | 28 | $ meteor remove blaze-html-templates 29 | 30 | And we will replace it with a package called `angular2-compilers`: 31 | 32 | $ meteor add angular2-compilers 33 | 34 | The `angular2-compilers` package not only replace the `Blaze` template engine, but it also applies `Typescript` to our `Meteor` project, as it's the recommended scripting language recommended by the `Angular` team. In addition, all `CSS` files will be compiled with a pre-processor called [SASS](http://sass-lang.com/), something which will definitely ease the styling process. 35 | 36 | The `Typescript` compiler operates based on a user defined config, and without it, it's not going to behave expectedly; Therefore, we will define the following `Typescript` config file: 37 | 38 | {{{diff_step 1.3}}} 39 | 40 | > More information regards `Typescript` configuration file can be found [here](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). 41 | 42 | Not all third-party libraries have any support for `Typescript` whatsoever, something that we should be careful with when building a `Typescript` app. To allow non-supported third-party libraries, we will add the following declaration file: 43 | 44 | {{{diff_step 1.4}}} 45 | 46 | Like most libraries, `Angular 2` is relied on peer dependencies, which we will have to make sure exist in our app by installing the following packages: 47 | 48 | $ meteor npm install --save @angular/common 49 | $ meteor npm install --save @angular/compiler 50 | $ meteor npm install --save @angular/compiler-cli 51 | $ meteor npm install --save @angular/core 52 | $ meteor npm install --save @angular/forms 53 | $ meteor npm install --save @angular/http 54 | $ meteor npm install --save @angular/platform-browser 55 | $ meteor npm install --save @angular/platform-browser-dynamic 56 | $ meteor npm install --save @angular/platform-server 57 | $ meteor npm install --save meteor-rxjs 58 | $ meteor npm install --save reflect-metadata 59 | $ meteor npm install --save rxjs 60 | $ meteor npm install --save zone.js 61 | $ meteor npm install --save-dev @types/meteor 62 | $ meteor npm install --save-dev @types/meteor-accounts-phone 63 | $ meteor npm install --save-dev @types/underscore 64 | $ meteor npm install --save-dev meteor-typings 65 | 66 | Now that we have all the compilers and their dependencies ready, we will have to transform our project files into their supported extension. `.js` files should be renamed to `.ts` files, and `.css` files should be renamed to `.scss` files. 67 | 68 | $ mv server/main.js server/main.ts 69 | $ mv client/main.js client/main.ts 70 | $ mv client/main.css client/main.scss 71 | 72 | Last but not least, we will add some basic [Cordova](https://cordova.apache.org/) plugins which will take care of mobile specific features: 73 | 74 | $ meteor add cordova:cordova-plugin-whitelist@1.3.1 75 | $ meteor add cordova:cordova-plugin-console@1.0.5 76 | $ meteor add cordova:cordova-plugin-statusbar@2.2.1 77 | $ meteor add cordova:cordova-plugin-device@1.1.4 78 | $ meteor add cordova:ionic-plugin-keyboard@1.1.4 79 | $ meteor add cordova:cordova-plugin-splashscreen@4.0.1 80 | 81 | > The least above was determined based on Ionic 2's [app base](https://github.com/driftyco/ionic2-app-base). 82 | 83 | Everything is set, and we can start using the `Angular 2` framework. We will start by setting the basis for our app: 84 | 85 | {{{diff_step 1.8}}} 86 | 87 | In the step above, we simply created the entry module and component for our application, so as we go further in this tutorial and add more features, we can simply easily extend our module and app main component. 88 | 89 | ## Ionic 2 90 | 91 | [Ionic](http://ionicframework.com/docs/) is a free and open source mobile SDK for developing native and progressive web apps with ease. When using `Ionic`CLI, it comes with a boilerplate, just like `Meteor` does, but since we're not using `Meteor` CLI and not `Ionic`'s, we will have to set it up manually. 92 | 93 | The first thing we're going to do in order to integrate `Ionic 2` in our app would be installing its `NPM` dependencies: 94 | 95 | $ meteor npm install --save @ionic/storage 96 | $ meteor npm install --save ionic-angular 97 | $ meteor npm install --save ionic-native 98 | $ meteor npm install --save ionicons 99 | 100 | `Ionic` build system comes with a built-in theming system which helps its users design their app. It's a powerful tool which we wanna take advantage of. In-order to do that, we will define the following `SCSS` file, and we will import it: 101 | 102 | {{{diff_step 1.10}}} 103 | 104 | > The `variables.scss` file is hooked to different `Ionic` packages located in the `node_modules` dir. It means that the logic already existed, we only bound all the necessary modules together in order to emulate `Ionic`'s theming system. 105 | 106 | `Ionic` looks for fonts in directory we can't access. To fix it, we will use the `mys:font` package to teach `Meteor` how to put them there. 107 | 108 | $ meteor add mys:fonts 109 | 110 | That plugin needs to know which font we want to use and where it should be available. 111 | 112 | Configuration is pretty easy, you will catch it by just looking on an example: 113 | 114 | {{{diff_step 1.12}}} 115 | 116 | `Ionic` is set. We will have to make few adjustments in our app in order to use `Ionic`, mostly importing its modules and using its components: 117 | 118 | {{{diff_step 1.13}}} 119 | 120 | ## Running on Mobile 121 | 122 | To add mobile support, select the platform(s) you want and run the following command: 123 | 124 | $ meteor add-platform ios 125 | # OR / AND 126 | $ meteor add-platform android 127 | 128 | To run an app in the emulator use: 129 | 130 | $ meteor run ios 131 | # OR 132 | $ meteor run android 133 | 134 | To learn more about **Mobile** in `Meteor` read the [*"Mobile"* chapter](https://guide.meteor.com/mobile.html) of the Meteor Guide. 135 | 136 | `Meteor` projects come with a package called `mobile-experience` by default, which is a bundle of `fastclick`, `mobile-status-bar` and `launch-screen`. The `fastclick` package might cause some conflicts between `Meteor` and `Ionic`'s functionality, something which will probably cause some unexpected behavior. To fix it, we're going to remove `mobile-experience` and install the rest of its packages explicitly: 137 | 138 | $ meteor remove mobile-experience 139 | $ meteor add mobile-status-bar 140 | $ meteor add launch-screen 141 | 142 | ### Web 143 | 144 | `Ionic` apps are still usable in the browser. You can run them using the command: 145 | 146 | $ meteor start 147 | # OR 148 | $ npm start 149 | 150 | The app should be running on port `3000`, and can be changed by specifying a `--port` option. 151 | 152 | {{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2-tutorial" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-page"}}} 153 | -------------------------------------------------------------------------------- /client/imports/pages/messages/messages.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit, ElementRef } from '@angular/core'; 2 | import { NavParams, PopoverController, ModalController } from 'ionic-angular'; 3 | import { MeteorObservable } from 'meteor-rxjs'; 4 | import { _ } from 'meteor/underscore'; 5 | import * as Moment from 'moment'; 6 | import { Observable, Subscription, Subscriber } from 'rxjs'; 7 | import { Messages } from '../../../../imports/collections'; 8 | import { Chat, Message, MessageType, Location } from '../../../../imports/models'; 9 | import { PictureService } from '../../services/picture'; 10 | import { MessagesAttachmentsComponent } from './messages-attachments'; 11 | import { MessagesOptionsComponent } from './messages-options'; 12 | import { ShowPictureComponent } from './show-picture'; 13 | import template from './messages.html'; 14 | 15 | @Component({ 16 | template 17 | }) 18 | export class MessagesPage implements OnInit, OnDestroy { 19 | selectedChat: Chat; 20 | title: string; 21 | picture: string; 22 | messagesDayGroups; 23 | message: string = ''; 24 | autoScroller: MutationObserver; 25 | scrollOffset = 0; 26 | senderId: string; 27 | loadingMessages: boolean; 28 | messagesComputation: Subscription; 29 | messagesBatchCounter: number = 0; 30 | 31 | constructor( 32 | navParams: NavParams, 33 | private el: ElementRef, 34 | private popoverCtrl: PopoverController, 35 | private pictureService: PictureService, 36 | private modalCtrl: ModalController 37 | ) { 38 | this.selectedChat = navParams.get('chat'); 39 | this.title = this.selectedChat.title; 40 | this.picture = this.selectedChat.picture; 41 | this.senderId = Meteor.userId(); 42 | } 43 | 44 | private get messagesPageContent(): Element { 45 | return this.el.nativeElement.querySelector('.messages-page-content'); 46 | } 47 | 48 | private get messagesList(): Element { 49 | return this.messagesPageContent.querySelector('.messages'); 50 | } 51 | 52 | private get scroller(): Element { 53 | return this.messagesList.querySelector('.scroll-content'); 54 | } 55 | 56 | ngOnInit() { 57 | this.autoScroller = this.autoScroll(); 58 | this.subscribeMessages(); 59 | 60 | // Get total messages count in database so we can have an indication of when to 61 | // stop the auto-subscriber 62 | MeteorObservable.call('countMessages').subscribe((messagesCount: number) => { 63 | Observable 64 | // Chain every scroll event 65 | .fromEvent(this.scroller, 'scroll') 66 | // Remove the scroll listener once all messages have been fetched 67 | .takeUntil(this.autoRemoveScrollListener(messagesCount)) 68 | // Filter event handling unless we're at the top of the page 69 | .filter(() => !this.scroller.scrollTop) 70 | // Prohibit parallel subscriptions 71 | .filter(() => !this.loadingMessages) 72 | // Invoke the messages subscription once all the requirements have been met 73 | .forEach(() => this.subscribeMessages()); 74 | }); 75 | } 76 | 77 | ngOnDestroy() { 78 | this.autoScroller.disconnect(); 79 | } 80 | 81 | // Subscribes to the relevant set of messages 82 | subscribeMessages(): void { 83 | // A flag which indicates if there's a subscription in process 84 | this.loadingMessages = true; 85 | // A custom offset to be used to re-adjust the scrolling position once 86 | // new dataset is fetched 87 | this.scrollOffset = this.scroller.scrollHeight; 88 | 89 | MeteorObservable.subscribe('messages', 90 | this.selectedChat._id, 91 | ++this.messagesBatchCounter 92 | ).subscribe(() => { 93 | // Keep tracking changes in the dataset and re-render the view 94 | if (!this.messagesComputation) { 95 | this.messagesComputation = this.autorunMessages(); 96 | } 97 | 98 | // Allow incoming subscription requests 99 | this.loadingMessages = false; 100 | }); 101 | } 102 | 103 | // Detects changes in the messages dataset and re-renders the view 104 | autorunMessages(): Subscription { 105 | return MeteorObservable.autorun().subscribe(() => { 106 | this.messagesDayGroups = this.findMessagesDayGroups(); 107 | }); 108 | } 109 | 110 | // Removes the scroll listener once all messages from the past were fetched 111 | autoRemoveScrollListener(messagesCount: number): Observable { 112 | return Observable.create((observer: Subscriber) => { 113 | Messages.find().subscribe({ 114 | next: (messages) => { 115 | // Once all messages have been fetched 116 | if (messagesCount !== messages.length) { 117 | return; 118 | } 119 | 120 | // Signal to stop listening to the scroll event 121 | observer.next(); 122 | 123 | // Finish the observation to prevent unnecessary calculations 124 | observer.complete(); 125 | }, 126 | error: (e) => { 127 | observer.error(e); 128 | } 129 | }); 130 | }); 131 | } 132 | 133 | showOptions(): void { 134 | const popover = this.popoverCtrl.create(MessagesOptionsComponent, { 135 | chat: this.selectedChat 136 | }, { 137 | cssClass: 'options-popover messages-options-popover' 138 | }); 139 | 140 | popover.present(); 141 | } 142 | 143 | findMessagesDayGroups() { 144 | return Messages.find({ 145 | chatId: this.selectedChat._id 146 | }, { 147 | sort: { createdAt: 1 } 148 | }) 149 | .map((messages: Message[]) => { 150 | const format = 'D MMMM Y'; 151 | 152 | // Compose missing data that we would like to show in the view 153 | messages.forEach((message) => { 154 | message.ownership = this.senderId == message.senderId ? 'mine' : 'other'; 155 | 156 | return message; 157 | }); 158 | 159 | // Group by creation day 160 | const groupedMessages = _.groupBy(messages, (message) => { 161 | return Moment(message.createdAt).format(format); 162 | }); 163 | 164 | // Transform dictionary into an array since Angular's view engine doesn't know how 165 | // to iterate through it 166 | return Object.keys(groupedMessages).map((timestamp: string) => { 167 | return { 168 | timestamp: timestamp, 169 | messages: groupedMessages[timestamp], 170 | today: Moment().format(format) === timestamp 171 | }; 172 | }); 173 | }); 174 | } 175 | 176 | autoScroll(): MutationObserver { 177 | const autoScroller = new MutationObserver(this.scrollDown.bind(this)); 178 | 179 | autoScroller.observe(this.messagesList, { 180 | childList: true, 181 | subtree: true 182 | }); 183 | 184 | return autoScroller; 185 | } 186 | 187 | scrollDown(): void { 188 | // Don't scroll down if messages subscription is being loaded 189 | if (this.loadingMessages) { 190 | return; 191 | } 192 | 193 | // Scroll down and apply specified offset 194 | this.scroller.scrollTop = this.scroller.scrollHeight - this.scrollOffset; 195 | // Zero offset for next invocation 196 | this.scrollOffset = 0; 197 | } 198 | 199 | onInputKeypress({ keyCode }: KeyboardEvent): void { 200 | if (keyCode === 13) { 201 | this.sendTextMessage(); 202 | } 203 | } 204 | 205 | sendTextMessage(): void { 206 | // If message was yet to be typed, abort 207 | if (!this.message) { 208 | return; 209 | } 210 | 211 | MeteorObservable.call('addMessage', MessageType.TEXT, 212 | this.selectedChat._id, 213 | this.message 214 | ).zone().subscribe(() => { 215 | // Zero the input field 216 | this.message = ''; 217 | }); 218 | } 219 | 220 | sendLocationMessage(location: Location): void { 221 | MeteorObservable.call('addMessage', MessageType.LOCATION, 222 | this.selectedChat._id, 223 | `${location.lat},${location.lng},${location.zoom}` 224 | ).zone().subscribe(() => { 225 | // Zero the input field 226 | this.message = ''; 227 | }); 228 | } 229 | 230 | showAttachments(): void { 231 | const popover = this.popoverCtrl.create(MessagesAttachmentsComponent, { 232 | chat: this.selectedChat 233 | }, { 234 | cssClass: 'attachments-popover' 235 | }); 236 | 237 | popover.onDidDismiss((params) => { 238 | if (params) { 239 | if (params.messageType === MessageType.LOCATION) { 240 | const location = params.selectedLocation; 241 | this.sendLocationMessage(location); 242 | } 243 | else if (params.messageType === MessageType.PICTURE) { 244 | const blob: Blob = params.selectedPicture; 245 | this.sendPictureMessage(blob); 246 | } 247 | } 248 | }); 249 | 250 | popover.present(); 251 | } 252 | 253 | sendPictureMessage(blob: Blob): void { 254 | this.pictureService.upload(blob).then((picture) => { 255 | MeteorObservable.call('addMessage', MessageType.PICTURE, 256 | this.selectedChat._id, 257 | picture.url 258 | ).zone().subscribe(); 259 | }); 260 | } 261 | 262 | getLocation(locationString: string): Location { 263 | const splitted = locationString.split(',').map(Number); 264 | 265 | return { 266 | lat: splitted[0], 267 | lng: splitted[1], 268 | zoom: Math.min(splitted[2] || 0, 19) 269 | }; 270 | } 271 | 272 | showPicture({ target }: Event) { 273 | const modal = this.modalCtrl.create(ShowPictureComponent, { 274 | pictureSrc: (target).src 275 | }); 276 | 277 | modal.present(); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /client/imports/theme/components.scss: -------------------------------------------------------------------------------- 1 | // Components 2 | // -------------------------------------------------- 3 | 4 | @import 5 | "{}/node_modules/ionic-angular/components/action-sheet/action-sheet", 6 | "{}/node_modules/ionic-angular/components/action-sheet/action-sheet.ios", 7 | "{}/node_modules/ionic-angular/components/action-sheet/action-sheet.md", 8 | "{}/node_modules/ionic-angular/components/action-sheet/action-sheet.wp"; 9 | 10 | @import 11 | "{}/node_modules/ionic-angular/components/alert/alert", 12 | "{}/node_modules/ionic-angular/components/alert/alert.ios", 13 | "{}/node_modules/ionic-angular/components/alert/alert.md", 14 | "{}/node_modules/ionic-angular/components/alert/alert.wp"; 15 | 16 | @import 17 | "{}/node_modules/ionic-angular/components/app/app", 18 | "{}/node_modules/ionic-angular/components/app/app.ios", 19 | "{}/node_modules/ionic-angular/components/app/app.md", 20 | "{}/node_modules/ionic-angular/components/app/app.wp"; 21 | 22 | @import 23 | "{}/node_modules/ionic-angular/components/backdrop/backdrop"; 24 | 25 | @import 26 | "{}/node_modules/ionic-angular/components/badge/badge", 27 | "{}/node_modules/ionic-angular/components/badge/badge.ios", 28 | "{}/node_modules/ionic-angular/components/badge/badge.md", 29 | "{}/node_modules/ionic-angular/components/badge/badge.wp"; 30 | 31 | @import 32 | "{}/node_modules/ionic-angular/components/button/button", 33 | "{}/node_modules/ionic-angular/components/button/button-icon", 34 | "{}/node_modules/ionic-angular/components/button/button.ios", 35 | "{}/node_modules/ionic-angular/components/button/button.md", 36 | "{}/node_modules/ionic-angular/components/button/button.wp"; 37 | 38 | @import 39 | "{}/node_modules/ionic-angular/components/card/card", 40 | "{}/node_modules/ionic-angular/components/card/card.ios", 41 | "{}/node_modules/ionic-angular/components/card/card.md", 42 | "{}/node_modules/ionic-angular/components/card/card.wp"; 43 | 44 | @import 45 | "{}/node_modules/ionic-angular/components/checkbox/checkbox.ios", 46 | "{}/node_modules/ionic-angular/components/checkbox/checkbox.md", 47 | "{}/node_modules/ionic-angular/components/checkbox/checkbox.wp"; 48 | 49 | @import 50 | "{}/node_modules/ionic-angular/components/chip/chip", 51 | "{}/node_modules/ionic-angular/components/chip/chip.ios", 52 | "{}/node_modules/ionic-angular/components/chip/chip.md", 53 | "{}/node_modules/ionic-angular/components/chip/chip.wp"; 54 | 55 | @import 56 | "{}/node_modules/ionic-angular/components/content/content", 57 | "{}/node_modules/ionic-angular/components/content/content.ios", 58 | "{}/node_modules/ionic-angular/components/content/content.md", 59 | "{}/node_modules/ionic-angular/components/content/content.wp"; 60 | 61 | @import 62 | "{}/node_modules/ionic-angular/components/datetime/datetime", 63 | "{}/node_modules/ionic-angular/components/datetime/datetime.ios", 64 | "{}/node_modules/ionic-angular/components/datetime/datetime.md", 65 | "{}/node_modules/ionic-angular/components/datetime/datetime.wp"; 66 | 67 | @import 68 | "{}/node_modules/ionic-angular/components/fab/fab", 69 | "{}/node_modules/ionic-angular/components/fab/fab.ios", 70 | "{}/node_modules/ionic-angular/components/fab/fab.md", 71 | "{}/node_modules/ionic-angular/components/fab/fab.wp"; 72 | 73 | @import 74 | "{}/node_modules/ionic-angular/components/grid/grid"; 75 | 76 | @import 77 | "{}/node_modules/ionic-angular/components/icon/icon", 78 | "{}/node_modules/ionic-angular/components/icon/icon.ios", 79 | "{}/node_modules/ionic-angular/components/icon/icon.md", 80 | "{}/node_modules/ionic-angular/components/icon/icon.wp"; 81 | 82 | @import 83 | "{}/node_modules/ionic-angular/components/img/img"; 84 | 85 | @import 86 | "{}/node_modules/ionic-angular/components/infinite-scroll/infinite-scroll"; 87 | 88 | @import 89 | "{}/node_modules/ionic-angular/components/input/input", 90 | "{}/node_modules/ionic-angular/components/input/input.ios", 91 | "{}/node_modules/ionic-angular/components/input/input.md", 92 | "{}/node_modules/ionic-angular/components/input/input.wp"; 93 | 94 | @import 95 | "{}/node_modules/ionic-angular/components/item/item", 96 | "{}/node_modules/ionic-angular/components/item/item-media", 97 | "{}/node_modules/ionic-angular/components/item/item-reorder", 98 | "{}/node_modules/ionic-angular/components/item/item-sliding", 99 | "{}/node_modules/ionic-angular/components/item/item.ios", 100 | "{}/node_modules/ionic-angular/components/item/item.md", 101 | "{}/node_modules/ionic-angular/components/item/item.wp"; 102 | 103 | @import 104 | "{}/node_modules/ionic-angular/components/label/label", 105 | "{}/node_modules/ionic-angular/components/label/label.ios", 106 | "{}/node_modules/ionic-angular/components/label/label.md", 107 | "{}/node_modules/ionic-angular/components/label/label.wp"; 108 | 109 | @import 110 | "{}/node_modules/ionic-angular/components/list/list", 111 | "{}/node_modules/ionic-angular/components/list/list.ios", 112 | "{}/node_modules/ionic-angular/components/list/list.md", 113 | "{}/node_modules/ionic-angular/components/list/list.wp"; 114 | 115 | @import 116 | "{}/node_modules/ionic-angular/components/loading/loading", 117 | "{}/node_modules/ionic-angular/components/loading/loading.ios", 118 | "{}/node_modules/ionic-angular/components/loading/loading.md", 119 | "{}/node_modules/ionic-angular/components/loading/loading.wp"; 120 | 121 | @import 122 | "{}/node_modules/ionic-angular/components/menu/menu", 123 | "{}/node_modules/ionic-angular/components/menu/menu.ios", 124 | "{}/node_modules/ionic-angular/components/menu/menu.md", 125 | "{}/node_modules/ionic-angular/components/menu/menu.wp"; 126 | 127 | @import 128 | "{}/node_modules/ionic-angular/components/modal/modal", 129 | "{}/node_modules/ionic-angular/components/modal/modal.ios", 130 | "{}/node_modules/ionic-angular/components/modal/modal.md", 131 | "{}/node_modules/ionic-angular/components/modal/modal.wp"; 132 | 133 | @import 134 | "{}/node_modules/ionic-angular/components/note/note.ios", 135 | "{}/node_modules/ionic-angular/components/note/note.md", 136 | "{}/node_modules/ionic-angular/components/note/note.wp"; 137 | 138 | @import 139 | "{}/node_modules/ionic-angular/components/picker/picker", 140 | "{}/node_modules/ionic-angular/components/picker/picker.ios", 141 | "{}/node_modules/ionic-angular/components/picker/picker.md", 142 | "{}/node_modules/ionic-angular/components/picker/picker.wp"; 143 | 144 | @import 145 | "{}/node_modules/ionic-angular/components/popover/popover", 146 | "{}/node_modules/ionic-angular/components/popover/popover.ios", 147 | "{}/node_modules/ionic-angular/components/popover/popover.md", 148 | "{}/node_modules/ionic-angular/components/popover/popover.wp"; 149 | 150 | @import 151 | "{}/node_modules/ionic-angular/components/radio/radio.ios", 152 | "{}/node_modules/ionic-angular/components/radio/radio.md", 153 | "{}/node_modules/ionic-angular/components/radio/radio.wp"; 154 | 155 | @import 156 | "{}/node_modules/ionic-angular/components/range/range", 157 | "{}/node_modules/ionic-angular/components/range/range.ios", 158 | "{}/node_modules/ionic-angular/components/range/range.md", 159 | "{}/node_modules/ionic-angular/components/range/range.wp"; 160 | 161 | @import 162 | "{}/node_modules/ionic-angular/components/refresher/refresher"; 163 | 164 | @import 165 | "{}/node_modules/ionic-angular/components/scroll/scroll"; 166 | 167 | @import 168 | "{}/node_modules/ionic-angular/components/searchbar/searchbar", 169 | "{}/node_modules/ionic-angular/components/searchbar/searchbar.ios", 170 | "{}/node_modules/ionic-angular/components/searchbar/searchbar.md", 171 | "{}/node_modules/ionic-angular/components/searchbar/searchbar.wp"; 172 | 173 | @import 174 | "{}/node_modules/ionic-angular/components/segment/segment", 175 | "{}/node_modules/ionic-angular/components/segment/segment.ios", 176 | "{}/node_modules/ionic-angular/components/segment/segment.md", 177 | "{}/node_modules/ionic-angular/components/segment/segment.wp"; 178 | 179 | @import 180 | "{}/node_modules/ionic-angular/components/select/select", 181 | "{}/node_modules/ionic-angular/components/select/select.ios", 182 | "{}/node_modules/ionic-angular/components/select/select.md", 183 | "{}/node_modules/ionic-angular/components/select/select.wp"; 184 | 185 | @import 186 | "{}/node_modules/ionic-angular/components/show-hide-when/show-hide-when"; 187 | 188 | @import 189 | "{}/node_modules/ionic-angular/components/slides/slides"; 190 | 191 | @import 192 | "{}/node_modules/ionic-angular/components/spinner/spinner", 193 | "{}/node_modules/ionic-angular/components/spinner/spinner.ios", 194 | "{}/node_modules/ionic-angular/components/spinner/spinner.md", 195 | "{}/node_modules/ionic-angular/components/spinner/spinner.wp"; 196 | 197 | @import 198 | "{}/node_modules/ionic-angular/components/tabs/tabs", 199 | "{}/node_modules/ionic-angular/components/tabs/tabs.ios", 200 | "{}/node_modules/ionic-angular/components/tabs/tabs.md", 201 | "{}/node_modules/ionic-angular/components/tabs/tabs.wp"; 202 | 203 | @import 204 | "{}/node_modules/ionic-angular/components/toast/toast", 205 | "{}/node_modules/ionic-angular/components/toast/toast.ios", 206 | "{}/node_modules/ionic-angular/components/toast/toast.md", 207 | "{}/node_modules/ionic-angular/components/toast/toast.wp"; 208 | 209 | @import 210 | "{}/node_modules/ionic-angular/components/toggle/toggle.ios", 211 | "{}/node_modules/ionic-angular/components/toggle/toggle.md", 212 | "{}/node_modules/ionic-angular/components/toggle/toggle.wp"; 213 | 214 | @import 215 | "{}/node_modules/ionic-angular/components/toolbar/toolbar", 216 | "{}/node_modules/ionic-angular/components/toolbar/toolbar-button", 217 | "{}/node_modules/ionic-angular/components/toolbar/toolbar.ios", 218 | "{}/node_modules/ionic-angular/components/toolbar/toolbar.md", 219 | "{}/node_modules/ionic-angular/components/toolbar/toolbar.wp"; 220 | 221 | @import 222 | "{}/node_modules/ionic-angular/components/typography/typography", 223 | "{}/node_modules/ionic-angular/components/typography/typography.ios", 224 | "{}/node_modules/ionic-angular/components/typography/typography.md", 225 | "{}/node_modules/ionic-angular/components/typography/typography.wp"; 226 | 227 | @import 228 | "{}/node_modules/ionic-angular/components/virtual-scroll/virtual-scroll"; 229 | 230 | 231 | // Platforms 232 | // -------------------------------------------------- 233 | @import 234 | "{}/node_modules/ionic-angular/platform/cordova", 235 | "{}/node_modules/ionic-angular/platform/cordova.ios", 236 | "{}/node_modules/ionic-angular/platform/cordova.md", 237 | "{}/node_modules/ionic-angular/platform/cordova.wp"; 238 | 239 | 240 | // Fonts 241 | // -------------------------------------------------- 242 | @import 243 | "ionicons", 244 | "{}/node_modules/ionic-angular/fonts/noto-sans", 245 | "{}/node_modules/ionic-angular/fonts/roboto"; 246 | -------------------------------------------------------------------------------- /manuals/templates/step5.md.tmpl: -------------------------------------------------------------------------------- 1 | As you have probably noticed, our tutorial app has a strict modular structure at this point; There are no pure JavaScript files that are being bundled together and auto-executed, so Meteor's file loading conventions don't have any effect. Furthermore, every `.ts` file is being compiled into a separate CommonJS module, which we can then import whenever we desire. 2 | 3 | ## TypeScript 4 | 5 | TypeScript is a rather new language that has been growing in [popularity](https://www.google.com/trends/explore#q=%2Fm%2F0n50hxv) since it's creation 3 years ago. TypeScript has one of the fullest implementations of ES2015 features on the market: including some experimental features, pseudo type-checking and a rich toolset developed by Microsoft and the TypeScript community. It has support already in all major IDEs including Visual Studio, WebStorm, Sublime, Atom, etc. 6 | 7 | One of the biggest issues in JavaScript is making code less bug-prone and more suitable for big projects. In the OOP world, well-known solutions include modularity and strict type-checking. While OOP is available in JavaScript in some way, it turned out to be very hard to create good type-checking. One always needs to impose a certain number of rules to follow to make a JavaScript compiler more effective. For many years, we’ve seen around a number of solutions including the Closure Compiler and GWT from Google, a bunch of C#-to-JavaScript compilers and others. 8 | 9 | This was, for sure, one of the problems the TypeScript team were striving to solve: to create a language that would inherit the flexibility of JavaScript while, at the same time, having effective and optional type-checking with minimum effort required from the user. 10 | 11 | ### Interfaces 12 | 13 | TypeScript's type-checking is based on the "shapes" that types have. That's where interfaces kicks in; Interfaces are TypeScript's means to describe these type "shapes", which is sometimes called "duck typing". More on that you can read [here](http://www.typescriptlang.org/docs/handbook/interfaces.html). 14 | 15 | ### TypeScript Configuration and IDEs 16 | 17 | As you already know from the [bootstrapping step](./step1.md), TypeScript is generally configured by a special JSON file called [_tsconfig.json_](https://github.com/Microsoft/typescript/wiki/tsconfig.json). 18 | 19 | As mentioned, the TypeScript language today has development plugins in many [popular IDEs](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support), including Visual Studio, WebStorm, Sublime, Atom etc. These plugins work in the same style as it's become de facto today — compile, using TypeScript shell command, `.ts` and `tsconfig.json` files update automatically as you change them. 20 | With that, if you've configured your project right with declaration files in place you'll get a bunch of invaluable features such as better code completion and instantaneous highlighting of compilation errors. 21 | 22 | If you use one of the mentioned IDEs, you've likely noticed that a bunch of the code lines 23 | are now marked in red, indicating the TypeScript plugins don't work right quite yet. 24 | 25 | That's because most of the plugins recognize _tsconfig.json_ as well if it's placed in the root folder, 26 | but so far our _tsconfig.json_ contains only a "files" property, which is certainly not enough for 27 | a general compilation. At the same time, Angular2-Meteor's TypeScript compiler, defaults most of the 28 | compilation options internally to fit our needs. To fix plugins, let's set up our _tsconfig.json_ 29 | properly with the options that will make plugins understand our needs and the structure of our app. 30 | 31 | ```json 32 | { 33 | "compilerOptions": { 34 | "target": "es5", 35 | "module": "commonjs", 36 | "isolatedModules": false, 37 | "moduleResolution": "node", 38 | "experimentalDecorators": true, 39 | "emitDecoratorMetadata": true, 40 | "removeComments": false, 41 | "noImplicitAny": false, 42 | "sourceMap": true 43 | }, 44 | "exclude": [ 45 | "node_modules" 46 | ], 47 | "compileOnSave": false 48 | } 49 | ``` 50 | 51 | **CompilerOptions:** 52 | 53 | - `target` - Specify ECMAScript target version 54 | - `module` - Specify module code generation 55 | - `isolatedModules` - Unconditionally emit imports for unresolved files 56 | - `moduleResolution` - Determine how modules get resolved 57 | - `experimentalDecorators` - Enables experimental support for ES7 decorators. 58 | - `emitDecoratorMetadata` - Emit design-type metadata for decorated declarations in source 59 | - `removeComments` - Remove all comments except copy-right header comments beginning with 60 | - `noImplicitAny` - Raise error on expressions and declarations with an implied 'any' type 61 | - `sourceMap` - Generates corresponding '.map' file 62 | 63 | Now, let's go to any of the `.ts` files and check if all that annoying redness has disappeared. 64 | 65 | > Note: you may need to reload you IDE to pick up the latest changes to the config. 66 | 67 | Please note, since the Meteor environment is quite specific, some of the `tsconfig.json` options won't make sense in Meteor. You can read about the exceptions [here](https://github.com/barbatus/typescript#compiler-options). 68 | TypeScript compiler of this package supports some additional options that might be useful in the Meteor environment. 69 | They can be included in the "meteorCompilerOptions" section of _tsconfig.json_ and described [here](https://github.com/barbatus/ts-compilers#typescript-config). 70 | 71 | ## Meteor Folder Structure 72 | 73 | Even though it is recommended that you write your application to use ES2015 modules and the `imports/` directory, Meteor 1.3 continues to support eager loading of files, using these default load order rules, to provide backwards compatibility with applications written for Meteor 1.2 and earlier. You may combine both eager loading and lazy loading using `import` in a single app. Any import statements are evaluated in the order they are listed in a file when that file is loaded and evaluated using these rules. 74 | 75 | There are several load order rules. They are **applied sequentially** to all applicable files in the application, in the priority given below: 76 | 77 | - HTML template files are always loaded before everything else 78 | - Files beginning with main. are loaded last 79 | - Files inside any lib/ directory are loaded next 80 | - Files with deeper paths are loaded next 81 | - Files are then loaded in alphabetical order of the entire path 82 | 83 | ``` 84 | nav.html 85 | main.html 86 | client/lib/methods.js 87 | client/lib/styles.js 88 | lib/feature/styles.js 89 | lib/collections.js 90 | client/feature-y.js 91 | feature-x.js 92 | client/main.js 93 | ``` 94 | 95 | For example, the files above are arranged in the correct load order. `main.html` is loaded second because HTML templates are always loaded first, even if it begins with main., since rule 1 has priority over rule 2. However, it will be loaded after `nav.html` because rule 2 has priority over rule 5. 96 | 97 | `client/lib/styles.js` and `lib/feature/styles.js` have identical load order up to rule 4; however, since `client` comes before `lib` alphabetically, it will be loaded first. 98 | 99 | > You can also use [Meteor.startup](http://docs.meteor.com/#/full/meteor_startup) to control when run code is run on both the server and the client. 100 | 101 | By default, any JavaScript files in your Meteor application folder are bundled and loaded on both the client and the server. However, the names of the files and directories inside your project can affect their load order, where they are loaded, and some other characteristics. Here is a list of file and directory names that are treated specially by Meteor: 102 | 103 | - **imports** 104 | 105 | Any directory named `imports/` is not loaded anywhere and files must be imported using `import`. 106 | 107 | - **node_modules** 108 | 109 | Any directory named `node_modules/` is not loaded anywhere. node.js packages installed into `node_modules` directories must be imported using `import` or by using `Npm.depends` in `package.js`. 110 | 111 | - **client** 112 | 113 | Any directory named `client/` is not loaded on the server. Similar to wrapping your code in `if (Meteor.isClient) { ... }`. All files loaded on the client are automatically concatenated and minified when in production mode. In development mode, JavaScript and CSS files are not minified, to make debugging easier. CSS files are still combined into a single file for consistency between production and development, because changing the CSS file's URL affects how URLs in it are processed. 114 | 115 | > HTML files in a Meteor application are treated quite a bit differently from a server-side framework. Meteor scans all the HTML files in your directory for three top-level elements: ``, ``, and `