Add anonymous and remote participations

This commit is contained in:
Thomas Citharel 2019-12-20 13:04:34 +01:00
parent 17e0b3968f
commit 2ed9050a90
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
135 changed files with 10141 additions and 2271 deletions

View File

@ -156,6 +156,28 @@ config :mobilizon, :maps,
attribution: System.get_env("MAPS_TILES_ATTRIBUTION")
]
config :mobilizon, :anonymous,
participation: [
allowed: true,
validation: %{
email: [
enabled: true,
confirmation_required: true
],
captcha: [enabled: false]
}
],
event_creation: [
allowed: false,
validation: %{
email: [
enabled: true,
confirmation_required: true
],
captcha: [enabled: false]
}
]
config :mobilizon, Oban,
repo: Mobilizon.Storage.Repo,
prune: {:maxlen, 10_000},

View File

@ -73,3 +73,5 @@ config :mobilizon, Mobilizon.Storage.Repo,
port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432",
pool_size: 10,
show_sensitive_data_on_connection_error: true
config :mobilizon, :activitypub, sign_object_fetches: false

View File

@ -18,7 +18,9 @@ config :mobilizon, Mobilizon.Web.Endpoint,
# Print only warnings and errors during test
config :logger,
backends: [:console],
compile_time_purge_level: :debug,
compile_time_purge_matching: [
[level_lower_than: :debug]
],
level: :info
# Configure your database

View File

@ -28,6 +28,7 @@
"apollo-link-ws": "^1.0.19",
"apollo-utilities": "^1.3.2",
"buefy": "^0.8.2",
"bulma-divider": "^0.2.0",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"intersection-observer": "^0.7.0",

View File

@ -85,6 +85,7 @@ export default class App extends Vue {
/* Bulma imports */
@import "~bulma/bulma";
@import '~bulma-divider';
/* Buefy imports */
@import "~buefy/src/scss/buefy";
@ -102,12 +103,12 @@ $mdi-font-path: "~@mdi/font/fonts";
body {
// background: #f7f8fa;
background: #ebebeb;
background: $body-background-color;
font-family: BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif;
main {
margin: 1rem auto 0;
}
/*main {*/
/* margin: 1rem auto 0;*/
/*}*/
}
#mobilizon > .container > .message {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="address-autocomplete">
<b-field expanded>
<template slot="label">
{{ $t('Find an address') }}
@ -245,6 +245,10 @@ export default class AddressAutoComplete extends Vue {
}
</script>
<style lang="scss">
.address-autocomplete {
margin-bottom: 0.75rem;
}
.autocomplete {
.dropdown-menu {
z-index: 2000;

View File

@ -0,0 +1,171 @@
<docs>
A simple card for an event
```vue
<template>
<div>
<EventListViewCard
:event="event"
/>
</div>
</template>
<script>
export default {
data() {
return {
event: {
title: 'Vue Styleguidist first meetup: learn the basics!',
id: 5,
uuid: 'some uuid',
beginsOn: new Date(),
organizerActor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null,
id: 4,
displayName() {
return 'Some random dude'
}
},
options: {
maximumAttendeeCapacity: 4
},
participantStats: {
approved: 1,
notApproved: 2
}
}
}
}
}
}
</script>
```
</docs>
<template>
<article class="box">
<div class="columns">
<div class="content column">
<div class="title-wrapper">
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"><h2 class="title">{{ event.title }}</h2></router-link>
</div>
<div class="participation-actor has-text-grey">
<span v-if="event.physicalAddress && event.physicalAddress.locality">{{ event.physicalAddress.locality }}</span>
<span>
<span>{{ $t('Organized by {name}', { name: event.organizerActor.displayName() } ) }}</span>
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" />
<b-icon icon="lock-open" v-if="event.visibility === EventVisibility.UNLISTED" />
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" />
</span>
<span class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0">
{{ $t('{approved} / {total} seats', {approved: event.participantStats.participant, total: event.options.maximumAttendeeCapacity }) }}
</span>
<span v-else>
{{ $tc('{count} participants', event.participantStats.participant, { count: event.participantStats.participant })}}
</span>
</span>
</div>
</div>
</div>
</article>
</template>
<script lang="ts">
import { IParticipant, ParticipantRole, EventVisibility, IEventCardOptions } from '@/types/event.model';
import { Component, Prop } from 'vue-property-decorator';
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
import { IPerson } from '@/types/actor';
import { mixins } from 'vue-class-component';
import ActorMixin from '@/mixins/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import EventMixin from '@/mixins/event';
import { RouteName } from '@/router';
import { changeIdentity } from '@/utils/auth';
import { Route } from 'vue-router';
const defaultOptions: IEventCardOptions = {
hideDate: true,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
};
@Component({
components: {
DateCalendarIcon,
},
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class EventListViewCard extends mixins(ActorMixin, EventMixin) {
/**
* The participation associated
*/
@Prop({ required: true }) event!: IParticipant;
/**
* Options are merged with default options
*/
@Prop({ required: false, default: () => defaultOptions }) options!: IEventCardOptions;
currentActor!: IPerson;
ParticipantRole = ParticipantRole;
EventVisibility = EventVisibility;
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>
@import "../../variables";
article.box {
div.content {
padding: 5px;
.participation-actor span, .participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
}
div.title-wrapper {
display: flex;
align-items: center;
div.date-component {
flex: 0;
margin-right: 16px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
}
}
}
}
</style>

View File

@ -25,7 +25,7 @@ A button to set your participation
<template>
<div class="participation-button">
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
<button class="button is-success" type="button" slot="trigger">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="check" />
<template>
<span>{{ $t('I participate') }}</span>
@ -33,10 +33,6 @@ A button to set your participation
<b-icon icon="menu-down" />
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
<!-- {{ $t('Change my identity…')}}-->
<!-- </b-dropdown-item>-->
<b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave" class="has-text-danger">
{{ $t('Cancel my participation…')}}
</b-dropdown-item>
@ -44,7 +40,7 @@ A button to set your participation
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<button class="button is-success" type="button" slot="trigger">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" />
<template>
<span>{{ $t('I participate') }}</span>
@ -68,8 +64,8 @@ A button to set your participation
<span>{{ $t('Unfortunately, your participation request was rejected by the organizers.')}}</span>
</div>
<b-dropdown aria-role="list" position="is-bottom-left" v-if="!participation">
<button class="button is-primary" type="button" slot="trigger">
<b-dropdown aria-role="list" position="is-bottom-left" v-else-if="!participation && currentActor.id">
<button class="button is-primary is-large" type="button" slot="trigger">
<template>
<span>{{ $t('Participate') }}</span>
</template>
@ -93,22 +89,29 @@ A button to set your participation
{{ $t('with another identity…')}}
</b-dropdown-item>
</b-dropdown>
<b-button tag="router-link" :to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }" v-else-if="!participation && hasAnonymousParticipationMethods" type="is-primary" size="is-large" native-type="button">{{ $t('Participate') }}</b-button>
<b-button tag="router-link" :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }" v-else-if="!currentActor.id" type="is-primary" size="is-large" native-type="button">{{ $t('Participate') }}</b-button>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IParticipant, ParticipantRole } from '@/types/event.model';
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson, Person } from '@/types/actor';
import { IDENTITIES } from '@/graphql/actor';
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { RouteName } from '@/router';
import { FETCH_EVENT } from '@/graphql/event';
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
},
currentActor: CURRENT_ACTOR_CLIENT,
config: CONFIG,
identities: {
query: IDENTITIES,
update: ({ identities }) => identities ? identities.map(identity => new Person(identity)) : [],
@ -120,11 +123,13 @@ import { ICurrentUser } from '@/types/current-user.model';
})
export default class ParticipationButton extends Vue {
@Prop({ required: true }) participation!: IParticipant;
@Prop({ required: true }) event!: IEvent;
@Prop({ required: true }) currentActor!: IPerson;
ParticipantRole = ParticipantRole;
currentUser!: ICurrentUser;
identities: IPerson[] = [];
config!: IConfig;
RouteName = RouteName;
joinEvent(actor: IPerson) {
this.$emit('joinEvent', actor);
@ -138,10 +143,13 @@ export default class ParticipationButton extends Vue {
this.$emit('confirmLeave');
}
get hasAnonymousParticipationMethods(): boolean {
return this.event.options.anonymousParticipation;
}
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.participation-button {
.dropdown {
display: flex;
@ -151,9 +159,11 @@ export default class ParticipationButton extends Vue {
opacity: 0.5;
}
}
}
button {
font-size: 1.5rem;
.anonymousParticipationModal {
/deep/ .animation-content {
z-index: 1;
}
}
</style>

View File

@ -4,6 +4,7 @@
<img src="../assets/footer.png" :alt="$t('World map')" />
<ul>
<li><a href="https://joinmobilizon.org">{{ $t('About') }}</a></li>
<li><router-link :to="{ name: RouteName.TERMS }">{{ $t('Terms') }}</router-link></li>
<li><a href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">{{ $t('License') }}</a></li>
</ul>
<div class="content has-text-centered">
@ -14,6 +15,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Logo from './Logo.vue';
import { RouteName } from '@/router';
@Component({
components: {
@ -21,6 +23,7 @@ import Logo from './Logo.vue';
},
})
export default class Footer extends Vue {
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,59 @@
<template>
<section class="container">
<h1 class="title" v-if="loading">
{{ $t('Your participation is being validated') }}
</h1>
<div v-else>
<div v-if="failed">
<b-message :title="$t('Error while validating participation')" type="is-danger">
{{ $t('Either the participation has already been validated, either the validation token is incorrect.') }}
</b-message>
</div>
<h1 class="title" v-else>
{{ $t('Your participation has been validated') }}
</h1>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import { IParticipant } from '@/types/event.model';
import { CONFIRM_PARTICIPATION } from '@/graphql/event';
import { confirmLocalAnonymousParticipation } from '@/services/AnonymousParticipationStorage';
@Component
export default class ConfirmParticipation extends Vue {
@Prop({ type: String, required: true }) token!: string;
loading = true;
failed = false;
async created() {
await this.validateAction();
}
async validateAction() {
try {
const { data } = await this.$apollo.mutate<{ confirmParticipation: IParticipant }>({
mutation: CONFIRM_PARTICIPATION,
variables: {
token: this.token,
},
});
if (data) {
const { confirmParticipation: participation } = data;
await confirmLocalAnonymousParticipation(participation.event.uuid);
await this.$router.replace({ name: RouteName.EVENT, params: { uuid: data.confirmParticipation.event.uuid } } );
}
} catch (err) {
console.error(err);
this.failed = true;
} finally {
this.loading = false;
}
}
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<section class="section container hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<b-button type="is-primary" size="is-medium" tag="router-link" :to="{ name: RouteName.LOGIN }">{{ $t('Login on {instance}', { instance: host }) }}</b-button>
</div>
<vertical-divider :content="$t('Or')" />
<div class="column">
<h3 class="subtitle">{{ $t('I have an account on another Mobilizon instance.')}}</h3>
<p>{{ $t('Other software may also support this.') }}</p>
<p>{{ $t('We will redirect you to your instance in order to interact with this event') }}</p>
<form @submit.prevent="redirectToInstance">
<b-field :label="$t('Your federated identity')">
<b-field>
<b-input
expanded
autocapitalize="none" autocorrect="off"
v-model="remoteActorAddress"
:placeholder="$t('profile@instance')">
</b-input>
<p class="control">
<button class="button is-primary" type="submit">{{ $t('Go') }}</button>
</p>
</b-field>
</b-field>
</form>
</div>
</div>
<div class="has-text-centered">
<b-button tag="a" type="is-text" @click="$router.go(-1)">
{{ $t('Back to previous page') }}
</b-button>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import VerticalDivider from '@/components/Utils/VerticalDivider.vue';
@Component({
components: { VerticalDivider },
})
export default class ParticipationWithAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
remoteActorAddress: string = '';
RouteName = RouteName;
get host() {
return window.location.hostname;
}
get uri(): string {
return `${window.location.origin}${this.$router.resolve({ name: RouteName.EVENT, params: { uuid: this.uuid } }).href}`;
}
async redirectToInstance() {
let res;
const [_, host] = res = this.remoteActorAddress.split('@', 2);
const remoteInteractionURI = await this.webFingerFetch(host, this.remoteActorAddress);
window.open(remoteInteractionURI);
}
private async webFingerFetch(hostname: string, identity: string): Promise<string> {
const data = await ((await fetch(`http://${hostname}/.well-known/webfinger?resource=acct:${identity}`)).json());
if (data && Array.isArray(data.links)) {
const link: { template: string } = data.links.find((link: any) => {
return link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe';
});
if (link && link.template.includes('{uri}')) {
return link.template.replace('{uri}', encodeURIComponent(this.uri));
}
}
throw new Error('No interaction path found in webfinger data');
}
}
</script>

View File

@ -0,0 +1,109 @@
<template>
<section class="container section hero is-fullheight">
<div class="hero-body">
<div class="container">
<form @submit.prevent="joinEvent">
<p>{{ $t('This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.') }}</p>
<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-field>
<div class="has-text-centered">
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">
{{ $t('Back to previous page') }}
</b-button>
</div>
</form>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { FETCH_EVENT, JOIN_EVENT } from '@/graphql/event';
import { IConfig } from '@/types/config.model';
import { CONFIG } from '@/graphql/config';
import { addLocalUnconfirmedAnonymousParticipation } from '@/services/AnonymousParticipationStorage';
import { RouteName } from '@/router';
@Component({
apollo: {
event: {
query: FETCH_EVENT,
variables() {
return {
uuid: this.uuid,
};
},
skip() { return !this.uuid; },
update: (data) => new EventModel(data.event),
},
config: CONFIG,
},
})
export default class ParticipationWithoutAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
anonymousParticipation: { email: String } = { email: '' };
event!: IEvent;
config!: IConfig;
error: String|boolean = false;
async joinEvent() {
this.error = false;
try {
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
mutation: JOIN_EVENT,
variables: {
eventId: this.event.id,
actorId: this.config.anonymous.actorId,
email: this.anonymousParticipation.email,
},
update: (store, { data }) => {
if (data == null) return;
const cachedData = store.readQuery<{ event: IEvent }>({
query: FETCH_EVENT,
variables: { uuid: this.event.uuid },
});
if (cachedData == null) return;
const { event } = cachedData;
if (event === null) {
console.error('Cannot update event participant cache, because of null value.');
return;
}
if (data.joinEvent.role === ParticipantRole.NOT_CONFIRMED) {
event.participantStats.notConfirmed = event.participantStats.notConfirmed + 1;
} else {
event.participantStats.going = event.participantStats.going + 1;
event.participantStats.participant = event.participantStats.participant + 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.event.uuid }, data: { event } });
},
});
if (data && data.joinEvent.metadata.cancellationToken) {
await addLocalUnconfirmedAnonymousParticipation(this.event, data.joinEvent.metadata.cancellationToken);
return this.$router.push({ name: RouteName.EVENT, params: { uuid: this.event.uuid } });
}
} catch (e) {
console.log(JSON.stringify(e));
if (e.message === 'GraphQL error: You are already a participant of this event') {
this.error = this.$t('This email is already registered as participant for this event') as string;
}
}
}
}
</script>

View File

@ -0,0 +1,104 @@
<template>
<section class="section container hero">
<div class="hero-body" v-if="event">
<div class="container">
<h2 class="subtitle">{{ $t('You wish to participate to the following event')}}</h2>
<EventListViewCard v-if="event" :event="event" />
<div class="columns has-text-centered">
<div class="column">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
<figure class="image is-128x128">
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
</figure>
<b-button type="is-primary">{{ $t('I have a Mobilizon account') }}</b-button>
</router-link>
<p>
<small>{{ $t('Either on the {instance} instance or on another instance.', {instance: host })}}</small>
<b-tooltip type="is-dark" :label="$t('Mobilizon is a federated network. You can interact with this event from a different server.')">
<b-icon size="is-small" icon="help-circle-outline" />
</b-tooltip>
</p>
</div>
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
<div class="column" v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }" v-if="event.local">
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
</router-link>
<a :href="`${event.url}/participate/without-account`" v-else>
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
</a>
<p>
<small>{{ $t('Participate using your email address')}}</small><br />
<small v-if="!event.local">{{ $t('You will be redirected to the original instance')}}</small>
</p>
</div>
</div>
<div class="has-text-centered">
<b-button tag="a" type="is-text" @click="$router.go(-1)">
{{ $t('Back to previous page') }}
</b-button>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import { FETCH_EVENT } from '@/graphql/event';
import EventListCard from '@/components/Event/EventListCard.vue';
import EventListViewCard from '@/components/Event/EventListViewCard.vue';
import { EventModel, IEvent } from '@/types/event.model';
import VerticalDivider from '@/components/Utils/VerticalDivider.vue';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
@Component({
components: { VerticalDivider, EventListViewCard, EventListCard },
apollo: {
event: {
query: FETCH_EVENT,
variables() {
return {
uuid: this.uuid,
};
},
skip() { return !this.uuid; },
update: (data) => new EventModel(data.event),
},
config: CONFIG,
},
})
export default class UnloggedParticipation extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
RouteName = RouteName;
event!: IEvent;
config!: IConfig;
get host() {
return window.location.hostname;
}
get anonymousParticipationAllowed(): boolean {
return this.event.options.anonymousParticipation;
}
get hasAnonymousEmailParticipationMethod(): boolean {
return this.config.anonymous.participation.allowed && this.config.anonymous.participation.validation.email.enabled;
}
}
</script>
<style lang="scss" scoped>
.column > a {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="is-divider-vertical" :data-content="dataContent"></div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class VerticalDivider extends Vue {
@Prop({ default: 'Or' }) content;
get dataContent() {
return this.content.toLocaleUpperCase();
}
}
</script>
<style lang="scss" scoped>
@import "@/variables.scss";
.is-divider-vertical[data-content]::after {
background-color: $body-background-color;
}
</style>

View File

@ -103,3 +103,45 @@ export const REJECT_RELAY = gql`
}
${RELAY_FRAGMENT}
`;
export const ADMIN_SETTINGS_FRAGMENT = gql`
fragment adminSettingsFragment on AdminSettings {
instanceName,
instanceDescription,
instanceTerms,
instanceTermsType,
instanceTermsUrl
registrationsOpen
}
`;
export const ADMIN_SETTINGS = gql`
query {
adminSettings {
...adminSettingsFragment
}
}
${ADMIN_SETTINGS_FRAGMENT}
`;
export const SAVE_ADMIN_SETTINGS = gql`
mutation SaveAdminSettings(
$instanceName: String,
$instanceDescription: String,
$instanceTerms: String,
$instanceTermsType: InstanceTermsType,
$instanceTermsUrl: String,
$registrationsOpen: Boolean) {
saveAdminSettings(
instanceName: $instanceName,
instanceDescription: $instanceDescription,
instanceTerms: $instanceTerms,
instanceTermsType: $instanceTermsType,
instanceTermsUrl: $instanceTermsUrl,
registrationsOpen: $registrationsOpen
) {
...adminSettingsFragment
}
}
${ADMIN_SETTINGS_FRAGMENT}
`;

View File

@ -9,6 +9,33 @@ query {
registrationsWhitelist,
demoMode,
countryCode,
anonymous {
participation {
allowed,
validation {
email {
enabled,
confirmationRequired
},
captcha {
enabled
}
}
}
eventCreation {
allowed,
validation {
email {
enabled,
confirmationRequired
},
captcha {
enabled
}
}
}
actorId
},
location {
latitude,
longitude,
@ -27,3 +54,15 @@ query {
}
}
`;
export const TERMS = gql`
query Terms($locale: String) {
config {
terms(locale: $locale) {
type,
url,
bodyHtml
}
}
}
`;

View File

@ -14,7 +14,11 @@ const participantQuery = `
domain
},
event {
id
id,
uuid
},
metadata {
cancellationToken
}
`;
@ -41,6 +45,7 @@ const optionsQuery = `
maximumAttendeeCapacity,
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
anonymousParticipation,
showStartTime,
showEndTime,
offers {
@ -366,10 +371,11 @@ export const EDIT_EVENT = gql`
`;
export const JOIN_EVENT = gql`
mutation JoinEvent($eventId: ID!, $actorId: ID!) {
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String) {
joinEvent(
eventId: $eventId,
actorId: $actorId
actorId: $actorId,
email: $email
) {
${participantQuery}
}
@ -377,10 +383,11 @@ export const JOIN_EVENT = gql`
`;
export const LEAVE_EVENT = gql`
mutation LeaveEvent($eventId: ID!, $actorId: ID!) {
mutation LeaveEvent($eventId: ID!, $actorId: ID!, $token: String) {
leaveEvent(
eventId: $eventId,
actorId: $actorId
actorId: $actorId,
token: $token
) {
actor {
id
@ -389,6 +396,20 @@ export const LEAVE_EVENT = gql`
}
`;
export const CONFIRM_PARTICIPATION = gql`
mutation ConfirmParticipation($token: String!) {
confirmParticipation(confirmationToken: $token) {
actor {
id,
},
event {
uuid
},
role
}
}
`;
export const UPDATE_PARTICIPANT = gql`
mutation AcceptParticipant($id: ID!, $moderatorActorId: ID!, $role: ParticipantRoleEnum!) {
updateParticipation(id: $id, moderatorActorId: $moderatorActorId, role: $role) {

View File

@ -15,10 +15,16 @@
"Add to my calendar": "Add to my calendar",
"Add": "Add",
"Additional comments": "Additional comments",
"Admin settings successfully saved.": "Admin settings successfully saved.",
"Admin settings": "Admin settings",
"Administration": "Administration",
"All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available",
"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",
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Are you sure you want to <b>delete</b> this comment? This action cannot be undone.",
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.",
@ -27,8 +33,10 @@
"Are you sure you want to cancel your participation at event \"{title}\"?": "Are you sure you want to cancel your participation at event \"{title}\"?",
"Are you sure you want to delete this event? This action cannot be reverted.": "Are you sure you want to delete this event? This action cannot be reverted.",
"Avatar": "Avatar",
"Back to previous page": "Back to previous page",
"Before you can login, you need to click on the link inside it to validate your account": "Before you can login, you need to click on the link inside it to validate your account",
"By {name}": "By {name}",
"Cancel anonymous participation": "Cancel anonymous participation",
"Cancel creation": "Cancel creation",
"Cancel edition": "Cancel edition",
"Cancel my participation request…": "Cancel my participation request…",
@ -67,10 +75,15 @@
"Create": "Create",
"Creator": "Creator",
"Current identity has been changed to {identityName} in order to manage this event.": "Current identity has been changed to {identityName} in order to manage this event.",
"Custom URL": "Custom URL",
"Custom text": "Custom text",
"Custom": "Custom",
"Dashboard": "Dashboard",
"Date and time settings": "Date and time settings",
"Date parameters": "Date parameters",
"Date": "Date",
"Default Mobilizon.org terms": "Default Mobilizon.org terms",
"Default": "Default",
"Delete Comment": "Delete Comment",
"Delete Event": "Delete Event",
"Delete event": "Delete event",
@ -90,14 +103,18 @@
"Drafts": "Drafts",
"Edit": "Edit",
"Eg: Stockholm, Dance, Chess…": "Eg: Stockholm, Dance, Chess…",
"Either on the {instance} instance or on another instance.": "Either on the {instance} instance or on another instance.",
"Either the account is already validated, either the validation token is incorrect.": "Either the account is already validated, either the validation token is incorrect.",
"Either the participation has already been validated, either the validation token is incorrect.": "Either the participation has already been validated, either the validation token is incorrect.",
"Email": "Email",
"Ends on…": "Ends on…",
"Enjoy discovering Mobilizon!": "Enjoy discovering Mobilizon!",
"Enter the link URL": "Enter the link URL",
"Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.": "Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.",
"Error while communicating with the server.": "Error while communicating with the server.",
"Error while saving report.": "Error while saving report.",
"Error while validating account": "Error while validating account",
"Error while validating participation": "Error while validating participation",
"Event already passed": "Event already passed",
"Event cancelled": "Event cancelled",
"Event creation": "Event creation",
@ -113,6 +130,7 @@
"Ex: test.mobilizon.org": "Ex: test.mobilizon.org",
"Exclude": "Exclude",
"Explore": "Explore",
"Failed to save admin settings": "Failed to save admin settings",
"Featured events": "Featured events",
"Features": "Features",
"Find an address": "Find an address",
@ -128,6 +146,7 @@
"Gather ⋅ Organize ⋅ Mobilize": "Gather ⋅ Organize ⋅ Mobilize",
"General information": "General information",
"Getting location": "Getting location",
"Go": "Go",
"Going as {name}": "Going as {name}",
"Group List": "Group List",
"Group full name": "Group full name",
@ -137,7 +156,11 @@
"Headline picture": "Headline picture",
"Hide replies": "Hide replies",
"I create an identity": "I create an identity",
"I don't have a Mobilizon account": "I don't have a Mobilizon account",
"I have a Mobilizon account": "I have a Mobilizon account",
"I have an account on another Mobilizon instance.": "I have an account on another Mobilizon instance.",
"I participate": "I participate",
"I want to allow people to participate without an account.": "I want to allow people to participate without an account.",
"I want to approve every participation request": "I want to approve every participation request",
"Identity {displayName} created": "Identity {displayName} created",
"Identity {displayName} deleted": "Identity {displayName} deleted",
@ -147,6 +170,11 @@
"Impossible to login, your email or password seems incorrect.": "Impossible to login, your email or password seems incorrect.",
"In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.": "In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.",
"Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.": "Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.",
"Instance Description": "Instance Description",
"Instance Name": "Instance Name",
"Instance Terms Source": "Instance Terms Source",
"Instance Terms URL": "Instance Terms URL",
"Instance Terms": "Instance Terms",
"Instances": "Instances",
"Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance",
"Last published event": "Last published event",
@ -163,10 +191,12 @@
"Log in": "Log in",
"Log out": "Log out",
"Login on Mobilizon!": "Login on Mobilizon!",
"Login on {instance}": "Login on {instance}",
"Login": "Login",
"Manage participations": "Manage participations",
"Mark as resolved": "Mark as resolved",
"Members": "Members",
"Mobilizon is a federated network. You can interact with this event from a different server.": "Mobilizon is a federated network. You can interact with this event from a different server.",
"Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.": "Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.",
"Mobilizon is under development, we will add new features to this site during regular updates, until the release of <b>version 1 of the software in the first half of 2020</b>.": "Mobilizon is under development, we will add new features to this site during regular updates, until the release of <b>version 1 of the software in the first half of 2020</b>.",
"Mobilizons licence": "Mobilizons licence",
@ -207,15 +237,18 @@
"Only alphanumeric characters and underscores are supported.": "Only alphanumeric characters and underscores are supported.",
"Open": "Open",
"Opened reports": "Opened reports",
"Or": "Or",
"Organized by {name}": "Organized by {name}",
"Organized": "Organized",
"Organizer": "Organizer",
"Other software may also support this.": "Other software may also support this.",
"Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.",
"Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)",
"Page not found": "Page not found",
"Participant already was rejected.": "Participant already was rejected.",
"Participant has already been approved as participant.": "Participant has already been approved as participant.",
"Participants": "Participants",
"Participate using your email address": "Participate using your email address",
"Participate": "Participate",
"Participation approval": "Participation approval",
"Participation requested!": "Participation requested!",
@ -234,6 +267,7 @@
"Post a comment": "Post a comment",
"Post a reply": "Post a reply",
"Postal Code": "Postal Code",
"Privacy Policy": "Privacy Policy",
"Private event": "Private event",
"Private feeds": "Private feeds",
"Public RSS/Atom Feed": "Public RSS/Atom Feed",
@ -245,10 +279,13 @@
"Published events": "Published events",
"RSS/Atom Feed": "RSS/Atom Feed",
"Read Framasofts statement of intent on the Framablog": "Read Framasofts statement of intent on the Framablog",
"Redirecting to event…": "Redirecting to event…",
"Region": "Region",
"Register an account on Mobilizon!": "Register an account on Mobilizon!",
"Register for an event by choosing one of your identities": "Register for an event by choosing one of your identities",
"Register": "Register",
"Registration is allowed, anyone can register.": "Registration is allowed, anyone can register.",
"Registration is closed.": "Registration is closed.",
"Registration is currently closed.": "Registration is currently closed.",
"Registrations are restricted by whitelisting.": "Registrations are restricted by whitelisting.",
"Reject": "Reject",
@ -269,15 +306,19 @@
"Resend confirmation email": "Resend confirmation email",
"Reset my password": "Reset my password",
"Resolved": "Resolved",
"Resource provided is not an URL": "Resource provided is not an URL",
"Save draft": "Save draft",
"Save": "Save",
"Search events, groups, etc.": "Search events, groups, etc.",
"Search results: \"{search}\"": "Search results: \"{search}\"",
"Search": "Search",
"Searching…": "Searching…",
"Send email": "Send email",
"Send me an email to reset my password": "Send me an email to reset my password",
"Send me the confirmation email once again": "Send me the confirmation email once again",
"Send the report": "Send the report",
"Set an URL to a page with your own terms.": "Set an URL to a page with your own terms.",
"Settings": "Settings",
"Share this event": "Share this event",
"Show map": "Show map",
"Show remaining number of places": "Show remaining number of places",
@ -289,6 +330,8 @@
"Status": "Status",
"Street": "Street",
"Tentative: Will be confirmed later": "Tentative: Will be confirmed later",
"Terms": "Terms",
"The actual number of participants may differ, as this event is hosted on another instance.": "The actual number of participants may differ, as this event is hosted on another instance.",
"The content came from another server. Transfer an anonymous copy of the report?": "The content came from another server. Transfer an anonymous copy of the report ?",
"The current identity doesn't have any permission on this event. You should probably change it.": "The current identity doesn't have any permission on this event. You should probably change it.",
"The draft event has been updated": "The draft event has been updated",
@ -302,8 +345,12 @@
"The password was successfully changed": "The password was successfully changed",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.",
"The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.": "The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.",
"The {default_terms} will be used. They will be translated in the user's language.": "The {default_terms} will be used. They will be translated in the user's language.",
"There are {participants} participants.": "There are {participants} participants.",
"These events may interest you": "These events may interest you",
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.",
"This email is already registered as participant for this event": "This email is already registered as participant for this event",
"This information is saved only on your computer. Click for details": "This information is saved only on your computer. Click for details",
"This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.",
"This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.",
"This is a demonstration site to test the beta version of Mobilizon.": "This is a demonstration site to test the beta version of Mobilizon.",
@ -315,6 +362,7 @@
"To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"",
"Transfer to {outsideDomain}": "Transfer to {outsideDomain}",
"Type": "Type",
"URL": "URL",
"Unfortunately, this instance isn't opened to registrations": "Unfortunately, this instance isn't opened to registrations",
"Unfortunately, your participation request was rejected by the organizers.": "Unfortunately, your participation request was rejected by the organizers.",
"Unknown actor": "Unknown actor",
@ -337,6 +385,7 @@
"Warning": "Warning",
"We just sent an email to {email}": "We just sent an email to {email}",
"We want to develop a <b>digital common</b>, that everyone can make their own, which respects <b>privacy and activism by design</b>.": "We want to develop a <b>digital common</b>, that everyone can make their own, which respects <b>privacy and activism by design</b>.",
"We will redirect you to your instance in order to interact with this event": "We will redirect you to your instance in order to interact with this event",
"We wont change the world from Facebook. The tool we dream of, surveillance capitalism corporations wont develop it, as they couldnt profit from it. This is an opportunity to build something better, by taking another approach.": "We wont change the world from Facebook. The tool we dream of, surveillance capitalism corporations wont develop it, as they couldnt profit from it. This is an opportunity to build something better, by taking another approach.",
"Website / URL": "Website / URL",
"Welcome back {username}!": "Welcome back {username}!",
@ -348,7 +397,8 @@
"Write something…": "Write something…",
"You and one other person are going to this event": "You're the only one going to this event | You and one other person are going to this event | You and {approved} persons are going to this event.",
"You are already a participant of this event.": "You are already a participant of this event.",
"You are already logged-in.": "You are already logged-in.",
"You are participating in this event anonymously but didn't confirm participation": "You are participating in this event anonymously but didn't confirm participation",
"You are participating in this event anonymously": "You are participating in this event anonymously",
"You can add tags by hitting the Enter key or by adding a comma": "You can add tags by hitting the Enter key or by adding a comma",
"You can try another search term or drag and drop the marker on the map": "You can try another search term or drag and drop the marker on the map",
"You can't remove your last identity.": "You can't remove your last identity.",
@ -360,25 +410,33 @@
"You have one event tomorrow.": "You have no events tomorrow | You have one event tomorrow. | You have {count} events tomorrow",
"You may also ask to {resend_confirmation_email}.": "You may also ask to {resend_confirmation_email}.",
"You need to login.": "You need to login.",
"You will be redirected to the original instance": "You will be redirected to the original instance",
"You wish to participate to the following event": "You wish to participate to the following event",
"Your account has been validated": "Your account has been validated",
"Your account is being validated": "Your account is being validated",
"Your account is nearly ready, {username}": "Your account is nearly ready, {username}",
"Your email is not whitelisted, you can't register.": "Your email is not whitelisted, you can't register.",
"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.": "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.",
"Your federated identity": "Your federated identity",
"Your local administrator resumed its policy:": "Your local administrator resumed its policy:",
"Your participation has been confirmed": "Your participation has been confirmed",
"Your participation has been rejected": "Your participation has been rejected",
"Your participation has been requested": "Your participation has been requested",
"Your participation has been validated": "Your participation has been validated",
"Your participation is being validated": "Your participation is being validated",
"Your participation status has been changed": "Your participation status has been changed",
"[This comment has been deleted]": "[This comment has been deleted]",
"[deleted]": "[deleted]",
"a decentralised federation protocol": "a decentralised federation protocol",
"as {identity}": "as {identity}",
"default Mobilizon terms": "default Mobilizon terms",
"e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot",
"firstDayOfWeek": "0",
"iCal Feed": "iCal Feed",
"interconnect with others like it": "interconnect with others like it",
"its source code is public": "its source code is public",
"on our blog": "on our blog",
"profile@instance": "profile@instance",
"resend confirmation email": "resend confirmation email",
"respect of the fundamental freedoms": "respect of the fundamental freedoms",
"with another identity…": "with another identity…",
@ -388,4 +446,4 @@
"{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"
}
}

View File

@ -3,22 +3,28 @@
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
"A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
"Abandon edition": "Abandonner la modification",
"About": "À propos",
"About Mobilizon": "À propos de Mobilizon",
"About this event": "À propos de cet évènement",
"About this instance": "À propos de cette instance",
"About": "À propos",
"Accepted": "Accepté",
"Add": "Ajouter",
"Add a note": "Ajouter une note",
"Add an address": "Ajouter une adresse",
"Add an instance": "Ajouter une instance",
"Add some tags": "Ajouter des tags",
"Add to my calendar": "Ajouter à mon agenda",
"Add": "Ajouter",
"Additional comments": "Commentaires additionnels",
"Admin settings successfully saved.": "Les paramètres administrateur ont bien été sauvegardés",
"Admin settings": "Paramètres administrateur",
"Administration": "Administration",
"All the places have already been taken": "Toutes les places ont été prises|Une place est encore disponible|{places} places sont encore disponibles",
"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",
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> ce commentaire ? Cette action ne peut pas être annulée.",
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet évènement ? Cette action n'est pas réversible. Vous voulez peut-être engager la conversation avec le créateur de l'évènement ou bien modifier son évènement à la place.",
@ -27,35 +33,36 @@
"Are you sure you want to cancel your participation at event \"{title}\"?": "Êtes-vous certain⋅e de vouloir annuler votre participation à l'évènement « {title} » ?",
"Are you sure you want to delete this event? This action cannot be reverted.": "Êtes-vous certain⋅e de vouloir supprimer cet évènement ? Cette action ne peut être annulée.",