Add anonymous and remote participations

This commit is contained in:
Thomas Citharel
2019-12-20 13:04:34 +01:00
parent 17e0b3968f
commit 2ed9050a90
135 changed files with 10141 additions and 2271 deletions

View File

@@ -1,5 +1,3 @@
import {ParticipantRole} from "@/types/event.model";
import {ParticipantRole} from "@/types/event.model";
<template>
<div class="container">
<b-loading :active.sync="$apollo.loading" />
@@ -7,7 +5,7 @@ import {ParticipantRole} from "@/types/event.model";
<div>
<div class="header-picture" v-if="event.picture" :style="`background-image: url('${event.picture.url}')`" />
<div class="header-picture-default" v-else />
<section>
<section class="section">
<div class="title-and-participate-button">
<div class="title-wrapper">
<div class="date-component">
@@ -33,18 +31,39 @@ import {ParticipantRole} from "@/types/event.model";
<small v-if="event.options.maximumAttendeeCapacity">
{{ $tc('All the places have already been taken', numberOfPlacesStillAvailable, { places: numberOfPlacesStillAvailable}) }}
</small>
<b-tooltip type="is-dark" v-if="!event.local" :label="$t('The actual number of participants may differ, as this event is hosted on another instance.')">
<b-icon size="is-small" icon="help-circle-outline" />
</b-tooltip>
</span>
</div>
</div>
<div class="event-participation has-text-right" v-if="new Date(endDate) > new Date()">
<participation-button
v-if="currentActor.id && !actorIsOrganizer && !event.draft && (eventCapacityOK || actorIsParticipant) && event.status !== EventStatus.CANCELLED"
v-if="anonymousParticipation === null && (config.anonymous.participation.allowed || (currentActor.id && !actorIsOrganizer && !event.draft && (eventCapacityOK || actorIsParticipant) && event.status !== EventStatus.CANCELLED))"
:participation="participations[0]"
:event="event"
:current-actor="currentActor"
@joinEvent="joinEvent"
@joinModal="isJoinModalActive = true"
@confirmLeave="confirmLeave"
/>
<b-button type="is-text" v-if="anonymousParticipation !== null" @click="cancelAnonymousParticipation">{{ $t('Cancel anonymous participation')}}</b-button>
<small v-if="anonymousParticipation">
{{ $t('You are participating in this event anonymously')}}
<b-tooltip :label="$t('This information is saved only on your computer. Click for details')">
<router-link :to="{ name: RouteName.TERMS }">
<b-icon size="is-small" icon="help-circle-outline" />
</router-link>
</b-tooltip>
</small>
<small v-else-if="anonymousParticipation === false">
{{ $t("You are participating in this event anonymously but didn't confirm participation")}}
<b-tooltip :label="$t('This information is saved only on your computer. Click for details')">
<router-link :to="{ name: RouteName.TERMS }">
<b-icon size="is-small" icon="help-circle-outline" />
</router-link>
</b-tooltip>
</small>
</div>
<div v-else>
<button class="button is-primary" type="button" slot="trigger" disabled>
@@ -68,7 +87,9 @@ import {ParticipantRole} from "@/types/event.model";
<b-tag type="is-info" v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</b-tag>
</span>
<span v-if="!event.local">
<b-tag type="is-primary">{{ event.organizerActor.domain }}</b-tag>
<a :href="event.url">
<b-tag type="is-primary">{{ event.organizerActor.domain }}</b-tag>
</a>
</span>
<router-link
v-if="event.tags && event.tags.length > 0"
@@ -165,7 +186,7 @@ import {ParticipantRole} from "@/types/event.model";
</div>
</div>
</section>
<div class="description" :class="{ exists: event.description }">
<section class="description section" :class="{ exists: event.description }">
<div class="description-container container">
<h3 class="title">
{{ $t('About this event') }}
@@ -178,14 +199,14 @@ import {ParticipantRole} from "@/types/event.model";
</div>
</div>
</div>
</div>
<section class="comments" ref="commentsObserver">
</section>
<section class="comments section" ref="commentsObserver">
<a href="#comments">
<h3 class="title" id="comments">{{ $t('Comments') }}</h3>
</a>
<comment-tree v-if="loadComments" :event="event" />
</section>
<section class="share" v-if="!event.draft">
<section class="share section" v-if="!event.draft">
<div class="container">
<div class="columns is-centered is-multiline">
<div class="column is-half-widescreen has-text-centered">
@@ -218,7 +239,7 @@ import {ParticipantRole} from "@/types/event.model";
</div>
</div>
</section>
<section class="more-events container" v-if="event.relatedEvents.length > 0">
<section class="more-events section container" v-if="event.relatedEvents.length > 0">
<h3 class="title has-text-centered">{{ $t('These events may interest you') }}</h3>
<div class="columns">
<div class="column is-one-third-desktop" v-for="relatedEvent in event.relatedEvents" :key="relatedEvent.uuid">
@@ -283,6 +304,14 @@ import { RouteName } from '@/router';
import { Address } from '@/types/address.model';
import CommentTree from '@/components/Comment/CommentTree.vue';
import 'intersection-observer';
import { CONFIG } from '@/graphql/config';
import {
AnonymousParticipationNotFoundError,
getLeaveTokenForParticipation,
isParticipatingInThisEvent,
removeAnonymousParticipation,
} from '@/services/AnonymousParticipationStorage';
import { IConfig } from '@/types/config.model';
@Component({
components: {
@@ -339,6 +368,7 @@ import 'intersection-observer';
return !this.currentActor || !this.event || !this.event.id || !this.currentActor.id;
},
},
config: CONFIG,
},
metaInfo() {
return {
@@ -360,6 +390,7 @@ export default class Event extends EventMixin {
event: IEvent = new EventModel();
currentActor!: IPerson;
identity: IPerson = new Person();
config!: IConfig;
participations: IParticipant[] = [];
oldParticipationRole!: String;
showMap: boolean = false;
@@ -370,6 +401,7 @@ export default class Event extends EventMixin {
RouteName = RouteName;
observer!: IntersectionObserver;
loadComments: boolean = false;
anonymousParticipation: boolean|null = null;
get eventTitle() {
if (!this.event) return undefined;
@@ -381,12 +413,22 @@ export default class Event extends EventMixin {
return this.event.description;
}
mounted() {
async mounted() {
this.identity = this.currentActor;
if (this.$route.hash.includes('#comment-')) {
this.loadComments = true;
}
try {
this.anonymousParticipation = await this.anonymousParticipationConfirmed();
} catch (e) {
if (e instanceof AnonymousParticipationNotFoundError) {
this.anonymousParticipation = null;
} else {
console.error(e);
}
}
this.observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry) {
@@ -529,63 +571,14 @@ export default class Event extends EventMixin {
cancelText: this.$t('Cancel') as string,
type: 'is-danger',
hasIcon: true,
onConfirm: () => this.leaveEvent(),
onConfirm: () => {
if (this.currentActor.id) {
this.leaveEvent(this.event, this.currentActor.id);
}
},
});
}
async leaveEvent() {
try {
const { data } = await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
mutation: LEAVE_EVENT,
variables: {
eventId: this.event.id,
actorId: this.currentActor.id,
},
update: (store, { data }) => {
if (data == null) return;
const participationCachedData = store.readQuery<{ person: IPerson }>({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, actorId: this.currentActor.id },
});
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error('Cannot update participation cache, because of null value.');
return;
}
const participation = person.participations[0];
person.participations = [];
store.writeQuery({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: this.event.id, actorId: this.currentActor.id },
data: { person },
});
const eventCachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
if (eventCachedData == null) return;
const { event } = eventCachedData;
if (event === null) {
console.error('Cannot update event cache, because of null value.');
return;
}
if (participation.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.notApproved = event.participantStats.notApproved - 1;
} else {
event.participantStats.going = event.participantStats.going - 1;
event.participantStats.participant = event.participantStats.participant - 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
},
});
if (data) {
this.participationCancelledMessage();
}
} catch (error) {
console.error(error);
}
}
@Watch('participations')
watchParticipations() {
if (this.participations.length > 0) {
@@ -624,10 +617,6 @@ export default class Event extends EventMixin {
this.$notifier.info(this.$t('Your participation status has been changed') as string);
}
private participationCancelledMessage() {
this.$notifier.success(this.$t('You have cancelled your participation') as string);
}
async downloadIcsEvent() {
const data = await (await fetch(`${GRAPHQL_API_ENDPOINT}/events/${this.uuid}/export/ics`)).text();
const blob = new Blob([data], { type: 'text/calendar' });
@@ -709,11 +698,30 @@ export default class Event extends EventMixin {
if (!this.event.physicalAddress) return null;
return new Address(this.event.physicalAddress);
}
async anonymousParticipationConfirmed(): Promise<boolean> {
return await isParticipatingInThisEvent(this.uuid);
}
async cancelAnonymousParticipation() {
const token = await getLeaveTokenForParticipation(this.uuid) as String;
await this.leaveEvent(this.event, this.config.anonymous.actorId, token);
await removeAnonymousParticipation(this.uuid);
this.anonymousParticipation = null;
}
}
</script>
<style lang="scss" scoped>
@import "../../variables";
.section {
padding: 1rem 1.5rem;
}
main > .container {
background: $white;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
@@ -821,14 +829,14 @@ export default class Event extends EventMixin {
div.title-and-participate-button {
display: flex;
flex-wrap: wrap;
// flex-wrap: wrap;
/*flex-flow: row wrap;*/
justify-content: space-between;
/*align-self: center;*/
align-items: stretch;
/*align-content: space-around;*/
padding: 7.5px 10px 0;
margin-bottom: 1rem;
margin-bottom: 0.5rem;
div.title-wrapper {
display: flex;
@@ -906,7 +914,7 @@ export default class Event extends EventMixin {
p.tags {
span {
&.tag {
margin: 0 2px 4px;
margin: 0 2px;
&.is-success {
&::before {
@@ -919,7 +927,7 @@ export default class Event extends EventMixin {
margin: auto 5px;
}
margin-bottom: 1rem;
//margin-bottom: 1rem;
}
h3.title {
@@ -927,7 +935,7 @@ export default class Event extends EventMixin {
}
.description {
padding: 10px 0;
//padding: 10px 0;
min-height: 7rem;
&.exists {
@@ -942,8 +950,8 @@ export default class Event extends EventMixin {
background-image: url('../../assets/texting.svg');
}
}
border-top: solid 1px #111;
border-bottom: solid 1px #111;
border-top: solid 1px lighten($primary, 60%);
border-bottom: solid 1px lighten($primary, 60%);
.description-content {
/deep/ h1 {
@@ -990,8 +998,6 @@ export default class Event extends EventMixin {
}
.comments {
margin: 1rem auto 2rem;
a h3#comments {
margin-bottom: 5px;
}