Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2918 from nextcloud/enhancement/free-busy-colors
Improve free/busy colors
- Loading branch information
Showing
7 changed files
with
217 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
src/fullcalendar/eventSources/freeBusyBlockedForAllEventSource.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
/* | ||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* | ||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* | ||
* @license GNU AGPL version 3 or any later version | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as | ||
* published by the Free Software Foundation, either version 3 of the | ||
* License, or (at your option) any later version. | ||
* | ||
* 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 Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
import getTimezoneManager from '../../services/timezoneDataProviderService.js' | ||
import { createFreeBusyRequest, getParserManager } from 'calendar-js' | ||
import DateTimeValue from 'calendar-js/src/values/dateTimeValue.js' | ||
import { findSchedulingOutbox } from '../../services/caldavService.js' | ||
import logger from '../../utils/logger.js' | ||
// import AttendeeProperty from 'calendar-js/src/properties/attendeeProperty.js' | ||
|
||
/** | ||
* Returns an event source for free-busy | ||
* | ||
* @param {AttendeeProperty} organizer The organizer of the event | ||
* @param {AttendeeProperty[]} attendees Array of the event's attendees | ||
* @param {String[]} resources List of resources | ||
* @returns {{startEditable: boolean, resourceEditable: boolean, editable: boolean, id: string, durationEditable: boolean, events: events}} | ||
*/ | ||
export default function(organizer, attendees, resources) { | ||
const resourceIds = resources.map((resource) => resource.id) | ||
|
||
return { | ||
id: 'free-busy-free-for-all', | ||
editable: false, | ||
startEditable: false, | ||
durationEditable: false, | ||
resourceEditable: false, | ||
events: async({ | ||
start, | ||
end, | ||
timeZone, | ||
}, successCallback, failureCallback) => { | ||
console.debug('freeBusyBlockedForAllEventSource', start, end, timeZone) | ||
|
||
let timezoneObject = getTimezoneManager().getTimezoneForId(timeZone) | ||
if (!timezoneObject) { | ||
timezoneObject = getTimezoneManager().getTimezoneForId('UTC') | ||
logger.error(`FreeBusyEventSource: Timezone ${timeZone} not found, falling back to UTC.`) | ||
} | ||
|
||
const startDateTime = DateTimeValue.fromJSDate(start, true) | ||
const endDateTime = DateTimeValue.fromJSDate(end, true) | ||
|
||
// const organizerAsAttendee = new AttendeeProperty('ATTENDEE', organizer.email) | ||
const freeBusyComponent = createFreeBusyRequest(startDateTime, endDateTime, organizer, attendees) | ||
const freeBusyICS = freeBusyComponent.toICS() | ||
|
||
let outbox | ||
try { | ||
outbox = await findSchedulingOutbox() | ||
} catch (error) { | ||
failureCallback(error) | ||
return | ||
} | ||
|
||
let freeBusyData | ||
try { | ||
freeBusyData = await outbox.freeBusyRequest(freeBusyICS) | ||
} catch (error) { | ||
failureCallback(error) | ||
return | ||
} | ||
|
||
const slots = [] | ||
for (const [, data] of Object.entries(freeBusyData)) { | ||
if (!data.success) { | ||
continue | ||
} | ||
|
||
const parserManager = getParserManager() | ||
const parser = parserManager.getParserForFileType('text/calendar') | ||
parser.parse(data.calendarData) | ||
|
||
// TODO: fix me upstream, parser only exports VEVENT, VJOURNAL and VTODO at the moment | ||
const calendarComponent = parser._calendarComponent | ||
const freeBusyComponent = calendarComponent.getFirstComponent('VFREEBUSY') | ||
if (!freeBusyComponent) { | ||
continue | ||
} | ||
|
||
for (const freeBusyProperty of freeBusyComponent.getPropertyIterator('FREEBUSY')) { | ||
if (freeBusyProperty.type === 'FREE') { | ||
// We care about anything BUT free slots | ||
continue | ||
} | ||
|
||
slots.push({ | ||
start: freeBusyProperty.getFirstValue().start.getInTimezone(timezoneObject).jsDate, | ||
end: freeBusyProperty.getFirstValue().end.getInTimezone(timezoneObject).jsDate, | ||
}) | ||
} | ||
} | ||
|
||
// Now that we have all the busy slots we try to combine them to iron | ||
// out any overlaps between them. | ||
// The algorithm below will sort the slots by their start time ane then | ||
// iteratively collapse anything that starts and stops within the same | ||
// time. The complexity of this algorithms is n^2, but assuming the | ||
// number of attendees of an event is relatively low, this should be | ||
// fine to calculate. | ||
slots.sort((a, b) => a.start - b.start) | ||
const slotsWithoutOverlap = [] | ||
if (slots.length) { | ||
let currentSlotStart = slots[0].start | ||
slots.forEach(() => { | ||
const combined = findNextCombinedSlot(slots, currentSlotStart) | ||
if (combined) { | ||
slotsWithoutOverlap.push(combined) | ||
currentSlotStart = combined.end | ||
} | ||
}) | ||
} | ||
console.debug('deduplicated slots', slots, slotsWithoutOverlap) | ||
|
||
const events = slotsWithoutOverlap.map(slot => { | ||
return { | ||
groupId: 'free-busy-blocked-for-all', | ||
start: slot.start.toISOString(), | ||
end: slot.end.toISOString(), | ||
resourceIds: resourceIds, | ||
display: 'background', | ||
allDay: false, | ||
backgroundColor: 'var(--color-text-maxcontrast)', | ||
borderColor: 'var(--color-text-maxcontrast)', | ||
} | ||
}) | ||
|
||
console.debug('freeBusyBlockedForAllEventSource', slots, events) | ||
|
||
successCallback(events) | ||
}, | ||
} | ||
} | ||
|
||
function findNextCombinedSlot(slots, start) { | ||
const slot = slots | ||
.filter(slot => slot.start > start) | ||
.reduce((combined, slot) => { | ||
if (slot.start < combined.start) { | ||
// This slot starts too early | ||
return combined | ||
} | ||
|
||
if (slot.end <= combined.end) { | ||
// This slots starts and ends within the combined one | ||
return combined | ||
} | ||
|
||
// The slot is extended | ||
return { | ||
start: combined.start, | ||
end: slot.end, | ||
} | ||
}, { | ||
start, | ||
end: start, | ||
}) | ||
|
||
if (slot.start === slot.end) { | ||
// Empty -> no slot | ||
return undefined | ||
} | ||
|
||
return slot | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters