Files
openstapps/src/app/modules/background/schedule/schedule-sync.service.ts
2022-02-10 16:21:35 +01:00

205 lines
6.2 KiB
TypeScript

/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {Injectable, OnDestroy} from '@angular/core';
import {
DateSeriesRelevantData,
dateSeriesRelevantKeys,
formatRelevantKeys,
ScheduleProvider,
} from '../../calendar/schedule.provider';
import {SCDateSeries, SCThingType, SCUuid} from '@openstapps/core';
import {Device} from '@capacitor/device';
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,
CALENDAR_SYNC_SETTINGS_KEY,
} from '../../settings/page/calendar-sync-settings-keys';
import {filter} from 'rxjs/operators';
@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,
) {
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<boolean> {
return await this.storageProvider.get(
`${CALENDAR_SYNC_SETTINGS_KEY}.${CALENDAR_SYNC_ENABLED_KEY}`,
);
}
private async isNotificationsEnabled(): Promise<boolean> {
return await this.storageProvider.get(
`${CALENDAR_SYNC_SETTINGS_KEY}.${CALENDAR_NOTIFICATIONS_ENABLED_KEY}`,
);
}
async enable() {
if ((await Device.getInfo()).platform === 'web') return;
await BackgroundFetch.stop();
if (
[this.isSyncEnabled, this.isNotificationsEnabled].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<SCDateSeries, DateSeriesRelevantData>[]
> {
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<keyof DateSeriesRelevantData>).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<SCDateSeries, DateSeriesRelevantData>,
): string[] {
return changes.changes.map(
change =>
`${
this.translator.translator.translatedPropertyNames<SCDateSeries>(
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 ((await Device.getInfo()).platform === 'web') {
// TODO: Implement web notification
} else {
await LocalNotifications.schedule({
notifications: differences.map(it => ({
title: it.new.event.name,
body: this.formatChanges(it).join('\n'),
id: hashStringToInt(it.new.uid),
})),
});
}
}
}