Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/components/Editor/Invitees/InviteesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
</div>

<InviteesListSearch
v-if="!isReadOnly && hasUserEmailAddress"
v-if="(!isReadOnly || canAddGuests) && hasUserEmailAddress"
:alreadyInvitedEmails="alreadyInvitedEmails"
:organizer="calendarObjectInstance.organizer"
@addAttendee="addAttendee" />
Expand All @@ -67,6 +67,7 @@
:key="invitee.email"
:attendee="invitee"
:isReadOnly="isReadOnly"
:canRemove="isForwardedByMe(invitee)"
:organizerDisplayName="organizerDisplayName"
:members="invitee.members"
:isViewedByOrganizer="isViewedByOrganizer"
Expand Down Expand Up @@ -122,6 +123,15 @@ export default {
required: true,
},

/**
* Whether the current user may add guests even though the list is
* otherwise read-only for them, because they are an attendee.
*/
canAddGuests: {
type: Boolean,
default: false,
},

isSharedWithMe: {
type: Boolean,
required: true,
Expand Down Expand Up @@ -165,6 +175,9 @@ export default {
creatingTalkRoom: false,
showFreeBusyModel: false,
recentAttendees: [],
// Guests added in this session, so they can be taken off again before
// saving. Once saved, only the organizer can remove an attendee.
forwardedAttendees: [],
}
},

Expand Down Expand Up @@ -424,6 +437,23 @@ export default {
member,
})
this.recentAttendees.push(email)
if (this.canAddGuests) {
this.forwardedAttendees.push(email)
}
},

/**
* Whether the given attendee was added by the current user and can
* therefore still be removed again.
*
* @param {object} attendee The attendee to check
* @return {boolean}
*/
isForwardedByMe(attendee) {
// Not once the editor switched to viewing, because the guest reached
// the organizer by then and only they can remove an attendee
return this.canAddGuests
&& this.forwardedAttendees.includes(removeMailtoPrefix(attendee.uri))
Comment on lines +452 to +456
},

removeAttendee(attendee) {
Expand All @@ -441,6 +471,7 @@ export default {
attendee,
})
this.recentAttendees = this.recentAttendees.filter((a) => a.uri !== attendee.email)
this.forwardedAttendees = this.forwardedAttendees.filter((email) => email !== removeMailtoPrefix(attendee.uri))

if (this.showFreeBusyModel && this.calendarObjectInstance.attendees.length === 0) {
showWarning(this.$t('calendar', 'Please add at least one attendee to use the "Find a time" feature.'))
Expand Down
19 changes: 19 additions & 0 deletions src/components/Editor/Invitees/InviteesListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
:size="20" />
</template>
</NcButton>
<NcButton
v-if="canRemove && !isViewedByOrganizer"
variant="tertiary"
:aria-label="removeAttendeeText"
:title="removeAttendeeText"
@click="removeAttendee(attendee)">
<template #icon>
<Delete :size="20" />
</template>
</NcButton>
<Actions v-if="!isReadOnly && isViewedByOrganizer">
<ActionCheckbox
v-if="!members.length"
Expand Down Expand Up @@ -160,6 +170,15 @@ export default {
required: true,
},

/**
* Whether this attendee can be removed even though the list is read-only,
* because the current user just added them.
*/
canRemove: {
type: Boolean,
default: false,
},

members: {
type: Array,
default: () => [],
Expand Down
36 changes: 36 additions & 0 deletions src/mixins/EditorMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { showError, showSuccess } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { mapState, mapStores } from 'pinia'
Expand Down Expand Up @@ -241,6 +242,41 @@ export default {
const organizer = this.calendarObjectInstance.organizer
return removeMailtoPrefix(organizer.uri) === principal
},
/**
* Returns whether the organizer let attendees invite guests to this event.
*
* X-NC-ALLOW-ATTENDEE-GUESTS has to be turned on explicitly, so it is also
* absent on invitations we could not report guests back to.
*
* @return {boolean}
*/
isAddingGuestsAllowed() {
// Our own server has to generate the reply carrying the guests
if (loadState('core', 'capabilities', {})?.dav?.attendee_guests !== true) {
return false
}

// isViewedByAttendee is false on read-only calendars and for non-attendees,
// but true for an organizer who also attends their own event
return this.isViewedByAttendee
&& this.isViewedByOrganizer !== true
&& this.calendarObjectInstance?.allowAttendeeGuests === 'TRUE'
},
/**
* Returns whether guests added here actually reach the organizer.
*
* The organizer only picks up guests of the base instance, so recurring
* events are left to them.
*
* @return {boolean}
*/
canAddGuests() {
// A recurrence exception is not the base instance, but it can not create
// one either, so both checks are needed
return this.isAddingGuestsAllowed
&& this.calendarObjectInstance?.isMasterItem === true
&& !this.canCreateRecurrenceException
},
/**
* Returns the attendee property corresponding to the current user
*
Expand Down
2 changes: 2 additions & 0 deletions src/mixins/PropertyMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* See inline for more documentation
*/

import AccountMultiplePlusOutline from 'vue-material-design-icons/AccountMultiplePlusOutline.vue'
import AccountPlusOutline from 'vue-material-design-icons/AccountPlusOutline.vue'
import Bell from 'vue-material-design-icons/BellOutline.vue'
import Briefcase from 'vue-material-design-icons/BriefcaseOutline.vue'
Expand All @@ -22,6 +23,7 @@ import TextBoxOutline from 'vue-material-design-icons/TextBoxOutline.vue'

export default {
components: {
AccountMultiplePlusOutline,
AccountPlusOutline,
Briefcase,
Check,
Expand Down
6 changes: 6 additions & 0 deletions src/models/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ function getDefaultEventObject(props = {}) {
attachments: [],
// Invitation forwarding
invitationForwarding: 'TRUE',
// Attendees inviting guests, off unless the organizer turned it on
allowAttendeeGuests: 'FALSE',
...props,
}
}
Expand Down Expand Up @@ -179,6 +181,10 @@ function mapEventComponentToEventObject(eventComponent) {
eventObject.invitationForwarding = eventComponent.getFirstPropertyFirstValue('X-NC-INVITATION-FORWARDING')
}

if (eventComponent.hasProperty('X-NC-ALLOW-ATTENDEE-GUESTS')) {
eventObject.allowAttendeeGuests = eventComponent.getFirstPropertyFirstValue('X-NC-ALLOW-ATTENDEE-GUESTS')
}

return eventObject
}

Expand Down
12 changes: 12 additions & 0 deletions src/store/calendarObjectInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,18 @@ export default defineStore('calendarObjectInstance', {
calendarObjectInstance.invitationForwarding = invitationForwarding
},

/**
* Change whether attendees of an event may invite guests
*
* @param {object} data The destructuring object
* @param {object} data.calendarObjectInstance The calendarObjectInstance object
* @param {string} data.allowAttendeeGuests Allow attendee guests value
*/
changeAllowAttendeeGuests({ calendarObjectInstance, allowAttendeeGuests }) {
calendarObjectInstance.eventComponent.updatePropertyWithValue('X-NC-ALLOW-ATTENDEE-GUESTS', allowAttendeeGuests)
calendarObjectInstance.allowAttendeeGuests = allowAttendeeGuests
},

/**
* Change the customized color of an event
*
Expand Down
64 changes: 64 additions & 0 deletions src/views/EditFull.vue
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@
:propModel="propInvitationForwarding"
:value="invitationForwarding"
@update:value="updateInvitationForwarding" />
<PropertySelect
v-if="showAllowAttendeeGuests"
:isReadOnly="isReadOnly || isRecurring"
:propModel="propAllowAttendeeGuests"
:value="allowAttendeeGuests"
@update:value="updateAllowAttendeeGuests" />
</div>
</div>

Expand Down Expand Up @@ -318,6 +324,7 @@
:calendar="selectedCalendar"
:calendarObjectInstance="calendarObjectInstance"
:isReadOnly="isReadOnly || isViewedByOrganizer === false"
:canAddGuests="canAddGuests"
:isSharedWithMe="isSharedWithMe"
:showHeader="true"
@updateDates="updateDates" />
Expand Down Expand Up @@ -345,6 +352,7 @@
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
import IconDelete from '@mdi/svg/svg/delete.svg?raw'
import { Parameter } from '@nextcloud/calendar-js'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import moment from '@nextcloud/moment'
import { generateUrl } from '@nextcloud/router'
Expand Down Expand Up @@ -476,6 +484,23 @@ export default {
info: t('calendar', 'Choose "Only invited attendees can respond" to prevent attendees from forwarding the invitation to others.'),
defaultValue: 'TRUE',
},

propAllowAttendeeGuests: {
// TRANSLATORS Setting of an event, deciding whether the people invited to it may invite further people
readableName: t('calendar', 'Allow attendees to invite guests'),
icon: 'AccountMultiplePlusOutline',
options: [
// TRANSLATORS A guest is someone invited by an attendee rather than by the organizer
{ value: 'TRUE', label: t('calendar', 'Attendees can invite guests') },
// TRANSLATORS "you" is the organizer of the event, who is reading this setting
{ value: 'FALSE', label: t('calendar', 'Only you can invite attendees') },
],

multiple: false,
// TRANSLATORS "you" is the organizer of the event, who is reading this setting
info: t('calendar', 'Attendees can add other people, who are then invited to the event just like the attendees you added yourself.'),
defaultValue: 'FALSE',
},
}
},

Expand Down Expand Up @@ -508,6 +533,10 @@ export default {
return this.calendarObjectInstance?.invitationForwarding ?? null
},

allowAttendeeGuests() {
return this.calendarObjectInstance?.allowAttendeeGuests ?? null
},

subTitle() {
if (!this.calendarObjectInstance) {
return ''
Expand Down Expand Up @@ -542,6 +571,29 @@ export default {
showInvitationForwarding() {
return isAfterVersion(34)
},

showAllowAttendeeGuests() {
return loadState('core', 'capabilities', {})?.dav?.attendee_guests === true
&& this.isViewedByOrganizer !== false
},

/**
* Guests are only reported to the organizer for the whole event, so they
* are not offered for a recurring one.
*
* @return {boolean}
*/
isRecurring() {
return this.calendarObjectInstance?.recurrenceRule?.frequency !== 'NONE'
},
},

watch: {
isRecurring(isRecurring) {
if (isRecurring && this.allowAttendeeGuests === 'TRUE') {
this.updateAllowAttendeeGuests('FALSE')
}
},
},

mounted() {
Expand Down Expand Up @@ -633,6 +685,18 @@ export default {
})
},

/**
* Updates whether attendees may invite guests
*
* @param {string} allowAttendeeGuests Allow attendee guests value
*/
updateAllowAttendeeGuests(allowAttendeeGuests) {
this.calendarObjectInstanceStore.changeAllowAttendeeGuests({
calendarObjectInstance: this.calendarObjectInstance,
allowAttendeeGuests,
})
},

/**
* Adds a category to the event
*
Expand Down
1 change: 1 addition & 0 deletions src/views/EditSimple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
:hideErrors="true"
:showHeader="true"
:isReadOnly="isReadOnlyOrViewing || isViewedByOrganizer === false"
:canAddGuests="canAddGuests && !isReadOnlyOrViewing"
:isSharedWithMe="isSharedWithMe"
:calendar="selectedCalendar"
:calendarObjectInstance="calendarObjectInstance" />
Expand Down
Loading
Loading