#57 Add type annotations where necessary
Merged 6 months ago by a2batic. Opened 7 months ago by amitosh.
amitosh/Fedora-app tsify  into  master

file modified
+4 -8

@@ -3,23 +3,19 @@ 

    "app_id": "",

    "proxies": [

      {

-       "path": "/facebook/",

-       "proxyUrl": "https://graph.facebook.com/v2.6/"

-     },

-     {

-       "path": "/twitter/",

+       "path": "/twitter",

        "proxyUrl": "https://api.twitter.com/1.1/"

      },

      {

-       "path": "/fedocal/",

+       "path": "/fedocal",

        "proxyUrl": "https://apps.fedoraproject.org/calendar/api/"

      },

      {

-       "path": "/fedoramag/",

+       "path": "/fedora-magazine",

        "proxyUrl": "https://fedoramagazine.org/wp-json/wp/v2/"

      },

      {

-       "path": "/askfedora/",

+       "path": "/ask-fedora",

        "proxyUrl": "https://ask.fedoraproject.org/en/api/v1/"

      }

    ],

file modified
+5 -0

@@ -4123,6 +4123,11 @@ 

        "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",

        "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="

      },

+     "lodash-es": {

+       "version": "4.17.10",

+       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.10.tgz",

+       "integrity": "sha512-iesFYPmxYYGTcmQK0sL8bX3TGHyM6b2qREaB4kamHfQyfPJP0xgoGxp19nsH16nsfquLdiyKyX3mQkfiSGV8Rg=="

+     },

      "lodash.assign": {

        "version": "4.2.0",

        "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",

file modified
+1 -0

@@ -59,6 +59,7 @@ 

      "ionic-plugin-keyboard": "^2.2.1",

      "ionicons": "^3.0.0",

      "lodash": "^4.14.0",

+     "lodash-es": "^4.17.10",

      "moment": "^2.14.1",

      "moment-timezone": "^0.5.5",

      "query-string": "^4.2.2",

file modified
+27 -16

@@ -1,26 +1,32 @@ 

- import {Component, ViewChild} from '@angular/core';

+ import { Component, ViewChild } from '@angular/core';

  import { Platform, NavController } from 'ionic-angular';

  

- import {MagazinePage} from '../pages/magazine/magazine';

- import {AskPage} from '../pages/ask/ask';

- import {CalendarPage} from '../pages/calendar/calendar';

- import {SocialPage} from '../pages/social/social';

- import {WomenPage} from '../pages/women/women';

- import {FirstPage} from '../pages/first/first';

+ import { MagazinePage } from '../pages/magazine/magazine';

+ import { AskPage } from '../pages/ask/ask';

+ import { CalendarPage } from '../pages/calendar/calendar';

+ import { SocialPage } from '../pages/social/social';

+ import { WomenPage } from '../pages/women/women';

+ import { FirstPage } from '../pages/first/first';

  import { SplashScreen } from '@ionic-native/splash-screen';

  

+ /**

+  * Entrypoint for the Fedora App

+  */

  @Component({

    templateUrl: 'app.html',

  })

- export class MyApp {

-   pages:Array<{title:string, component:any}>;

-   rootPage:any;

+ export class App {

+   /**

+    * Contains the pages that constitute this app

+    */

+   pages: { title: string, component: any }[];

  

-   @ViewChild('content') nav:NavController;

+   rootPage: any;

  

+   @ViewChild('content') nav: NavController;

  

-   constructor(platform:Platform, splashScreen:SplashScreen) {

-     // used for an example of ngFor and navigation

+ 

+   constructor(platform: Platform, splashScreen: SplashScreen) {

      this.pages = [

        { title: 'Home', component: FirstPage },

        { title: 'Magazine', component: MagazinePage },

@@ -40,9 +46,14 @@ 

Shouldnt it be containing all the pages ?

      });

    }

  

-   openPage(page) {

-     // Reset the content nav to have just this page

-     // we wouldn't want the back button to show in this scenario

+   /**

+    * Navigate to a new page

+    *

+    * @param page page to navigate to

+    */

+   openPage(page): void {

+     // Reset the content nav to have just this page we wouldn't want the back

+     // button to show in this scenario

      this.nav.push(page.component);

    }

  }

file modified
+5 -9

@@ -9,7 +9,7 @@ 

  import { SpinnerDialog } from '@ionic-native/spinner-dialog';

  import { Toast } from '@ionic-native/toast';

  

- import { MyApp } from './app.component';

+ import { App } from './app.component';

  import { FirstPage } from '../pages/first/first';

  import { MagazinePage } from '../pages/magazine/magazine';

  import { AskPage } from '../pages/ask/ask';

@@ -18,12 +18,10 @@ 

  import { WomenPage } from '../pages/women/women';

  

  import { Browser } from '../providers/browser/browser';

- import { Request } from '../providers/request/request';

- import { IonicConfig } from '../providers/ionic-config/ionic-config';

  

  @NgModule({

    declarations: [

-     MyApp,

+     App,

      FirstPage,

      MagazinePage,

      AskPage,

@@ -34,11 +32,11 @@ 

    imports: [

      BrowserModule,

      HttpClientModule,

-     IonicModule.forRoot(MyApp)

+     IonicModule.forRoot(App)

    ],

    bootstrap: [IonicApp],

    entryComponents: [

-     MyApp,

+     App,

      FirstPage,

      MagazinePage,

      AskPage,

@@ -48,15 +46,13 @@ 

    ],

    providers: [

      Browser,

-     IonicConfig,

      InAppBrowser,

-     Request,

      SocialSharing,

      SplashScreen,

      SpinnerDialog,

      StatusBar,

      Toast,

-     { provide: ErrorHandler, useClass: IonicErrorHandler }

+     { provide: ErrorHandler, useClass: IonicErrorHandler },

    ]

  })

  export class AppModule { }

@@ -8,7 +8,8 @@ 

  };

  

  const TWITTER_CONFIG = {

-   BEARER_TOKEN: 'AAAAAEXAMPLEAAAAAAAAMmWwEXAMPLE9tGRvIFnIR8XYXmIFTFaEGagjX0%3Dup2JDIi9hjbCJKGaEGDqkLMtYSGumkyMa6SbwXx0FMB1vOlvN0'

+   BEARER_TOKEN: 'AAAAAEXAMPLEAAAAAAAAMmWwEXAMPLE9tGRvIFnIR8XYXmEXAMpLEGgjX0%3' +

+                 'Dup2JDExAmplEJKGaEXAmpLetYSGumkyExAMplEx0FMB1vOlvN0'

  };

  

  const ENV = {

file modified
+2 -0

@@ -4,6 +4,8 @@ 

  import { enableProdMode } from '@angular/core';

  

  if (ENV.PROD) {

+   // Enable Angular prod mode in PROD builds only. Angular Prod mode drops a

+   // considerable amount of assertions

    enableProdMode();

  }

  

file modified
+3 -3

@@ -39,17 +39,17 @@ 

        </ion-card-content>

        <ion-row id="stats">

          <ion-col id="vote">

-           <strong>{{ question.vote }}</strong>

+           <strong>{{ question.score }}</strong>

            <br>

            VOTE

          </ion-col>

          <ion-col id="ans">

-           <strong>{{ question.answers }}</strong>

+           <strong>{{ question.answerCount }}</strong>

            <br>

            Ans

          </ion-col>

          <ion-col id="view">

-           <strong>{{ question.view }}</strong>

+           <strong>{{ question.viewCount }}</strong>

            <br>

            VIEW

          </ion-col>

file modified
+40 -19

@@ -1,24 +1,27 @@ 

  import { Component } from '@angular/core';

  import { SocialSharing } from '@ionic-native/social-sharing';

- 

  import { Browser } from '../../providers/browser/browser';

- import { AskFedora } from '../../providers/ask-fedora/ask-fedora';

- 

- /*

-   Generated class for the AskPage page.

+ import { AskFedoraService, Question } from '../../providers/ask-fedora/ask-fedora';

  

-   See http://ionicframework.com/docs/v2/components/#navigation for more info on

-   Ionic pages and navigation.

- */

+ /**

+  * Shows latest questions from Ask Fedora

+  *

+  * Fetches latest 30 questions from Ask Fedora and displays the questions, the

+  * answers and the number of votes each question receives.

+  */

  @Component({

    templateUrl: 'ask.html',

-   providers: [Browser, AskFedora],

+   providers: [Browser, AskFedoraService],

  })

  export class AskPage {

-   private questions: any;

+ 

+   /**

+    * Stores list of displayed questions

+    */

+   private questions: Question[];

  

    constructor(private browser: Browser,

-     private askFedora: AskFedora, private socialSharing: SocialSharing) {

+     private askFedora: AskFedoraService, private socialSharing: SocialSharing) {

      this.questions = [];

    }

  

@@ -26,24 +29,42 @@ 

      this.updateQuestions();

    }

  

-   updateQuestions() {

+   /**

+    * Fetch a list of latest questions using Ask Fedora API.

+    */

+   updateQuestions(): void {

      this.askFedora

        .getQuestions()

-       .then(questions => {

+       .subscribe(questions => {

          this.questions = questions;

        });

    }

  

-   openQuestion(event) {

-     this.browser.open(event.link);

+   /**

+    * Open a question in a browser.

+    *

+    * Opens question in an in-app browser in mobile app and in a new tab on desktop.

+    *

+    * @param question question to open

+    */

+   openQuestion(question: Question): void {

+     this.browser.open(question.link);

    }

  

-   shareQuestion(event) {

+   /**

+    * Share the question using a third-party app installed in the user's device

+    *

+    * Allows to share the question using apps like WhatsApp, Facebook, or any app that

+    * exposes a share interface to the underlying OS.

+    *

+    * @param question question to share

+    */

+   shareQuestion(question: Question): void {

      this.socialSharing.share(

-       event.title,

-       event.title,

+       question.content,

+       question.title,

        null,

-       event.link

+       question.link

      );

    }

  }

@@ -16,8 +16,8 @@ 

      <ion-item>

  

        <ion-select [(ngModel)]="selectedCalendar" (ngModelChange)="updateMeetings()">

-         <ion-option *ngFor="let calendar of calendars" [value]="calendar.real_name" [selected]="calendar.real_name == selectedCalendar">

-           {{ calendar.display_name }}

+         <ion-option *ngFor="let calendar of calendars" [value]="calendar.realName" [selected]="calendar.realName == selectedCalendar">

+           {{ calendar.displayName }}

          </ion-option>

        </ion-select>

      </ion-item>

@@ -27,12 +27,12 @@ 

            <strong>{{ meeting.name }}</strong>

          </ion-card-header>

          <ion-card-content>

-           {{ meeting.display_description }}

+           {{ meeting.description }}

          </ion-card-content>

          <ion-row center>

            <ion-col width-67 padding>

-             <p>{{ meeting.display_date }}</p>

-             <p>{{ meeting.display_time }}</p>

+             <p>{{ meeting.displayTime.dateString }}</p>

+             <p>{{ meeting.displayTime.timeString }}</p>

            </ion-col>

            <ion-col width-33 padding>

              <button ion-button id="add_button" small tappable (click)="addToCalendar(meeting)">

file modified
+48 -22

@@ -1,27 +1,42 @@ 

  import { Component } from '@angular/core';

  import { Calendar } from '@ionic-native/calendar';

  

- import { FedoCal } from '../../providers/fedo-cal/fedo-cal';

+ import { FedoCalService, Calendar as CalendarType, Meeting } from '../../providers/fedo-cal/fedo-cal';

  

- /*

-   Generated class for the CalendarPage page.

- 

-   See http://ionicframework.com/docs/v2/components/#navigation for more info on

-   Ionic pages and navigation.

- */

  

+ /**

+  * We default to the QA calendar

+  */

  const DEFAULT_CALENDAR = 'QA';

  

+ /**

+  * The FedoCal interface

+  *

+  * Shows the list of calendars availabe on FedoCal and the meetings of each

+  * calendar. Also allows to add meetings from the calendar to the system calendar.

+  */

  @Component({

    templateUrl: 'calendar.html',

-   providers: [ FedoCal, Calendar ]

+   providers: [FedoCalService, Calendar]

  })

  export class CalendarPage {

-   private calendars:Array<any>;

-   private meetings:Array<any>;

-   private selectedCalendar:string;

  

-   constructor(private fedoCal:FedoCal, private calendar:Calendar) {

+   /**

+    * List of calendars in FedoCal

+    */

+   private calendars: CalendarType[];

+ 

+   /**

+    * List of meetings in the selected calendar

+    */

+   private meetings: Meeting[];

+ 

+   /**

+    * ID of the selected calendar

+    */

+   private selectedCalendar: string;

+ 

+   constructor(private fedoCal: FedoCalService, private calendar: Calendar) {

      this.calendars = [];

      this.meetings = [];

  

@@ -33,29 +48,40 @@ 

      this.updateMeetings();

    }

  

-   updateCalendars() {

+   /**

+    * Update the list of calendars from FedoCal

+    */

+   updateCalendars(): void {

      this.fedoCal

        .getCalendars()

-       .then(calendars => {

+       .subscribe(calendars => {

          this.calendars = calendars;

        });

    }

  

-   updateMeetings() {

+   /**

+    * Update the list of meetings for the selected calendar

+    */

+   updateMeetings(): void {

      this.fedoCal

        .getMeetings(this.selectedCalendar)

-       .then(meetings => {

+       .subscribe(meetings => {

          this.meetings = meetings;

        });

    }

  

-   addToCalendar(event) {

+   /**

+    * Add a FedoCal meeting to the system calendar

+    *

+    * @param meeting meeting to add to the calendar

+    */

+   addToCalendar(meeting:Meeting): void {

      this.calendar.createEventInteractively(

-       event.name,

-       event.location,

-       event.real_description,

-       event.datetime_start,

-       event.datetime_end

+       meeting.name,

+       meeting.location,

+       meeting.description,

+       meeting.time,

+       meeting.timeEnd

      );

    }

  }

file modified
+23 -44

@@ -1,68 +1,47 @@ 

  import { Component } from '@angular/core';

  import { NavController } from 'ionic-angular';

- import { SocialSharing } from '@ionic-native/social-sharing';

- 

- import { Browser } from '../../providers/browser/browser';

- import { FedoraMag } from '../../providers/fedora-mag/fedora-mag';

  import { MagazinePage } from '../magazine/magazine';

  import { AskPage } from '../ask/ask';

  import { CalendarPage } from '../calendar/calendar';

  import { SocialPage } from '../social/social';

  

- /*

-   Generated class for the MagazinePage page.

- 

-   See http://ionicframework.com/docs/v2/components/#navigation for more info on

-   Ionic pages and navigation.

- */

+ /**

+  * Home page of the Fedora App

+  */

  @Component({

    templateUrl: 'first.html',

-   providers: [ FedoraMag ]

  })

  

  export class FirstPage {

-   private posts: Array<any>;

- 

-   constructor(private nav: NavController, private browser: Browser,

-     private fedoraMag: FedoraMag, private socialSharing: SocialSharing) {

-     this.posts = [];

-   }

- 

-   ngOnInit() {

-     this.updatePosts();

-   }

- 

-   updatePosts() {

-     this.fedoraMag.getPosts().then(posts => {

-       this.posts = posts;

-     });

-   }

  

-   openPost(event) {

-     this.browser.open(event.link);

+   constructor(private nav: NavController) {

    }

  

-   openMag() {

+   /**

+    * Navigate to Fedora Magazine section

+    */

+   openMag(): void {

      this.nav.push(MagazinePage);

    }

-   openAsk() {

+ 

+   /**

+    * Navigate to Ask Fedora section

+    */

+   openAsk(): void {

      this.nav.push(AskPage);

    }

-   openSocial() {

-     this.nav.push(SocialPage);

-   }

-   openCal() {

-     this.nav.push(CalendarPage);

-   }

- 

-   login(event) {

  

+   /**

+    * Navigate to Fedora Social section

+    */

+   openSocial(): void {

+     this.nav.push(SocialPage);

    }

  

-   sharePost(event) {

-     this.socialSharing.share(

-       event.title, event.title,

-       null, event.link

-     );

+   /**

+    * Navigate to Fedora Calendar section

+    */

+   openCal(): void {

+     this.nav.push(CalendarPage);

    }

  }

file modified
+40 -20

@@ -2,23 +2,24 @@ 

  import { SocialSharing } from '@ionic-native/social-sharing';

  

  import { Browser } from '../../providers/browser/browser';

- import { FedoraMag } from '../../providers/fedora-mag/fedora-mag';

+ import { FedoraMagazineService, Post } from '../../providers/fedora-magazine/fedora-magazine';

  

- /*

-   Generated class for the MagazinePage page.

- 

-   See http://ionicframework.com/docs/v2/components/#navigation for more info on

-   Ionic pages and navigation.

- */

+ /**

+  * Shows latest posts from Fedora Magazine

+  */

  @Component({

    templateUrl: 'magazine.html',

-   providers: [FedoraMag],

+   providers: [FedoraMagazineService],

  })

  export class MagazinePage {

-   private posts:Array<any>;

  

-   constructor(private browser:Browser,

-     private fedoraMag:FedoraMag, private socialSharing:SocialSharing) {

+   /**

+    * List of posts from Fedora Magazine

+    */

+   private posts: Post[];

+ 

+   constructor(private browser: Browser,

+     private fedoraMag: FedoraMagazineService, private socialSharing: SocialSharing) {

      this.posts = [];

    }

  

@@ -26,20 +27,39 @@ 

      this.updatePosts();

    }

  

-   updatePosts() {

-     this.fedoraMag.getPosts().then(posts => {

-       this.posts = posts;

-     });

+   /**

+    * Fetch latest posts from Fedor Magazine API

+    */

+   updatePosts(): void {

+     this.fedoraMag.getPosts()

+       .subscribe(posts => {

+         this.posts = posts;

+       });

    }

  

-   openPost(event) {

-     this.browser.open(event.link);

+   /**

+    * Open a post in a browser

+    *

+    * Opens the post in an in-app browser in mobile app and in a new tab on desktop.

+    *

+    * @param post post to open

+    */

+   openPost(post:Post): void {

+     this.browser.open(post.link);

    }

  

-   sharePost(event) {

+   /**

+    * Share the post using a third-party app installed in the user's device

+    *

+    * Allows to share the post using apps like WhatsApp, Facebook, or any app that

+    * exposes a share interface to the underlying OS.

+    *

+    * @param post post to share

+    */

+   sharePost(post:Post): void {

      this.socialSharing.share(

-       event.title, event.title,

-       null, event.link

+       post.excerpt, post.title,

+       null, post.permalink

      );

    }

  }

file modified
+6 -6

@@ -14,23 +14,23 @@ 

  

    <img id="social_logo" src="assets/img/social_icon.svg">

  

-   <ion-list *ngIf="updates.length !== 0" id="social_card">

-     <ion-card *ngFor="let update of updates" >

+   <ion-list *ngIf="posts.length !== 0" id="social_card">

+     <ion-card *ngFor="let post of posts" >

  

          <ion-row>

-         <ion-col tappable (click)="openUpdate(update)">

-           {{ update.content }}

+         <ion-col tappable (click)="openPost(post)">

+           {{ post.content }}

          </ion-col>

  

        </ion-row>

        <ion-row >

          <ion-col center >

            <button ion-button small clear disabled class="s-icon">

-             <ion-icon name="logo-{{ update.origin }}"></ion-icon>

+             <ion-icon name="logo-{{ post.origin }}"></ion-icon>

            </button>

          </ion-col>

          <ion-col center>

-           <button ion-button small clear tappable color="dark" (click)="shareUpdate(update)">

+           <button ion-button small clear tappable color="dark" (click)="sharePost(post)">

              <ion-icon name="share"></ion-icon>

            </button>

          </ion-col>

file modified
+53 -48

@@ -2,75 +2,80 @@ 

  import { SocialSharing } from '@ionic-native/social-sharing';

  

  import { Browser } from '../../providers/browser/browser';

- import { FB } from '../../providers/fb/fb';

- import { Tw } from '../../providers/tw/tw';

+ import { FacebookProvider } from '../../providers/social/facebook';

+ import { TwitterProvider } from '../../providers/social/twitter';

+ import { Post } from '../../providers/social/social';

+ import { forkJoin } from 'rxjs/observable/forkJoin';

  

- /*

-   Generated class for the SocialPage page.

+ const HANDLE = {

+   FB: 'fedoraqa',

+   TWITTER: 'fedora_qa',

+ };

  

-   See http://ionicframework.com/docs/v2/components/#navigation for more info on

-   Ionic pages and navigation.

- */

+ const DEFAULT_POST_TITLE = 'Update from Fedora Project';

+ 

+ /**

+  * Shows updates from the social media channels of Fedora

+  */

  @Component({

    templateUrl: 'social.html',

-   providers: [FB, Tw],

+   providers: [FacebookProvider, TwitterProvider],

  })

  export class SocialPage {

  

-   // TODO: Refactor, we are not supposed to use `any`, rather, ww should define an

-   // interface Post { content:string, date:Date }

-   private posts:Array<any>;

-   private tweets:Array<any>;

-   private updates:Array<any>;

-   private USER:any;

+   /**

+    * List of posts from different social media channels

+    */

+   private posts: Post[];

  

-   constructor(private browser:Browser, private fb:FB, private tw:Tw,

-               private socialSharing:SocialSharing) {

+   constructor(private browser: Browser, private fb: FacebookProvider, private twitter: TwitterProvider,

+     private socialSharing: SocialSharing) {

      this.posts = [];

-     this.tweets = [];

-     this.updates = [];

- 

-     this.USER = {

-       FB: 'fedoraqa',

-       TW: 'fedora_qa',

-     };

    }

  

    ngOnInit() {

-     this.updateUpdates();

+     this.updatePosts();

    }

  

-   updateUpdates() {

-     this.fb

-       .getPagePosts(this.USER.FB)

-       .then((posts:Array<any>) => {

-         this.posts = posts;

-         this.mergeUpdates();

-       });

- 

-     this.tw

-       .getTimelineTweets(this.USER.TW)

-       .then((tweets:Array<any>) => {

-         this.tweets = tweets;

-         this.mergeUpdates();

+   /**

+    * Fetch posts from social media channels

+    *

+    * Currently, we fetch posts from Facebook and Twitter

+    */

+   private updatePosts(): void {

+     forkJoin(this.fb.getPosts(HANDLE.FB), this.twitter.getPosts(HANDLE.TWITTER))

+       .subscribe(values => {

+         this.posts = [...values[0], ...values[1]] as Post[];

        });

    }

  

-   mergeUpdates() {

-     // TODO: Merge as per ascending order of timestamps?

-     this.updates = [ ...this.posts, ...this.tweets ];

-   }

- 

-   openUpdate(event) {

-     this.browser.open(event.link);

+   /**

+    * Open a post in a browser

+    *

+    * Opens the post in an in-app browser in mobile app and in a new tab on desktop.

+    * On some platforms, the post may directly open in the app of the social media

+    * service.

+    *

+    * @param post post to open

+    */

+   openPost(post:Post): void {

+     this.browser.open(post.link);

    }

  

-   shareUpdate(event) {

+   /**

+    * Share the post using a third-party app installed in the user's device

+    *

+    * Allows to share the post using apps like WhatsApp, Facebook, or any app that

+    * exposes a share interface to the underlying OS.

+    *

+    * @param post post to share

+    */

+   sharePost(post:Post): void {

      this.socialSharing.share(

-       event.title,

-       event.title,

+       post.content,

+       DEFAULT_POST_TITLE,

        null,

-       event.link

+       post.link

      );

    }

  }

file modified
+5 -60

@@ -1,67 +1,12 @@ 

  import { Component } from '@angular/core';

- import { NavController } from 'ionic-angular';

- import { SocialSharing } from '@ionic-native/social-sharing';

  

- import { Browser } from '../../providers/browser/browser';

- import { FedoraMag } from '../../providers/fedora-mag/fedora-mag';

- import { MagazinePage } from '../magazine/magazine';

- import { AskPage } from '../ask/ask';

- import { CalendarPage } from '../calendar/calendar';

- import { SocialPage } from '../social/social';

- 

- /*

-   Generated class for the MagazinePage page.

- 

-   See http://ionicframework.com/docs/v2/components/#navigation for more info on

-   Ionic pages and navigation.

- */

+ /**

+  * Fedora Women section

+  *

+  * Under construction

+  */

  @Component({

    templateUrl: 'women.html',

-   providers: [Browser, FedoraMag],

  })

  export class WomenPage {

-   posts: Array<any>;

- 

-   constructor(private nav: NavController, private browser: Browser,

-     private fedoraMag: FedoraMag, private socialSharing: SocialSharing) {

-     this.posts = [];

-   }

- 

-   ngOnInit() {

-     this.updatePosts();

-   }

- 

-   updatePosts() {

-     this.fedoraMag.getPosts().then(posts => {

-       this.posts = posts;

-     });

-   }

- 

-   openPost(event) {

-     this.browser.open(event.link);

-   }

- 

-   openMag() {

-     this.nav.push(MagazinePage);

-   }

-   openAsk() {

-     this.nav.push(AskPage);

-   }

-   openSocial() {

-     this.nav.push(SocialPage);

-   }

-   openCal() {

-     this.nav.push(CalendarPage);

-   }

- 

-   login(event) {

- 

-   }

- 

-   sharePost(event) {

-     this.socialSharing.share(

-       event.title, event.title,

-       null, event.link

-     );

-   }

  }

@@ -1,57 +1,96 @@ 

- import { Injectable } from '@angular/core';

  import 'rxjs/add/operator/map';

+ import { Injectable } from '@angular/core';

+ import { HttpClient } from '@angular/common/http';

+ import { chooseEndpoint } from '../../utils';

+ import { Observable } from 'rxjs/Observable';

  

- import * as _ from 'lodash';

+ /**

+  * Ask Fedora API endpoint

+  *

+  * It does not support CORS so we have to proxy it through Ionic CLI during development

+  */

+ const ENDPOINT = chooseEndpoint('/ask-fedora', 'https://ask.fedoraproject.org/en/api/v1');

  

- import { Request } from '../request/request';

+ /**

+  * A question on Ask Fedora

+  */

+ export interface Question {

+   /**

+    * Question ID

+    */

+   id: number,

  

- const API_ENDPOINT = 'https://ask.fedoraproject.org/en/api/v1';

+   /**

+    * Question title

+    */

+   title: string,

  

- /*

-   Generated class for the AskFedora provider.

+   /**

+    * Permalink to the question

+    */

+   link: string,

  

-   See https://angular.io/docs/ts/latest/guide/dependency-injection.html

-   for more info on providers and Angular 2 DI.

- */

- @Injectable()

- export class AskFedora {

-   private API:any;

-   private questions:Array<any>;

+   /**

+    * Number of answers to this question

+    */

+   answerCount: number,

+ 

+   /**

+    * Content of this question

+    */

+   content: string,

+ 

+   /**

+    * Time of posting of this question

+    */

+   addedAt: Date,

  

-   constructor(private request:Request) {

-     this.API = {

-       base: [API_ENDPOINT],

-       questions: [API_ENDPOINT, 'questions'],

-     };

+   /**

+    * Tags associated with this question

+    */

+   tags: string[],

  

-     this.questions = [];

+   /**

+    * Number of views registered by this question

+    */

+   viewCount: number,

+ 

+   /**

+    * Total score for this question

+    *

+    * It is the sum of all up-votes and down-votes.

+    */

+   score: number,

+ }

+ 

+ 

+ /**

+  * Service for Ask Fedora API

+  *

+  * Provides a read-only access to questions and answers posted in Ask Fedora.

+  */

+ @Injectable()

+ export class AskFedoraService {

+   constructor(private http: HttpClient) {

    }

  

-   getQuestions() {

-     if (!_.isEmpty(this.questions)) {

-       return Promise.resolve(this.questions);

-     }

- 

-     return new Promise((resolve, reject) => {

-       this.request.get(this.API.questions)

-         .then((data:any) => {

-           this.questions = _.map(data.questions, q => {

-             const question = {

-               id: q.id,

-               title: q.title,

-               link: q.url,

-               answers: q.answer_count,

-               content: q.summary,

-               timestamp: q.added_at,

-               tags:q.tags,

-               view:q.view_count,

-               vote:q.score,

-             };

-             return question;

-           });

- 

-           return resolve(this.questions);

-         }).catch(reject);

-     });

+   /**

+    * Fetch a list of questions from Ask Fedora API

+    *

+    * @returns Observable which emits an array of questions

+    */

+   getQuestions(): Observable<Question[]> {

+     return this.http.get(`${ENDPOINT}/questions/`)

+       .map((data: any) => (data.questions as any[]).map(q => ({

+         id: q.id,

+         title: q.title,

+         link: q.url,

+         answerCount: q.answer_count,

+         content: q.summary,

+         addedAt: new Date(parseInt(q.added_at, 10)),

+         tags: q.tags,

+         viewCount: q.view_count,

+         score: q.score,

+       })));

    }

  }

@@ -4,28 +4,36 @@ 

  import { Toast } from '@ionic-native/toast';

  import { Platform } from 'ionic-angular';

  

- /*

-   Generated class for the Browser provider.

- 

-   See https://angular.io/docs/ts/latest/guide/dependency-injection.html

-   for more info on providers and Angular 2 DI.

- */

+ /**

+  * Wrapper over in-app browser to show a busy indicator while loading web content

+  */

  @Injectable()

  export class Browser {

+ 

    private browser: InAppBrowserObject;

+ 

    constructor(private platform: Platform, private inAppBrowser: InAppBrowser, private spinnerDialog: SpinnerDialog,

      private toast: Toast) {

      this.browser = null;

    }

  

+   /**

+    * Called when content loading is started

+    */

    private startHandler = () => {

      this.spinnerDialog.show();

    }

  

+   /**

+    * Called when content loading is complete

+    */

    private stopHandler = () => {

      this.spinnerDialog.hide();

    }

  

+   /**

+    * Called when there is an error while loading content

+    */

    private errorHandler = (error) => {

      this.toast.showShortBottom('Something went wrong.');

      this.browser.close();

@@ -33,8 +41,15 @@ 

      this.browser = null;

    }

  

-   open(link) {

-     const browser: InAppBrowserObject = this.inAppBrowser.create(encodeURI(link), '_blank');

+   /**

+    * Open a link in an in-app browser

+    *

+    * Attaches lifecycle callbacks only on platforms which support them.

+    *

+    * @param link link to open in a an in-app browser

+    */

+   public open(link): void {

+     const browser = this.inAppBrowser.create(encodeURI(link), '_blank');

      this.browser = browser;

  

      // Cordova does not fire the events in browser.

file removed
-56

@@ -1,56 +0,0 @@ 

- import { Injectable } from '@angular/core';

- import 'rxjs/add/operator/map';

- import ENV from '@environment';

- 

- import { Facebook } from 'fb';

- import * as _ from 'lodash';

- 

- 

- /*

-   Generated class for the Fb provider.

- 

-   See https://angular.io/docs/ts/latest/guide/dependency-injection.html

-   for more info on providers and Angular 2 DI.

- */

- @Injectable()

- export class FB {

-   private fb:Facebook;

-   constructor() {

-     this.fb = new Facebook(ENV.FB_CONFIG);

-   }

- 

-   api(urlParts) {

-     return new Promise((resolve, reject) => {

-       this.fb.api(urlParts.join('/'), res => {

-         if (!res || res.error) {

-           return reject(res);

-         } else {

-           return resolve(res);

-         }

-       });

-     });

-   }

- 

-   getPagePosts(page) {

-     return new Promise((resolve, reject) => {

-       this.api([page, 'posts']).then((res:any) => {

-         var posts = _.compact(_.map(res.data, p => {

-           var post = {

-             id: p.id,

-             link: 'https://facebook.com/' + p.id,

-             content: p.message,

-             origin: 'facebook',

-           };

- 

-           if (_.isEmpty(post.content)) {

-             return null;

-           } else {

-             return post;

-           }

-         }));

- 

-         return resolve(posts);

-       }).catch(reject);

-     });

-   }

- }

@@ -4,23 +4,37 @@ 

  import * as _ from 'lodash';

  import * as moment from 'moment-timezone';

  

- import { Request } from '../request/request';

- 

- const API_ENDPOINT = 'https://apps.fedoraproject.org/calendar/api/';

- const API = {

-   base: [API_ENDPOINT],

-   calendars: [API_ENDPOINT, 'calendars'],

-   meetings: [API_ENDPOINT, 'meetings'],

- };

- 

+ import { HttpClient } from '@angular/common/http';

+ import { Observable } from 'rxjs/Observable';

+ import { chooseEndpoint } from '../../utils';

+ 

+ /**

+  * FedoCal API endpoint

+  *

+  * It does not support CORS so we have to proxy it through Ionic CLI during development

+  */

+ const ENDPOINT = chooseEndpoint('/fedocal', 'https://apps.fedoraproject.org/calendar/api');

+ 

+ /**

+  * Date format for display

+  */

  const DATE_FORMAT = 'dddd, MMMM Do YYYY';

- const TIME_FORMAT = 'h:mm a z';

  

- /*

-   Convert title to capital case, but skip special names like FESCo, FAmSCo, i18n

- */

- function calendarNameToDisplayName(name:string) :string {

-   switch(name) {

+ /**

+  * Time format for display

+  */

+ const TIME_FORMAT = 'h:mm A z';

+ 

+ /**

+  * Convert calendar name from API to a value suitable for display

+  *

+  * Convert title to capital case, but skip special names like FESCo, FAmSCo, i18n

+  *

+  * @param name Calendar name as in API

+  * @returns    Friendly representation of the name

+  */

+ function calendarNameToDisplayName(name: string): string {

+   switch (name) {

      case 'i18n':

        return 'i18n';

      case 'fesco':

@@ -28,93 +42,173 @@ 

      default:

        return /^[A-Z][^\d-]*.*$/.test(name) ? name : _.startCase(name);

    }

+ }

  

+ /**

+  * A calendar on FedoCal

+  *

+  * Calendars are assoicated with a group / SIG / or event. They contain a number

+  * of recurring meetings or events.

+  */

+ export interface Calendar {

+ 

+   /**

+    * Name of calendar, as expressed in the API

+    *

+    * Serves as an ID for API calls.

+    */

+   realName: string,

+ 

+   /**

+    * Human friendly calendar name obtained from `realName`

+    */

+   displayName: string,

+ 

+   /**

+    * Calendar's description

+    */

+   description: string,

+ 

+   /**

+    * Group with admin previlages for this calendar

+    */

+   adminGroup: string,

+ 

+   /**

+    * Group with edit previlages for this calendar

+    */

+   editorGroup: string,

+ 

+   /**

+    * Contact person for this calendar

+    */

+   contact: string,

+ 

+   /**

+    * Whether the calendar is enabled

+    */

+   enabled: boolean,

  }

  

- /*

-   Generated class for the FedoCal provider.

+ /**

+  * A meeting on a FedoCal calendar

+  */

+ export interface Meeting {

+ 

+   /**

+    * Name of the meeting

+    */

+   name: string,

+ 

+   /**

+    * Description of the meeting

+    */

+   description: string,

+ 

+   /**

+    * Where is the meeting happening?

+    *

+    * Can be an IRC channel or even a physical location.

+    */

+   location: string,

+ 

+   /**

+    * Meeting start time

+    */

+   time: Date,

+ 

+   /**

+    * Meeting end time

+    */

+   timeEnd: Date,

+ 

+   /**

+    * Start time formatted for display

+    */

+   displayTime: {

+     dateString: string,

+     timeString: string,

+   },

+ 

+   /**

+    * End time formatted for display

+    */

+   displayTimeEnd: {

+     dateString: string,

+     timeString: string,

+   }

+ }

  

-   See https://angular.io/docs/ts/latest/guide/dependency-injection.html

-   for more info on providers and Angular 2 DI.

- */

+ /**

+  * Service for FedoCal

+  */

  @Injectable()

- export class FedoCal {

-   private calendars:any;

-   private meetings:any;

- 

-   constructor(private request:Request) {

-     this.calendars = [];

-     this.meetings = [];

+ export class FedoCalService {

+   constructor(private http: HttpClient) {

    }

  

-   getCalendars() {

-     if (!_.isEmpty(this.calendars)) {

-       return Promise.resolve(this.calendars);

-     }

- 

-     return new Promise((resolve, reject) => {

-       this.request.get(API.calendars)

-         .then((data:any) => {

-           this.calendars = _.map(data.calendars, c => {

-             return {

-               real_name: c.calendar_name,

-               display_name: calendarNameToDisplayName(c.calendar_name),

-               description: c.calendar_description,

-               contact: c.calendar_contact,

-             };

-           });

- 

-           return resolve(this.calendars);

-         }).catch(reject);

-     });

+   /**

+    * Fetch the list of calendars from FedoCal API

+    *

+    * @returns Observable which emits an array of calendars

+    */

+   getCalendars(): Observable<Calendar[]> {

+     return this.http.get(`${ENDPOINT}/calendars/`)

+       .map((data: any) =>

+         data.calendars.map((c: any) => ({

+           realName: c.calendar_name,

+           displayName: calendarNameToDisplayName(c.calendar_name),

+           description: c.calendar_description,

+           contact: c.calendar_contact,

+           adminGroup: c.calendar_admin_group,

+           editorGroup: c.calendar_editor_group,

+           enabled: c.calendar_status === 'Enabled'

+         })));

    }

  

-   getMeetings(calendar) {

-     if (!_.isEmpty(this.meetings[calendar])) {

-       return Promise.resolve(this.meetings[calendar]);

-     }

- 

-     return new Promise((resolve, reject) => {

-       this.request.get(API.meetings, { calendar: calendar })

-         .then((data:any) => {

-           this.meetings[calendar] = _.map(data.meetings, m => {

-             const meeting:any = {

-               name: m.meeting_name,

-               real_description: m.meeting_information,

-               display_description: _.truncate(

-                 m.meeting_information,

-                 { length: 120, separator: ' ' }

-               ),

-               date_start: m.meeting_date,

-               time_start: m.meeting_time_start,

-               date_end: m.meeting_date_end,

-               time_end: m.meeting_time_end,

-               timezone: m.meeting_timezone,

-               location: m.meeting_location,

-             };

- 

-             const start = dateToMoment(meeting.date_start, meeting.time_start, meeting.timezone);

-             meeting.moment_start = start;

-             meeting.datetime_start = start.toDate();

- 

-             const end = dateToMoment(meeting.date_end, meeting.time_end, meeting.timezone);

-             meeting.moment_end = end;

-             meeting.datetime_end = end.toDate();

- 

-             meeting.display_date = start.format(DATE_FORMAT);

-             meeting.display_time = start.format(TIME_FORMAT);

- 

-             return meeting;

-           });

- 

-           return resolve(this.meetings[calendar]);

-         }).catch(reject);

-     });

+   /**

+    * Fetch the list of meetings for a given FedoCal calendar name

+    *

+    * @param calendar FedoCal calendar name

+    * @returns Observable which emits an array of meetings

+    */

+   getMeetings(calendar:string): Observable<Meeting[]> {

+ 

+     return this.http.get(`${ENDPOINT}/meetings/`, { params: { calendar: calendar } })

+       .map((data: any) => data.meetings.map(m => {

+         const mTime = dateToMoment(m.meeting_date, m.meeting_time_start, m.meeting_timezone);

+         const mTimeEnd = dateToMoment(m.meeting_date_end, m.meeting_time_stop, m.meeting_timezone);

+ 

+         return {

+           name: m.meeting_name,

+           description: m.meeting_information,

+           time: mTime.toDate(),

+           timeEnd: mTimeEnd.toDate(),

+           displayTime: {

+             // Format momentjs object to the defined format for display

+             dateString: mTime.format(DATE_FORMAT),

+             timeString: mTime.format(TIME_FORMAT)

+           },

+           displayTimeEnd: {

+             dateString: mTimeEnd.format(DATE_FORMAT),

+             timeString: mTimeEnd.format(TIME_FORMAT),

+           },

+           location: m.meeting_location,

+         };

+       }));

    }

  }

  

- 

- function dateToMoment(date, time, timezone) {

-   return moment.tz(date + 'T' + time + 'Z', timezone).tz('Etc/UTC');

+ /**

+  * Convert a date consisting of date, time and timezone as different strings to

+  * a single momentjs date

+  *

+  * @param date     Date string

+  * @param time     Time string

+  * @param timezone Timezone identifier

+  */

+ function dateToMoment(date:string, time:string, timezone:string) {

+   const m = moment.tz(`${date} ${time}`, timezone).tz('UTC');

+   return m;

  }

-  

+ 

@@ -1,84 +0,0 @@ 

- import { Injectable } from '@angular/core';

- import 'rxjs/add/operator/map';

- 

- import * as _ from 'lodash';

- 

- import { Request } from '../request/request';

- 

- const API_ENDPOINT = 'https://fedoramagazine.org/wp-json/wp/v2';

- const API = {

-   base: [API_ENDPOINT],

-   posts: [API_ENDPOINT, 'posts'],

- //  media_url: [API_ENDPOINT, 'media'],

- };

- 

- /*

-   Generated class for the FedoraMag provider.

- 

-   See https://angular.io/docs/ts/latest/guide/dependency-injection.html

-   for more info on providers and Angular 2 DI.

- */

- @Injectable()

- export class FedoraMag {

-   private posts:any;

-   private media_url:any;

-   constructor(private request:Request) {

-     this.posts = [];

-     this.media_url = [];

-   }

- 

-   getPosts() {

-     if (!_.isEmpty(this.posts)) {

-       return Promise.resolve(this.posts);

-     }

- 

-     return new Promise((resolve, reject) => {

-       this.request.get(API.posts)

-         .then(data => {

-           this.posts = _.map(data, p => {

-             var excerpt = p.excerpt.rendered;

-             var post = {

-               id: p.id,

-               link: p.guid.rendered,

-               title: p.title.rendered,

-               image: p.featured_media,

-               excerpt: _.truncate(

-                 _.truncate(excerpt, {

-                   length: excerpt.length,

-                   separator: '<a', omission: ''

-                 }), { length: 150, separator: ' ' }

-               ),

-               content: p.content.rendered,

-               date: p.date_gmt,

-             };

-             //image_url = func_url(post.image);

-             return post;

-           });

- 

-           return resolve(this.posts);

-         }).catch(reject);

-     });

-   }

- /*  getMedia_url(posts) {

-     if (!_.isEmpty(this.media_url[posts])) {

-       return Promise.resolve(this.media_url[posts]);

-     }

- 

-     return new Promise((resolve, reject) => {

-       this.request.get(this.API.media_url,{ posts : posts })

-         .then(data => {

-           this.media_url[posts] = _.map(data, i => {

-           //  var excerpt = p.excerpt.rendered;

-             var img_url = {

-               img_id: i.id,

-               img_link: i.guid.rendered,

-             };

-             //image_url = func_url(post.image);

-             return post;

-           });

- 

-           return resolve(this.posts);

-         }).catch(reject);

-     });

-   }*/

- }

@@ -0,0 +1,80 @@ 

+ import 'rxjs/add/operator/map';

+ import { Injectable } from '@angular/core';

+ import { HttpClient } from '@angular/common/http';

+ import { Observable } from 'rxjs/Observable';

+ import { chooseEndpoint } from '../../utils';

+ 

+ const ENDPOINT = chooseEndpoint('/fedora-magazine', 'https://fedoramagazine.org/wp-json/wp/v2');

+ 

+ /**

+  * Represents a post on Fedora Magazine

+  */

+ export interface Post {

+   /**

+    * Unique ID of the post, supplied by the CMS

+    */

+   id: number,

+ 

+   /**

+    * A sluggified link to the post

+    */

+   link: string,

+ 

+   /**

+    * Permalink to the post

+    */

+   permalink:string,

+ 

+   /**

+    * Post title

+    */

+   title: string,

+ 

+   /**

+    * URL to the featured image of the post

+    */

+   image: string,

+ 

+   /**

+    * A short excerpt of the post

+    */

+   excerpt: string,

+ 

+   /**

+    * The content of the post

+    */

+   content: string,

+ 

+   /**

+    * Time of publication

+    */

+   publishedAt: Date

+ }

+ 

+ /**

+  * Service for fetching posts from Fedora Magazine API

+  */

+ @Injectable()

+ export class FedoraMagazineService {

+   constructor(private http: HttpClient) {

+   }

+ 

+   /**

+    * Fetch the list of latest posts on Fedora Magazine

+    *

+    * @returns Observable which emits an array of posts

+    */

+   getPosts(): Observable<Post[]> {

+     return this.http.get(`${ENDPOINT}/posts`)

+       .map((data: any[]) => data.map((post: any) => ({

+         id: post.id,

+         link: post.link,

+         permalink: post.guid.rendered,

+         title: post.title.rendered,

+         image: post.featured_media,