Add ability to add message for participation and improve participation
management interface Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
130a3cf23f
commit
c732ec7f87
@ -16,8 +16,9 @@ A minimal file template [is available](https://framagit.org/framasoft/mobilizon/
|
||||
Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/master/support/systemd/mobilizon.service).
|
||||
|
||||
### Added
|
||||
- Possibility to participate anonymously to an event
|
||||
- Possibility to participate to an event without an account
|
||||
- Possibility to participate to a remote event (being redirected by providing federated identity)
|
||||
- Possibility to add a note as a participant when event participation is manually validated (required when participating without an account)
|
||||
- Possibility to change email address for the account
|
||||
- Possibility to delete your account
|
||||
|
||||
@ -26,6 +27,7 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
|
||||
- Signature validation also now checks if `Date` header has acceptable values
|
||||
- Actor profiles are now stale after two days and have to be refetched
|
||||
- Actor keys are rotated some time after sending a `Delete` activity
|
||||
- Improved event participations managing interface
|
||||
|
||||
### Fixed
|
||||
- Fixed URL search
|
||||
|
@ -21,7 +21,8 @@ Supported Activity | Supported Object
|
||||
`Create` | `Note`, `Event`
|
||||
`Delete` | `Object`
|
||||
`Flag` | `Object`
|
||||
`Follow` | `Object`
|
||||
`Follow` | `Object`
|
||||
`Join` | `Event`
|
||||
`Reject` | `Follow`, `Join`
|
||||
`Remove` | `Note`, `Event`
|
||||
`Undo` | `Announce`, `Follow`
|
||||
@ -155,4 +156,28 @@ We add [an `address` property](https://schema.org/address), which we assume to b
|
||||
},
|
||||
"type": "Event"
|
||||
}
|
||||
```
|
||||
|
||||
### Join
|
||||
|
||||
#### participationMessage
|
||||
|
||||
We add a `participationMessage` property on a `Join` activity so that participants may transmit a note to event organizers, to motivate their participation when event participations are manually approved. This field is restricted to plain text.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Join",
|
||||
"object": "http://mobilizon.test/events/some-uuid",
|
||||
"id": "http://mobilizon2.test/@admin/join/event/1",
|
||||
"actor": "http://mobilizon2.test/@admin",
|
||||
"participationMessage": "I want to join !",
|
||||
"@context": [
|
||||
{
|
||||
"participationMessage": {
|
||||
"@id": "mz:participationMessage",
|
||||
"@type": "sc:Text"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@ -6,7 +6,6 @@
|
||||
:loading="$apollo.queries.relayFollowers.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {EventJoinOptions} from "@/types/event.model";
|
||||
<docs>
|
||||
A button to set your participation
|
||||
|
||||
@ -80,7 +81,7 @@ A button to set your participation
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<span>{{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
||||
<span>{{ $t('as {identity}', {identity: currentActor.name || `@${currentActor.preferredUsername}` }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
@ -96,14 +97,13 @@ A button to set your participation
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { EventJoinOptions, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
|
||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { FETCH_EVENT } from '@/graphql/event';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -132,7 +132,11 @@ export default class ParticipationButton extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
joinEvent(actor: IPerson) {
|
||||
this.$emit('joinEvent', actor);
|
||||
if (this.event.joinOptions === EventJoinOptions.RESTRICTED) {
|
||||
this.$emit('joinEventWithConfirmation', actor);
|
||||
} else {
|
||||
this.$emit('joinEvent', actor);
|
||||
}
|
||||
}
|
||||
|
||||
joinModal() {
|
||||
|
164
js/src/components/Event/ParticipationTable.vue
Normal file
164
js/src/components/Event/ParticipationTable.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<b-table
|
||||
:data="data"
|
||||
ref="queueTable"
|
||||
detailed
|
||||
detail-key="id"
|
||||
:checked-rows.sync="checkedRows"
|
||||
checkable
|
||||
:is-row-checkable="row => row.role !== ParticipantRole.CREATOR"
|
||||
checkbox-position="left"
|
||||
default-sort="insertedAt"
|
||||
default-sort-direction="asc"
|
||||
:show-detail-icon="false"
|
||||
:loading="this.$apollo.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="total"
|
||||
:per-page="perPage"
|
||||
backend-sorting
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="page => $emit('page-change', page)"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="insertedAt" :label="$t('Date')" sortable>
|
||||
<b-tag type="is-success" class="has-text-centered">{{ props.row.insertedAt | formatDateString }}<br>{{ props.row.insertedAt | formatTimeString }}</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')" sortable v-if="showRole">
|
||||
<span v-if="props.row.role === ParticipantRole.CREATOR">
|
||||
{{ $t('Organizer') }}
|
||||
</span>
|
||||
<span v-else-if="props.row.role === ParticipantRole.PARTICIPANT">
|
||||
{{ $t('Participant') }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
<b-table-column field="actor.preferredUsername" :label="$t('Participant')" sortable>
|
||||
<article class="media">
|
||||
<figure class="media-left" v-if="props.row.actor.avatar">
|
||||
<p class="image is-48x48">
|
||||
<img :src="props.row.actor.avatar.url" alt="">
|
||||
</p>
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else-if="props.row.actor.preferredUsername === 'anonymous'" size="is-large" icon="incognito" />
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="props.row.actor.preferredUsername !== 'anonymous'">
|
||||
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span><br />
|
||||
<span class="is-size-7 has-text-grey">@{{ props.row.actor.preferredUsername }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('Anonymous participant') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="metadata.message" :label="$t('Message')">
|
||||
<span @click="toggleQueueDetails(props.row)" :class="{ 'ellipsed-message': props.row.metadata.message.length > MESSAGE_ELLIPSIS_LENGTH }" v-if="props.row.metadata && props.row.metadata.message">
|
||||
{{ props.row.metadata.message | ellipsize }}
|
||||
</span>
|
||||
<span v-else class="has-text-grey">
|
||||
{{ $t('No message') }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article v-html="nl2br(props.row.metadata.message)" />
|
||||
</template>
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button @click="acceptParticipants(checkedRows)" type="is-success" v-if="canAcceptParticipants">
|
||||
{{ $tc('No participant to approve|Approve participant|Approve {number} participants', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
<b-button @click="refuseParticipants(checkedRows)" type="is-danger" v-if="canRefuseParticipants">
|
||||
{{ $tc('No participant to reject|Reject participant|Reject {number} participants', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { Refs } from '@/shims-vue';
|
||||
import { nl2br } from '@/utils/html';
|
||||
import { asyncForEach } from '@/utils/asyncForEach';
|
||||
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
|
||||
},
|
||||
})
|
||||
export default class ParticipationTable extends Vue {
|
||||
@Prop({ required: true, type: Array }) data!: IParticipant[];
|
||||
@Prop({ required: true, type: Number }) total!: number;
|
||||
@Prop({ required: true, type: Function }) acceptParticipant;
|
||||
@Prop({ required: true, type: Function }) refuseParticipant;
|
||||
@Prop({ required: false, type: Boolean, default: false }) showRole;
|
||||
@Prop({ required: false, type: Number, default: 20 }) perPage;
|
||||
checkedRows: IParticipant[] = [];
|
||||
|
||||
MESSAGE_ELLIPSIS_LENGTH = MESSAGE_ELLIPSIS_LENGTH;
|
||||
nl2br = nl2br;
|
||||
ParticipantRole = ParticipantRole;
|
||||
|
||||
$refs!: Refs<{
|
||||
queueTable: any,
|
||||
}>;
|
||||
|
||||
toggleQueueDetails(row: IParticipant) {
|
||||
if (row.metadata.message && row.metadata.message.length < MESSAGE_ELLIPSIS_LENGTH) return;
|
||||
this.$refs.queueTable.toggleDetails(row);
|
||||
}
|
||||
|
||||
async acceptParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.acceptParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async refuseParticipants(participants: IParticipant[]) {
|
||||
await asyncForEach(participants, async (participant: IParticipant) => {
|
||||
await this.refuseParticipant(participant);
|
||||
});
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* We can accept participants if at least one of them is not approved
|
||||
*/
|
||||
get canAcceptParticipants(): boolean {
|
||||
return this.checkedRows.some(
|
||||
(participant: IParticipant) => [ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(participant.role),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* We can refuse participants if at least one of them is something different than not approved
|
||||
*/
|
||||
get canRefuseParticipants(): boolean {
|
||||
return this.checkedRows.some((participant: IParticipant) => participant.role !== ParticipantRole.REJECTED);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ellipsed-message {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table {
|
||||
span.tag {
|
||||
height: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -29,7 +29,7 @@
|
||||
<figure class="image is-32x32" v-if="identity.avatar">
|
||||
<img class="is-rounded" :src="identity.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else icon="account-circle" />
|
||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||
</div>
|
||||
|
||||
<div class="media-content">
|
||||
@ -180,6 +180,10 @@ nav {
|
||||
background: $secondary;
|
||||
}
|
||||
|
||||
span.icon.is-medium {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 2.5em;
|
||||
}
|
||||
|
@ -7,18 +7,24 @@
|
||||
<b-message type="is-info">{{ $t("Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.") }}</b-message>
|
||||
<b-message type="is-danger" v-if="error">{{ error }}</b-message>
|
||||
<b-field :label="$t('Email')">
|
||||
<b-field>
|
||||
<b-input
|
||||
type="email"
|
||||
v-model="anonymousParticipation.email"
|
||||
placeholder="Your email"
|
||||
required>
|
||||
</b-input>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Send email') }}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
<b-input
|
||||
type="email"
|
||||
v-model="anonymousParticipation.email"
|
||||
placeholder="Your email"
|
||||
required>
|
||||
</b-input>
|
||||
</b-field>
|
||||
<p v-if="event.joinOptions === EventJoinOptions.RESTRICTED">{{ $t("The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.") }}</p>
|
||||
<p v-else>{{ $t("If you want, you may send a message to the event organizer here.") }}</p>
|
||||
<b-field :label="$t('Message')">
|
||||
<b-input
|
||||
type="textarea"
|
||||
v-model="anonymousParticipation.message"
|
||||
minlength="10"
|
||||
:required="event.joinOptions === EventJoinOptions.RESTRICTED">
|
||||
</b-input>
|
||||
</b-field>
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Send email') }}</b-button>
|
||||
<div class="has-text-centered">
|
||||
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">
|
||||
{{ $t('Back to previous page') }}
|
||||
@ -31,7 +37,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { EventModel, IEvent, IParticipant, ParticipantRole, EventJoinOptions } from '@/types/event.model';
|
||||
import { FETCH_EVENT, JOIN_EVENT } from '@/graphql/event';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
@ -55,10 +61,11 @@ import { RouteName } from '@/router';
|
||||
})
|
||||
export default class ParticipationWithoutAccount extends Vue {
|
||||
@Prop({ type: String, required: true }) uuid!: string;
|
||||
anonymousParticipation: { email: String } = { email: '' };
|
||||
anonymousParticipation: { email: String, message: String } = { email: '', message: '' };
|
||||
event!: IEvent;
|
||||
config!: IConfig;
|
||||
error: String|boolean = false;
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
|
||||
async joinEvent() {
|
||||
this.error = false;
|
||||
@ -69,6 +76,7 @@ export default class ParticipationWithoutAccount extends Vue {
|
||||
eventId: this.event.id,
|
||||
actorId: this.config.anonymous.actorId,
|
||||
email: this.anonymousParticipation.email,
|
||||
message: this.anonymousParticipation.message,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
|
@ -2,23 +2,28 @@ import gql from 'graphql-tag';
|
||||
import { COMMENT_FIELDS_FRAGMENT } from '@/graphql/comment';
|
||||
|
||||
const participantQuery = `
|
||||
role,
|
||||
id,
|
||||
actor {
|
||||
preferredUsername,
|
||||
avatar {
|
||||
url
|
||||
total,
|
||||
elements {
|
||||
role,
|
||||
id,
|
||||
actor {
|
||||
preferredUsername,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
name,
|
||||
id,
|
||||
domain
|
||||
},
|
||||
name,
|
||||
id,
|
||||
domain
|
||||
},
|
||||
event {
|
||||
id,
|
||||
uuid
|
||||
},
|
||||
metadata {
|
||||
cancellationToken
|
||||
event {
|
||||
id,
|
||||
uuid
|
||||
},
|
||||
metadata {
|
||||
cancellationToken,
|
||||
message
|
||||
},
|
||||
insertedAt
|
||||
}
|
||||
`;
|
||||
|
||||
@ -371,11 +376,12 @@ export const EDIT_EVENT = gql`
|
||||
`;
|
||||
|
||||
export const JOIN_EVENT = gql`
|
||||
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String) {
|
||||
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String, $message: String) {
|
||||
joinEvent(
|
||||
eventId: $eventId,
|
||||
actorId: $actorId,
|
||||
email: $email
|
||||
email: $email,
|
||||
message: $message
|
||||
) {
|
||||
${participantQuery}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@
|
||||
"Allow all comments": "Allow all comments",
|
||||
"Allow registrations": "Allow registrations",
|
||||
"An error has occurred.": "An error has occurred.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "And no anonymous participations|And one anonymous participation|And {count} anonymous participations",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymous participants will be asked to confirm their participation through e-mail.",
|
||||
"Anonymous participations": "Anonymous participations",
|
||||
"Approve": "Approve",
|
||||
@ -467,5 +466,17 @@
|
||||
"{count} requests waiting": "{count} requests waiting",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks",
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors"
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors",
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.",
|
||||
"If you want, you may send a message to the event organizer here.": "If you want, you may send a message to the event organizer here.",
|
||||
"Message": "Message",
|
||||
"Anonymous participant": "Anonymous participant",
|
||||
"No message": "No message",
|
||||
"No participant to approve|Approve participant|Approve {number} participants": "No participant to approve|Approve participant|Approve {number} participants",
|
||||
"No participant to reject|Reject participant|Reject {number} participants": "No participant to reject|Reject participant|Reject {number} participants",
|
||||
"Role": "Role",
|
||||
"Participant": "Participant",
|
||||
"Participation confirmation": "Participation confirmation",
|
||||
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?",
|
||||
"Confirm my participation": "Confirm my participation"
|
||||
}
|
@ -23,7 +23,6 @@
|
||||
"Allow all comments": "Permitir todos los comentarios",
|
||||
"Allow registrations": "Permitir registros",
|
||||
"An error has occurred.": "Se ha producido un error.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "Y sin participaciones anónimas|Y una participación anónima|Y {count} participaciones anónimas",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Los participantes anónimos deberán confirmar su participación por correo electrónico.",
|
||||
"Anonymous participations": "Participaciones anónimas",
|
||||
"Approve": "Aprobar",
|
||||
|
@ -22,7 +22,6 @@
|
||||
"Allow all comments": "Salli kaikki kommentit",
|
||||
"Allow registrations": "Salli rekisteröityminen",
|
||||
"An error has occurred.": "Tapahtui virhe.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "Ei anonyymejä osallistujia|Myös yksi anonyymi osallistuja|Myös {count} anonyymiä osallistujaa",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonyymejä osallistujia pyydetään vahvistamaan osallistumisensa sähköpostitse.",
|
||||
"Anonymous participations": "Anonyymit osallistujat",
|
||||
"Approve": "Hyväksy",
|
||||
|
@ -23,7 +23,6 @@
|
||||
"Allow all comments": "Autoriser tous les commentaires",
|
||||
"Allow registrations": "Autoriser les inscriptions",
|
||||
"An error has occurred.": "Une erreur est survenue.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "Et aucune participation anonyme|Et une participation anonyme|Et {count} participations anonymes",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par email.",
|
||||
"Anonymous participations": "Participations anonymes",
|
||||
"Approve": "Approuver",
|
||||
@ -252,14 +251,14 @@
|
||||
"Or": "Ou",
|
||||
"Organized": "Organisés",
|
||||
"Organized by {name}": "Organisé par {name}",
|
||||
"Organizer": "Organisateur",
|
||||
"Organizer": "Organisateur⋅ice",
|
||||
"Other software may also support this.": "D'autres logiciels peuvent également supporter cette fonctionnalité.",
|
||||
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
|
||||
"Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
|
||||
"Page not found": "Page non trouvée",
|
||||
"Participant already was rejected.": "Le participant a déjà été refusé.",
|
||||
"Participant has already been approved as participant.": "Le participant a déjà été approuvé en tant que participant.",
|
||||
"Participants": "Participants",
|
||||
"Participants": "Participant⋅e⋅s",
|
||||
"Participate": "Participer",
|
||||
"Participate using your email address": "Participer en utilisant votre adresse email",
|
||||
"Participation approval": "Validation des participations",
|
||||
@ -473,5 +472,17 @@
|
||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "L'organisateur⋅ice de l'événement valide les participations manuellement. Comme vous avez choisi de participer sans compte, merci d'expliquer pourquoi vous voulez participer à cet événement.",
|
||||
"If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur⋅ice de l'événement ci-dessous.",
|
||||
"Message": "Message",
|
||||
"Anonymous participant": "Participant⋅e anonyme",
|
||||
"No message": "Pas de message",
|
||||
"No participant to approve|Approve participant|Approve {number} participants": "Aucun⋅e participant⋅e à valider|Valider le ou la participant⋅e|Valider {number} participant⋅es",
|
||||
"No participant to reject|Reject participant|Reject {number} participants": "Aucun⋅e participant⋅e à refuser|Refuser le ou la participant⋅e|Refuser {number} participant⋅es",
|
||||
"Role": "Rôle",
|
||||
"Participant": "Participant⋅e",
|
||||
"Participation confirmation": "Confirmation de votre participation",
|
||||
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "L'organisateur⋅ice de l'événement a choisi de valider manuellement les demandes de participation. Voulez-vous ajouter un petit message pour expliquer pourquoi vous souhaitez participer à cet événement ?",
|
||||
"Confirm my participation": "Confirmer ma participation"
|
||||
}
|
||||
|
@ -25,7 +25,6 @@
|
||||
"Allow all comments": "Autorizar totes los comentaris",
|
||||
"Allow registrations": "Permetre las inscripcions",
|
||||
"An error has occurred.": "Una error s’es producha.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "E cap de participacion anonima|E una participacion anonima|E {count} participacions anonima",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Òm demandarà als participants anonims de confirmar lor venguda via un corrièl.",
|
||||
"Anonymous participations": "Participacions anonimas",
|
||||
"Approve": "Aprovar",
|
||||
|
@ -22,7 +22,6 @@
|
||||
"Allow all comments": "Permitir todos comentários",
|
||||
"Allow registrations": "Permitir inscrições",
|
||||
"An error has occurred.": "Ocorreu um erro.",
|
||||
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "E nenhuma participação anônima|E uma participação anônima|E {count} participações anônimas",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Os participantes anônimos deverão confirmar sua participação por email.",
|
||||
"Anonymous participations": "Participações anônimas",
|
||||
"Approve": "Aprovar",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import { ColorModifiers } from 'buefy/types/helpers';
|
||||
import { Route, RawLocation } from 'vue-router';
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
@ -8,6 +9,23 @@ declare module 'vue/types/vue' {
|
||||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
};
|
||||
beforeRouteEnter?(
|
||||
to: Route,
|
||||
from: Route,
|
||||
next: (to?: RawLocation | false | ((vm: Vue) => void)) => void,
|
||||
): void;
|
||||
|
||||
beforeRouteLeave?(
|
||||
to: Route,
|
||||
from: Route,
|
||||
next: (to?: RawLocation | false | ((vm: Vue) => void)) => void,
|
||||
): void;
|
||||
|
||||
beforeRouteUpdate?(
|
||||
to: Route,
|
||||
from: Route,
|
||||
next: (to?: RawLocation | false | ((vm: Vue) => void)) => void,
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ export const eventRoutes: RouteConfig[] = [
|
||||
props: { isUpdate: true },
|
||||
},
|
||||
{
|
||||
path: '/events/participations/:eventId',
|
||||
path: '/events/:eventId/participations',
|
||||
name: EventRouteName.PARTICIPATIONS,
|
||||
component: participations,
|
||||
meta: { requiredAuth: true },
|
||||
|
@ -3,6 +3,7 @@ import { Address, IAddress } from '@/types/address.model';
|
||||
import { ITag } from '@/types/tag.model';
|
||||
import { IPicture } from '@/types/picture.model';
|
||||
import { IComment } from '@/types/comment.model';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
|
||||
export enum EventStatus {
|
||||
TENTATIVE = 'TENTATIVE',
|
||||
@ -59,7 +60,8 @@ export interface IParticipant {
|
||||
role: ParticipantRole;
|
||||
actor: IActor;
|
||||
event: IEvent;
|
||||
metadata: { cancellationToken?: string };
|
||||
metadata: { cancellationToken?: string, message?: string };
|
||||
insertedAt?: Date;
|
||||
}
|
||||
|
||||
export class Participant implements IParticipant {
|
||||
@ -68,6 +70,7 @@ export class Participant implements IParticipant {
|
||||
actor!: IActor;
|
||||
role: ParticipantRole = ParticipantRole.NOT_APPROVED;
|
||||
metadata = {};
|
||||
insertedAt?: Date;
|
||||
|
||||
constructor(hash?: IParticipant) {
|
||||
if (!hash) return;
|
||||
@ -77,6 +80,7 @@ export class Participant implements IParticipant {
|
||||
this.actor = new Actor(hash.actor);
|
||||
this.role = hash.role;
|
||||
this.metadata = hash.metadata;
|
||||
this.insertedAt = hash.insertedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +136,7 @@ export interface IEvent {
|
||||
organizerActor?: IActor;
|
||||
attributedTo: IActor;
|
||||
participantStats: IEventParticipantStats;
|
||||
participants: IParticipant[];
|
||||
participants: Paginate<IParticipant>;
|
||||
|
||||
relatedEvents: IEvent[];
|
||||
comments: IComment[];
|
||||
@ -205,7 +209,7 @@ export class EventModel implements IEvent {
|
||||
publishAt = new Date();
|
||||
|
||||
participantStats = { notApproved: 0, notConfirmed: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
|
||||
participants: IParticipant[] = [];
|
||||
participants!: Paginate<IParticipant>;
|
||||
|
||||
relatedEvents: IEvent[] = [];
|
||||
comments: IComment[] = [];
|
||||
|
7
js/src/utils/asyncForEach.ts
Normal file
7
js/src/utils/asyncForEach.ts
Normal file
@ -0,0 +1,7 @@
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index += 1) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
export { asyncForEach };
|
@ -45,6 +45,7 @@
|
||||
:current-actor="currentActor"
|
||||
@joinEvent="joinEvent"
|
||||
@joinModal="isJoinModalActive = true"
|
||||
@joinEventWithConfirmation="joinEventWithConfirmation"
|
||||
@confirmLeave="confirmLeave"
|
||||
/>
|
||||
<b-button type="is-text" v-if="anonymousParticipation !== null" @click="cancelAnonymousParticipation">{{ $t('Cancel anonymous participation')}}</b-button>
|
||||
@ -263,13 +264,44 @@
|
||||
<button
|
||||
class="button is-primary"
|
||||
ref="confirmButton"
|
||||
@click="joinEvent(identity)">
|
||||
@click="event.joinOptions === EventJoinOptions.RESTRICTED ? joinEventWithConfirmation(identity) : joinEvent(identity)">
|
||||
{{ $t('Confirm my particpation') }}
|
||||
</button>
|
||||
</footer>
|
||||
</template>
|
||||
</identity-picker>
|
||||
</b-modal>
|
||||
<b-modal :active.sync="isJoinConfirmationModalActive" has-modal-card ref="joinConfirmationModal">
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t('Participation confirmation')}}</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p>{{ $t('The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?') }}</p>
|
||||
<form @submit.prevent="joinEvent(actorForConfirmation, messageForConfirmation)">
|
||||
<b-field :label="$t('Message')">
|
||||
<b-input
|
||||
type="textarea"
|
||||
size="is-medium"
|
||||
v-model="messageForConfirmation"
|
||||
minlength="10">
|
||||
</b-input>
|
||||
</b-field>
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
native-type="button"
|
||||
class="button"
|
||||
ref="cancelButton"
|
||||
@click="isJoinConfirmationModalActive = false">
|
||||
{{ $t('Cancel') }}
|
||||
</b-button>
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Confirm my participation') }}</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -281,11 +313,10 @@ import {
|
||||
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
||||
FETCH_EVENT,
|
||||
JOIN_EVENT,
|
||||
LEAVE_EVENT,
|
||||
} from '@/graphql/event';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { EventModel, EventStatus, EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { EventModel, EventStatus, EventVisibility, IEvent, IParticipant, ParticipantRole, EventJoinOptions } from '@/types/event.model';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
|
||||
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
|
||||
@ -398,12 +429,16 @@ export default class Event extends EventMixin {
|
||||
showMap: boolean = false;
|
||||
isReportModalActive: boolean = false;
|
||||
isJoinModalActive: boolean = false;
|
||||
isJoinConfirmationModalActive: boolean = false;
|
||||
EventVisibility = EventVisibility;
|
||||
EventStatus = EventStatus;
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
RouteName = RouteName;
|
||||
observer!: IntersectionObserver;
|
||||
loadComments: boolean = false;
|
||||
anonymousParticipation: boolean|null = null;
|
||||
actorForConfirmation!: IPerson;
|
||||
messageForConfirmation: string = '';
|
||||
|
||||
get eventTitle() {
|
||||
if (!this.event) return undefined;
|
||||
@ -506,7 +541,13 @@ export default class Event extends EventMixin {
|
||||
}
|
||||
}
|
||||
|
||||
async joinEvent(identity: IPerson) {
|
||||
joinEventWithConfirmation(actor: IPerson) {
|
||||
this.isJoinConfirmationModalActive = true;
|
||||
this.actorForConfirmation = actor;
|
||||
}
|
||||
|
||||
async joinEvent(identity: IPerson, message: string|null = null) {
|
||||
this.isJoinConfirmationModalActive = false;
|
||||
this.isJoinModalActive = false;
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
||||
@ -514,6 +555,7 @@ export default class Event extends EventMixin {
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
actorId: identity.id,
|
||||
message,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {ParticipantRole} from "@/types/event.model";
|
||||
<template>
|
||||
<main class="container">
|
||||
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
|
||||
@ -7,22 +8,17 @@
|
||||
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="participantsAndCreators.length > 0">
|
||||
<section v-if="participants && participants.total > 0">
|
||||
<h2 class="title">{{ $t('Participants') }}</h2>
|
||||
<p v-if="confirmedAnonymousParticipantsCountCount > 1">
|
||||
{{ $tc('And no anonymous participations|And one anonymous participation|And {count} anonymous participations', confirmedAnonymousParticipantsCountCount, { count: confirmedAnonymousParticipantsCountCount}) }}
|
||||
</p>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-one-quarter-desktop" v-for="participant in participantsAndCreators" :key="participant.actor.id">
|
||||
<participant-card
|
||||
v-if="participant.actor.id !== config.anonymous.actorId"
|
||||
:participant="participant"
|
||||
:accept="acceptParticipant"
|
||||
:reject="refuseParticipant"
|
||||
:exclude="refuseParticipant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ParticipationTable
|
||||
:data="participants.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:showRole="true"
|
||||
:total="participants.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => participantPage = page"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
@ -32,18 +28,16 @@
|
||||
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="queue.length > 0">
|
||||
<section v-if="queue && queue.total > 0">
|
||||
<h2 class="title">{{ $t('Waiting list') }}</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-one-quarter-desktop" v-for="participant in queue" :key="participant.actor.id">
|
||||
<participant-card
|
||||
:participant="participant"
|
||||
:accept="acceptParticipant"
|
||||
:reject="refuseParticipant"
|
||||
:exclude="refuseParticipant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ParticipationTable
|
||||
:data="queue.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="queue.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => queuePage = page"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
@ -53,18 +47,16 @@
|
||||
<span>{{ $t('Rejected')}} <b-tag rounded> {{ participantStats.rejected }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="rejected.length > 0">
|
||||
<section v-if="rejected && rejected.total > 0">
|
||||
<h2 class="title">{{ $t('Rejected participations') }}</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-one-quarter-desktop" v-for="participant in rejected" :key="participant.actor.id">
|
||||
<participant-card
|
||||
:participant="participant"
|
||||
:accept="acceptParticipant"
|
||||
:reject="refuseParticipant"
|
||||
:exclude="refuseParticipant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ParticipationTable
|
||||
:data="rejected.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="rejected.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => rejectedPage = page"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
@ -81,9 +73,15 @@ import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import ParticipationTable from '@/components/Event/ParticipationTable.vue';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 20;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ParticipationTable,
|
||||
ParticipantCard,
|
||||
},
|
||||
apollo: {
|
||||
@ -97,7 +95,7 @@ import { IConfig } from '@/types/config.model';
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.PARTICIPANT].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
@ -106,18 +104,18 @@ import { IConfig } from '@/types/config.model';
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
organizers: {
|
||||
participants: {
|
||||
query: PARTICIPANTS,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
roles: [ParticipantRole.CREATOR].join(),
|
||||
page: this.participantPage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.CREATOR, ParticipantRole.PARTICIPANT].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
||||
update(data) { return this.dataTransform(data); },
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
@ -127,13 +125,13 @@ import { IConfig } from '@/types/config.model';
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
page: this.queuePage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.NOT_APPROVED].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
||||
update(data) { return this.dataTransform(data); },
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
@ -143,27 +141,36 @@ import { IConfig } from '@/types/config.model';
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
page: this.rejectedPage,
|
||||
limit: PARTICIPANTS_PER_PAGE,
|
||||
roles: [ParticipantRole.REJECTED].join(),
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: data => data.event.participants.map(participation => new Participant(participation)),
|
||||
update(data) { return this.dataTransform(data); },
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
|
||||
},
|
||||
})
|
||||
export default class Participants extends Vue {
|
||||
@Prop({ required: true }) eventId!: string;
|
||||
page: number = 1;
|
||||
limit: number = 10;
|
||||
|
||||
organizers: IParticipant[] = [];
|
||||
queue: IParticipant[] = [];
|
||||
rejected: IParticipant[] = [];
|
||||
participants!: Paginate<IParticipant>;
|
||||
participantPage: number = 1;
|
||||
|
||||
queue!: Paginate<IParticipant>;
|
||||
queuePage: number = 1;
|
||||
|
||||
rejected!: Paginate<IParticipant>;
|
||||
rejectedPage: number = 1;
|
||||
|
||||
event!: IEvent;
|
||||
config!: IConfig;
|
||||
|
||||
@ -173,23 +180,20 @@ export default class Participants extends Vue {
|
||||
hasMoreParticipants: boolean = false;
|
||||
activeTab: number = 0;
|
||||
|
||||
PARTICIPANTS_PER_PAGE = PARTICIPANTS_PER_PAGE;
|
||||
|
||||
dataTransform(data): Paginate<Participant> {
|
||||
return {
|
||||
total: data.event.participants.total,
|
||||
elements: data.event.participants.elements.map(participation => new Participant(participation)),
|
||||
};
|
||||
}
|
||||
|
||||
get participantStats(): IEventParticipantStats | null {
|
||||
if (!this.event) return null;
|
||||
return this.event.participantStats;
|
||||
}
|
||||
|
||||
get participantsAndCreators(): IParticipant[] {
|
||||
if (this.event) {
|
||||
return [...this.organizers, ...this.event.participants]
|
||||
.filter(participant => [ParticipantRole.PARTICIPANT, ParticipantRole.CREATOR].includes(participant.role));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
get confirmedAnonymousParticipantsCountCount(): number {
|
||||
return this.participantsAndCreators.filter(({ actor: { id } }) => id === this.config.anonymous.actorId).length;
|
||||
}
|
||||
|
||||
@Watch('participantStats', { deep: true })
|
||||
watchParticipantStats(stats: IEventParticipantStats) {
|
||||
if (!stats) return;
|
||||
@ -232,8 +236,8 @@ export default class Participants extends Vue {
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.rejected = this.rejected.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.rejected.elements = this.rejected.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participantStats.going += 1;
|
||||
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
@ -242,7 +246,7 @@ export default class Participants extends Vue {
|
||||
this.event.participantStats.rejected -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.PARTICIPANT;
|
||||
this.event.participants.push(participant);
|
||||
this.event.participants.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -260,8 +264,10 @@ export default class Participants extends Vue {
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.event.participants = this.event.participants.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participants.elements = this.event.participants.elements.filter(
|
||||
participant => participant.id !== data.updateParticipation.id,
|
||||
);
|
||||
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participantStats.rejected += 1;
|
||||
if (participant.role === ParticipantRole.PARTICIPANT) {
|
||||
this.event.participantStats.participant -= 1;
|
||||
@ -271,8 +277,8 @@ export default class Participants extends Vue {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.REJECTED;
|
||||
this.rejected = this.rejected.filter(participantIn => participantIn.id !== participant.id);
|
||||
this.rejected.push(participant);
|
||||
this.rejected.elements = this.rejected.elements.filter(participantIn => participantIn.id !== participant.id);
|
||||
this.rejected.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
@ -439,7 +439,10 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata: Map.get(additional, :metadata)
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HtmlSanitizeEx.strip_tags(&1)))
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
|
@ -306,13 +306,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => id} = data
|
||||
%{
|
||||
"type" => "Join",
|
||||
"object" => object,
|
||||
"actor" => _actor,
|
||||
"id" => id,
|
||||
"participationMessage" => note
|
||||
} = data
|
||||
) do
|
||||
with actor <- Utils.get_actor(data),
|
||||
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <- ActivityPub.join(object, actor, false, %{url: id}) do
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.join(object, actor, false, %{url: id, metadata: %{message: note}}) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
|
@ -77,6 +77,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
"anonymousParticipationEnabled" => %{
|
||||
"@id" => "mz:anonymousParticipationEnabled",
|
||||
"@type" => "sc:Boolean"
|
||||
},
|
||||
"participationMessage" => %{
|
||||
"@id" => "mz:participationMessage",
|
||||
"@type" => "sc:Text"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -25,7 +25,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Participant do
|
||||
"type" => "Join",
|
||||
"id" => participant.url,
|
||||
"actor" => participant.actor.url,
|
||||
"object" => participant.event.url
|
||||
"object" => participant.event.url,
|
||||
"participationMessage" => Map.get(participant.metadata, :message)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -102,7 +102,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
end
|
||||
|
||||
def list_participants_for_event(_, _args, _resolution) do
|
||||
{:ok, []}
|
||||
{:ok, %{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
|
||||
|
@ -17,12 +17,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||
"""
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{actor_id: actor_id, event_id: event_id} = args,
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
) do
|
||||
case User.owns_actor(user, actor_id) do
|
||||
{:is_owned, %Actor{} = actor} ->
|
||||
do_actor_join_event(actor, event_id)
|
||||
do_actor_join_event(actor, event_id, args)
|
||||
|
||||
_ ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
@ -136,7 +136,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id, token: token},
|
||||
_resolution
|
||||
) do
|
||||
)
|
||||
when not is_nil(token) do
|
||||
with {:anonymous_participation_enabled, true} <-
|
||||
{:anonymous_participation_enabled, Config.anonymous_participation?()},
|
||||
{:anonymous_actor_id, true} <-
|
||||
|
@ -65,7 +65,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
|
||||
field(:participant_stats, :participant_stats)
|
||||
|
||||
field(:participants, list_of(:participant), description: "The event's participants") do
|
||||
field(:participants, :paginated_participant_list, description: "The event's participants") do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
arg(:roles, :string, default_value: "")
|
||||
|
@ -33,12 +33,21 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||
field(:metadata, :participant_metadata,
|
||||
description: "The metadata associated to this participant"
|
||||
)
|
||||
|
||||
field(:inserted_at, :datetime, description: "The datetime this participant was created")
|
||||
end
|
||||
|
||||
object :participant_metadata do
|
||||
field(:cancellation_token, :string,
|
||||
description: "The eventual token to leave an event when user is anonymous"
|
||||
)
|
||||
|
||||
field(:message, :string, description: "The eventual message the participant left")
|
||||
end
|
||||
|
||||
object :paginated_participant_list do
|
||||
field(:elements, list_of(:participant), description: "A list of participants")
|
||||
field(:total, :integer, description: "The total number of participants in the list")
|
||||
end
|
||||
|
||||
enum :participant_role_enum do
|
||||
@ -64,6 +73,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||
arg(:event_id, non_null(:id))
|
||||
arg(:actor_id, non_null(:id))
|
||||
arg(:email, :string)
|
||||
arg(:message, :string)
|
||||
|
||||
resolve(&Participant. |