mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-22 01:22:54 +00:00
feat: calendar plugin
This commit is contained in:
committed by
Rainer Killinger
parent
080e6fa3e8
commit
a57c3029df
201
src/app/modules/background/schedule/schedule-sync.service.ts
Normal file
201
src/app/modules/background/schedule/schedule-sync.service.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
@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$.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),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user