From 0acf4b29bf5bbb17fd9a1035ac7e24a2b85c7d57 Mon Sep 17 00:00:00 2001 From: Amitosh Swain Mahapatra Date: Mar 17 2019 06:19:43 +0000 Subject: Add calendar subscription manager --- diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 924438b..7f15e4c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,6 +3,7 @@ import { Platform, NavController } from 'ionic-angular'; 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 @@ export class App { @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); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6d783ae..566ea94 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -27,6 +27,9 @@ import { NoInternetComponent } from '../components/no-internet/no-internet'; 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 @@ import { DatabaseProvider } from '../providers/database/database'; SpinnerDialog, StatusBar, Toast, + Calendar, + FedoCalService, + CalendarSubscriptionManager, { provide: ErrorHandler, useClass: IonicErrorHandler }, DatabaseProvider, ] diff --git a/src/pages/calendar/calendar.html b/src/pages/calendar/calendar.html index 3c7aaff..90a4c14 100644 --- a/src/pages/calendar/calendar.html +++ b/src/pages/calendar/calendar.html @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/src/pages/calendar/calendar.ts b/src/pages/calendar/calendar.ts index 4e97e46..5df5c40 100644 --- a/src/pages/calendar/calendar.ts +++ b/src/pages/calendar/calendar.ts @@ -4,6 +4,7 @@ import { ModalController, ViewController, NavParams, NavController, ToastControl 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 @@ const DEFAULT_CALENDAR = { realName: 'QA', displayName: 'QA' }; */ @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 @@ export class CalendarPage { /** * 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 @@ export class CalendarPage { 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 @@ export class CalendarPage { * 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 @@ export class CalendarPage { */ 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 @@ export class CalendarPage { 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 @@ export class CalendarPage { openMeetingDetails(meeting: Meeting) { this.navCtrl.push(meetingDetails, { meeting: meeting }); } + + onSegmentChange() { + this.updateMeetings(); + } } /** @@ -161,7 +171,7 @@ export class CalendarPage { @Component({ templateUrl: 'search.html', selector: 'searchpage', - providers: [FedoCalService, Calendar] + providers: [FedoCalService] }) export class Search { @@ -177,7 +187,10 @@ export class Search { 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 @@ export class Search { /** * 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 @@ export class Search { /** * 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 @@ export class meetingDetails { meeting.timeEnd ); } -} \ No newline at end of file +} diff --git a/src/providers/calendar-subscription-manager/calendar-subscription-manager.ts b/src/providers/calendar-subscription-manager/calendar-subscription-manager.ts new file mode 100644 index 0000000..5759cb9 --- /dev/null +++ b/src/providers/calendar-subscription-manager/calendar-subscription-manager.ts @@ -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 { + 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 + ); + } + +} diff --git a/src/providers/fedo-cal/fedo-cal.ts b/src/providers/fedo-cal/fedo-cal.ts index f07fa55..bab7aa3 100644 --- a/src/providers/fedo-cal/fedo-cal.ts +++ b/src/providers/fedo-cal/fedo-cal.ts @@ -128,6 +128,18 @@ export interface Meeting { timeEnd: Date, } +interface FetchMeetingOptions { + /** + * Start date of meetings + */ + start?:string, + + /** + * End date of meetings + */ + end?:string +} + /** * Service for FedoCal */ @@ -196,20 +208,16 @@ export class FedoCalService { * 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 { - - /** - * 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 { + 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 @@ export class FedoCalService { * - end(ended today i.e past meetings) * @returns Observable which emits an array of meetings */ - getMeetings(calendar: Calendar, type: string): Observable { - return merge(this.loadCachedMeetings(calendar), this.fetchMeetings(calendar, type).pipe( + getMeetings(calendar: Calendar, options?:FetchMeetingOptions): Observable { + return merge(this.loadCachedMeetings(calendar), this.fetchMeetings(calendar, options).pipe( tap(x => this.storage.set(getCalendarStorageKey(calendar), x)) )); } diff --git a/src/utils.ts b/src/utils.ts index ab8342b..409377d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -107,3 +107,20 @@ export function getPackageType(pkgName:string) { return ''; } +export interface AsyncResult { + result?: T + error?: any +} + +/** + * Create a Promise never rejects + * + * @return wrapped promise that reports success or error through `resolve` + */ +export function unbreakablePromise(p:Promise): Promise> { + return new Promise((resolve, reject) => { + p.then(result => ({ result })) + .then(resolve) + .catch(error => resolve({ error })); + }); +}