/* * Copyright (C) 2021 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import {Injectable, OnDestroy} from '@angular/core'; import { DateSeriesRelevantData, dateSeriesRelevantKeys, formatRelevantKeys, ScheduleProvider, } from '../../calendar/schedule.provider'; import {SCDateSeries, SCThingType, SCUuid} from '@openstapps/core'; import {LocalNotifications} from '@capacitor/local-notifications'; import {ThingTranslateService} from '../../../translation/thing-translate.service'; import {DateFormatPipe, DurationPipe} from 'ngx-moment'; import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; import {StorageProvider} from '../../storage/storage.provider'; import {CalendarService} from '../../calendar/calendar.service'; import {flatMap} from 'lodash-es'; import {toICal} from '../../calendar/ical/ical'; import {Subscription} from 'rxjs'; import {ChangesOf} from './changes'; import {hashStringToInt} from './hash'; import { CALENDAR_NOTIFICATIONS_ENABLED_KEY, CALENDAR_SYNC_ENABLED_KEY, getCalendarSetting, } from '../../settings/page/calendar-sync-settings-keys'; import {filter} from 'rxjs/operators'; import {Capacitor} from '@capacitor/core'; @Injectable() export class ScheduleSyncService implements OnDestroy { constructor( private scheduleProvider: ScheduleProvider, private storageProvider: StorageProvider, private translator: ThingTranslateService, private dateFormatPipe: DateFormatPipe, private durationFormatPipe: DurationPipe, private calendar: CalendarService, ) {} init() { this.scheduleProvider.uuids$ .pipe(filter(uuids => uuids?.length > 0)) .subscribe(uuids => { this.uuids = uuids; void this.syncNativeCalendar(); }); } uuids: SCUuid[]; uuidSubscription: Subscription; ngOnDestroy() { this.uuidSubscription?.unsubscribe(); } private async isSyncEnabled(): Promise { return getCalendarSetting(this.storageProvider, CALENDAR_SYNC_ENABLED_KEY); } private async isNotificationsEnabled(): Promise { return getCalendarSetting( this.storageProvider, CALENDAR_NOTIFICATIONS_ENABLED_KEY, ); } async enable() { if (!Capacitor.isNativePlatform()) return; await BackgroundFetch.stop(); if ( [ this.isSyncEnabled.bind(this), this.isNotificationsEnabled.bind(this), ].some(async it => await it()) ) { const status = await BackgroundFetch.configure( { minimumFetchInterval: 15, stopOnTerminate: false, enableHeadless: true, }, async taskId => { await Promise.all([ this.postDifferencesNotification(), this.syncNativeCalendar(), ]); await BackgroundFetch.finish(taskId); }, ); if (status !== BackgroundFetch.STATUS_AVAILABLE) { if (status === BackgroundFetch.STATUS_DENIED) { console.error( 'The user explicitly disabled background behavior for this app or for the whole system.', ); } else if (status === BackgroundFetch.STATUS_RESTRICTED) { console.error( 'Background updates are unavailable and the user cannot enable them again.', ); } } else { console.info('Starting background fetch.'); await BackgroundFetch.start(); } } } async getDifferences(): Promise< ChangesOf[] > { const partialEvents = this.scheduleProvider.partialEvents$.getValue(); const result = ( await this.scheduleProvider.getDateSeries(partialEvents.map(it => it.uid)) ).dates; return result .map(it => ({ new: it, old: partialEvents.find(partialEvent => partialEvent.uid === it.uid), })) .map(it => ({ ...it, changes: it.old ? (Object.keys(it.old) as Array).filter( key => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion JSON.stringify(it.old![key]) !== JSON.stringify(it.new[key]), ) : dateSeriesRelevantKeys, })); } private formatChanges( changes: ChangesOf, ): string[] { return changes.changes.map( change => `${ this.translator.translator.translatedPropertyNames( SCThingType.DateSeries, )?.[change] }: ${formatRelevantKeys[change]( changes.new[change] as never, this.dateFormatPipe, this.durationFormatPipe, )}`, ); } async syncNativeCalendar() { if (!(await this.isSyncEnabled())) return; const dateSeries = (await this.scheduleProvider.getDateSeries(this.uuids)) .dates; const events = flatMap(dateSeries, event => toICal(event, this.translator.translator, { allowRRuleExceptions: false, excludeCancelledEvents: true, }), ); return this.calendar.syncEvents(events); } async postDifferencesNotification() { if (!(await this.isNotificationsEnabled())) return; const differences = (await this.getDifferences()).filter( it => it.changes.length > 0, ); if (differences.length === 0) return; if (Capacitor.isNativePlatform()) { await LocalNotifications.schedule({ notifications: differences.map(it => ({ title: it.new.event.name, body: this.formatChanges(it).join('\n'), id: hashStringToInt(it.new.uid), })), }); } else { // TODO: Implement desktop notifications } } }