#103 Add calendar subscription manager
Merged 3 years ago by amitosh. Opened 4 years ago by amitosh.
amitosh/Fedora-app cal-subscription  into  master

file modified
+11 -1
@@ -3,6 +3,7 @@ 

  

  import { TabsPage } from '../pages/tabs/tabs';

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

+ import { CalendarSubscriptionManager } from '../providers/calendar-subscription-manager/calendar-subscription-manager';

  

  /**

   * Entrypoint for the Fedora App
@@ -16,11 +17,20 @@ 

  

    @ViewChild('content') nav: NavController;

  

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

+   constructor(platform: Platform, splashScreen: SplashScreen,

+     private subscriptionManager:CalendarSubscriptionManager) {

      platform.ready().then(() => {

        // Okay, so the platform is ready and our plugins are available.

        // Here you can do any higher level native things you might need.

        splashScreen.hide();

      });

    }

+ 

+   ngOnInit() {

+     this.doBackgroundTasks();

+   }

+ 

+   doBackgroundTasks() {

+     this.subscriptionManager.updateCalendarSubscriptions().then(console.log);

+   }

  }

file modified
+6
@@ -27,6 +27,9 @@ 

  

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

  import { DatabaseProvider } from '../providers/database/database';

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

+ import { CalendarSubscriptionManager } from '../providers/calendar-subscription-manager/calendar-subscription-manager';

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

  

  @NgModule({

    declarations: [
@@ -87,6 +90,9 @@ 

      SpinnerDialog,

      StatusBar,

      Toast,

+     Calendar,

+     FedoCalService,

+     CalendarSubscriptionManager,

      { provide: ErrorHandler, useClass: IonicErrorHandler },

      DatabaseProvider,

    ]

@@ -53,4 +53,4 @@ 

      </div>

    </ion-list>

    <no-meetings *ngIf="meetings.length === 0"></no-meetings>

- </ion-content> 

\ No newline at end of file

+ </ion-content>

file modified
+57 -41
@@ -4,6 +4,7 @@ 

  

  import { NotificationsPage } from '../../pages/notifications/notifications';

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

+ import { CalendarSubscriptionManager } from '../../providers/calendar-subscription-manager/calendar-subscription-manager';

  

  

  /**
@@ -19,14 +20,13 @@ 

   */

  @Component({

    templateUrl: 'calendar.html',

-   providers: [FedoCalService, Calendar]

  })

  export class CalendarPage {

  

    /**

     * Set the current active segment as Upcoming Events in the interface

     */

-   showEventsBy: string = "upcoming";

+   showEventsBy = 'upcoming';

  

    /**

     * List of calendars in FedoCal
@@ -36,14 +36,18 @@ 

    /**

     * List of meetings in the selected calendar

     */

-   private meetings: Meeting[];

+   meetings: Meeting[];

  

    /**

     * ID of the selected calendar

     */

    public selectedCalendar: CalendarType;

  

-   constructor(private fedoCal: FedoCalService, private calendar: Calendar, public modalCtrl: ModalController, public navCtrl: NavController) {

+   constructor(

+     private fedoCal: FedoCalService,

+     private calendar: Calendar,

+     private modalCtrl: ModalController,

+     private navCtrl: NavController) {

      this.calendars = [];

      this.meetings = [];

  
@@ -52,17 +56,7 @@ 

  

    ngOnInit() {

      this.updateCalendars();

-     /**

-      * Fetch upcoming meetings

-      */

-     this.updateMeetings('start');

-   }

- 

-   /**

-    * Change the meeting view according to active segment

-    */

-   onSegmentChange() {

-     this.showEventsBy == 'upcoming' ? this.updateMeetings('start') : this.updateMeetings('end');

+     this.updateMeetings();

    }

  

    /**
@@ -80,9 +74,20 @@ 

     * Update the list of meetings for the selected calendar

     * @param type type of meetings to fetch, upcoming or past

     */

-   updateMeetings(type: string): void {

+   updateMeetings(): void {

+     const params: any = {};

+ 

+     const now = new Date();

+     const today = now.toISOString().split('T')[0];

+ 

+     if (this.showEventsBy === 'upcoming') {

+       params.start = today;

+     } else {

+       params.end = today;

+     }

+ 

      this.fedoCal

-       .getMeetings(this.selectedCalendar, type)

+       .getMeetings(this.selectedCalendar, params)

        .subscribe(meetings => {

          this.meetings = meetings;

        });
@@ -93,10 +98,11 @@ 

     */

    showSearch() {

      let searchModal = this.modalCtrl.create(Search, { calendars: this.calendars }, {

-       cssClass: "search-modal",

+       cssClass: 'search-modal',

        showBackdrop: false,

        enableBackdropDismiss: false

      });

+ 

      /**

       * Update calendars when search modal closes

       */
@@ -105,7 +111,7 @@ 

        if (data !== undefined) {

          let receivedCalendar = { realName: data.calendarChoice };

          this.selectedCalendar = receivedCalendar as CalendarType;

-         this.showEventsBy == 'upcoming' ? this.updateMeetings('start') : this.updateMeetings('end');

+         this.updateMeetings();

        }

      });

      searchModal.present();
@@ -152,6 +158,10 @@ 

    openMeetingDetails(meeting: Meeting) {

      this.navCtrl.push(meetingDetails, { meeting: meeting });

    }

+ 

+   onSegmentChange() {

+     this.updateMeetings();

+   }

  }

  

  /**
@@ -161,7 +171,7 @@ 

  @Component({

    templateUrl: 'search.html',

    selector: 'searchpage',

-   providers: [FedoCalService, Calendar]

+   providers: [FedoCalService]

  })

  export class Search {

  
@@ -177,7 +187,10 @@ 

    activeIcon: string = './assets/img/star-active.svg';

    inactiveIcon: string = './assets/img/star-inactive.svg';

  

-   constructor(public viewCtrl: ViewController, navParams: NavParams, public toastCtrl: ToastController) {

+   constructor(

+     private viewCtrl: ViewController, navParams: NavParams,

+     private toastCtrl: ToastController,

+     private subscriptionManager: CalendarSubscriptionManager) {

      //store all the calendars in one place

      this.allCalendars = navParams.get('calendars');

  
@@ -191,9 +204,9 @@ 

  

    /**

     * Listens to input on the search bar

-    * 

+    *

     * Returns calendars matching the query

-    * 

+    *

     * @param query search query entered in the input box

     */

    onInput(query: string): void {
@@ -219,29 +232,32 @@ 

    /**

     * Function called when someone taps the star to subscribe to the calendar

     */

-   subscribeToCal(calendar: CalendarType, i: number): void {

-     /**

-      * Declare toasts for showing events

-      */

-     const subscribedToast = this.toastCtrl.create({

-       message: 'Subscribed to calendar: ' + calendar.realName,

-       duration: 2000

-     });

-     const unsubscribedToast = this.toastCtrl.create({

-       message: 'Unsubscribed from calendar: ' + calendar.realName,

-       duration: 2000

-     });

+   async subscribeToCal(calendar: CalendarType, i: number) {

  

-     /**

-      * Fire event on the basis of selected icon

-      */

+     const options:any = {

+       duration: 2000,

+       position: 'bottom'

+     };

+ 

+     // Fire event on the basis of selected icon

      if (this.calendarIcon[i] === this.inactiveIcon) {

        this.calendarIcon[i] = this.activeIcon;

-       subscribedToast.present();

+ 

+       try {

+         await this.subscriptionManager.subscribeToCalendar(calendar);

+       } catch(e) {

+         options.message = 'Error subscribing to calendar';

+         console.error(e);

+       }

+ 

+       options.message = 'Subscribed to calendar: ' + calendar.displayName;

+ 

      } else {

        this.calendarIcon[i] = this.inactiveIcon;

-       unsubscribedToast.present();

+       options.message = 'Unsubscribed to calendar: ' + calendar.displayName;

      }

+ 

+     this.toastCtrl.create(options).present();

    }

  }

  
@@ -271,4 +287,4 @@ 

        meeting.timeEnd

      );

    }

- } 

\ No newline at end of file

+ }

@@ -0,0 +1,90 @@ 

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

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

+ import { Storage } from '@ionic/storage';

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

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

+ 

+ const SUBSCRIBED_CALENDARS_KEY = 'subscription_mgr__subscribed_cals';

+ 

+ /*

+   Generated class for the CalendarSubscriptionManagerProvider provider.

+ 

+   See https://angular.io/guide/dependency-injection for more info on providers

+   and Angular DI.

+ */

+ @Injectable()

+ export class CalendarSubscriptionManager {

+ 

+   constructor(private storage: Storage,

+     private systemCalendar: SystemCalendar,

+     private fedoCal: FedoCalService) {

+   }

+ 

+   async subscribeToCalendar(c: FedoCalCalendar) {

+     const calendars = [...await this.getSubcribedCalendars(), c];

+     await this.storage.set(SUBSCRIBED_CALENDARS_KEY, calendars);

+     return this.updateCalendarSubscriptions();

+ 

+   }

+ 

+   getSubcribedCalendars(): Promise<FedoCalCalendar[]> {

+     return this.storage.get(SUBSCRIBED_CALENDARS_KEY);

+   }

+ 

+   async updateCalendarSubscriptions() {

+     const subscribed = await this.getSubcribedCalendars();

+     if(subscribed) {

+       for(const cal of subscribed) {

+         try {

+           await this.syncCalendar(cal);

+         } catch(err) {

+           // continue processing other calendars inspite of this error

+           // we log it just to aid in debugging

+           console.error(err);

+         }

+       }

+     }

+   }

+ 

+   async syncCalendar(cal: FedoCalCalendar) {

+     const date = new Date();

+     const start = date.toISOString().split('T')[0];

+ 

+     date.setDate(date.getDate() + 7);

+     const end = date.toISOString().split('T')[0];

+ 

+     const meetings = await this.fedoCal.fetchMeetings(cal, { start, end }).toPromise();

+ 

+     // this must be synchronous because the underlying lib doesn't support

+     // concurrent access to system calendar on android

+     for(const m of meetings) {

+       await this.syncMeeting(m);

+     }

+   }

+ 

+   async syncMeeting(meeting: Meeting) {

+     try {

+       const m:any[] = await this.systemCalendar.findEvent(meeting.name, meeting.location,

+       meeting.description, meeting.time, meeting.timeEnd);

+ 

+       if(!m.length) {

+         return this.addToSystemCalendar(meeting);

+       }

+     } catch(err) {

+       // we log this error just in case,

+       // we ignire this anyway

+       console.error(err);

+     }

+   }

+ 

+   async addToSystemCalendar(meeting:Meeting) {

+     return this.systemCalendar.createEvent(

+       meeting.name,

+       meeting.location,

+       meeting.description,

+       meeting.time,

+       meeting.timeEnd

+     );

+   }

+ 

+ }

@@ -128,6 +128,18 @@ 

    timeEnd: Date,

  }

  

+ interface FetchMeetingOptions {

+   /**

+    * Start date of meetings

+    */

+   start?:string,

+ 

+   /**

+    * End date of meetings

+    */

+   end?:string

+ }

+ 

  /**

   * Service for FedoCal

   */
@@ -196,20 +208,16 @@ 

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

     *

     * @param calendar FedoCal calendar name

-    * @param type Type of Meetings to fetch:

-    * - start(starting after today i.e upcoming meetings)

-    * - end(ended today i.e past meetings)

+    * @param options additional parameters for fetching meetings

     * @returns Observable which emits an array of meetings

     */

-   fetchMeetings(calendar: Calendar, type: string): Observable<Meeting[]> {

- 

-     /**

-      * set search parameters to current date in order to get past and upcoming events

-      */

-     let todaysDate: any = new Date();

-     todaysDate = todaysDate.toISOString().split('T')[0];

+   fetchMeetings(calendar: Calendar, options?:FetchMeetingOptions): Observable<Meeting[]> {

+     const params = {

+       calendar: calendar.realName,

+       ...options

+     }

  

-     return this.http.get(`${ENDPOINT}/meetings/`, { params: { calendar: calendar.realName, [type]: todaysDate } })

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

        .pipe(

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

            // FedoCal splits an ISO8601 string and sends us the date and time,
@@ -240,8 +248,8 @@ 

     * - end(ended today i.e past meetings)

     * @returns Observable which emits an array of meetings

     */

-   getMeetings(calendar: Calendar, type: string): Observable<Meeting[]> {

-     return merge(this.loadCachedMeetings(calendar), this.fetchMeetings(calendar, type).pipe(

+   getMeetings(calendar: Calendar, options?:FetchMeetingOptions): Observable<Meeting[]> {

+     return merge(this.loadCachedMeetings(calendar), this.fetchMeetings(calendar, options).pipe(

        tap(x => this.storage.set(getCalendarStorageKey(calendar), x))

      ));

    }

file modified
+17
@@ -107,3 +107,20 @@ 

    return '';

  }

  

+ export interface AsyncResult<T> {

+   result?: T

+   error?: any

+ }

+ 

+ /**

+  * Create a Promise never rejects

+  *

+  * @return wrapped promise that reports success or error through `resolve`

+  */

+ export function unbreakablePromise<T>(p:Promise<T>): Promise<AsyncResult<T>> {

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

+     p.then(result => ({ result }))

+       .then(resolve)

+       .catch(error => resolve({ error }));

+   });

+ }

Allows subscribing a calendar on FedoCal

Can confirm that the events created in the background actually gives out reminders

Can also confirm that this works across time zones and when changing from DST to non-DST

rebased onto 0acf4b2

3 years ago

Pull-Request has been merged by amitosh

3 years ago