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…",

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.",
"Avatar": "Avatar",
"Back to previous page": "Retour à la page précédente",
"Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
"By {name}": "Par {name}",
"Cancel": "Annuler",
"Cancel anonymous participation": "Annuler ma participation anonyme",
"Cancel creation": "Annuler la création",
"Cancel edition": "Annuler la modification",
"Cancel my participation request…": "Annuler ma demande de participation…",
"Cancel my participation…": "Annuler ma participation…",
"Cancel": "Annuler",
"Cancelled: Won't happen": "Annulé : N'aura pas lieu",
"Category": "Catégorie",
"Change": "Modifier",
"Change my identity…": "Changer mon identité…",
"Change my password": "Modifier mon mot de passe",
"Change password": "Modifier mot de passe",
"Change": "Modifier",
"Clear": "Effacer",
"Click to select": "Cliquez pour sélectionner",
"Click to upload": "Cliquez pour uploader",
"Close": "Fermé",
"Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)",
"Close": "Fermé",
"Closed": "Fermé",
"Comment deleted": "Commentaire supprimé",
"Comment from @{username} reported": "Commentaire de @{username} signalé",
"Comments": "Commentaires",
"Comments have been closed.": "Les commentaires sont fermés.",
"Comments on the event page": "Commentaires sur la page de l'événement",
"Comments": "Commentaires",
"Confirm my particpation": "Confirmer ma participation",
"Confirmed: Will happen": "Confirmé : aura lieu",
"Continue editing": "Continuer la modification",
"Country": "Pays",
"Create": "Créer",
"Create a new event": "Créer un nouvel évènement",
"Create a new group": "Créer un nouveau groupe",
"Create a new identity": "Créer une nouvelle identité",
@ -66,13 +73,18 @@
"Create my profile": "Créer mon profil",
"Create token": "Créer un jeton",
"Create, edit or delete events": "Créer, modifier ou supprimer des évènements",
"Create": "Créer",
"Creator": "Créateur",
"Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet évènement.",
"Custom URL": "URL personnalisée",
"Custom text": "Texte personnalisé",
"Custom": "Custom",
"Dashboard": "Tableau de bord",
"Date": "Date",
"Date and time settings": "Paramètres de date et d'heure",
"Date parameters": "Paramètres de date",
"Delete": "Supprimer",
"Date": "Date",
"Default Mobilizon.org terms": "Conditions d'utilisation par défaut de Mobilizon.org",
"Default": "Default",
"Delete Comment": "Supprimer le commentaire",
"Delete Event": "Supprimer l'évènement",
"Delete event": "Supprimer un évènement",
@ -80,6 +92,7 @@
"Delete your identity": "Supprimer votre identité",
"Delete {eventTitle}": "Supprimer {eventTitle}",
"Delete {preferredUsername}": "Supprimer {preferredUsername}",
"Delete": "Supprimer",
"Deleting comment": "Suppression du commentaire en cours",
"Deleting event": "Suppression de l'évènement",
"Description": "Description",
@ -91,15 +104,18 @@
"Drafts": "Brouillons",
"Edit": "Modifier",
"Eg: Stockholm, Dance, Chess…": "Par exemple : Lyon, Danse, Bridge…",
"Either on the {instance} instance or on another instance.": "Sur l'instance {instance} ou bien sur une autre instance.",
"Either the account is already validated, either the validation token is incorrect.": "Soit le compte est déjà validé, soit le jeton de validation est 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…": "Se termine le…",
"Enjoy discovering Mobilizon!": "Amusez-vous bien en découvrant Mobilizon !",
"Enter the link URL": "Entrez l'URL du lien",
"Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.": "Entrez vos propres conditions d'utilisations. Les balises HTML sont autorisées. Les conditions d'utilisation par défaut de Mobilizon.org sont fournies comme modèle.",
"Error while communicating with the server.": "Erreur de communication avec le serveur.",
"Error while saving report.": "Erreur lors de l'enregistrement du signalement.",
"Error while validating account": "Erreur lors de la validation du compte",
"Event": "Événement",
"Error while validating participation": "Error lors de la validation de la participation",
"Event already passed": "Événement déjà passé",
"Event cancelled": "Événement annulé",
"Event creation": "Création d'évènement",
@ -110,10 +126,12 @@
"Event to be confirmed": "Événement à confirmer",
"Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
"Event {eventTitle} reported": "Événement {eventTitle} signalé",
"Event": "Événement",
"Events": "Événements",
"Ex: test.mobilizon.org": "Ex : test.mobilizon.org",
"Exclude": "Exclure",
"Explore": "Explorer",
"Failed to save admin settings": "Échec de la sauvegarde des paramètres administrateur",
"Featured events": "Événements à la une",
"Features": "Fonctionnalités",
"Find an address": "Trouver une adresse",
@ -123,12 +141,13 @@
"For instance: London, Taekwondo, Architecture…": "Par exemple : Lyon, Taekwondo, Architecture…",
"Forgot your password ?": "Mot de passe oublié ?",
"From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "De lanniversaire entre ami·e·s à une marche pour le climat, aujourdhui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment sorganiser, comment cliquer sur «je participe» sans <b>livrer des données intimes</b> à Facebook ou<b> senfermer</b> dans MeetUp?",
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
"Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser",
"General information": "Informations générales",
"Getting location": "Récupération de la position",
"Go": "Allons-y",
"Going as {name}": "En tant que {name}",
"Group List": "Liste de groupes",
"Group full name": "Nom complet du groupe",
@ -138,7 +157,11 @@
"Headline picture": "Image à la une",
"Hide replies": "Masquer les réponses",
"I create an identity": "Je crée une identité",
"I don't have a Mobilizon account": "Je n'ai pas de compte Mobilizon",
"I have a Mobilizon account": "J'ai un compte Mobilizon",
"I have an account on another Mobilizon instance.": "J'ai un compte sur une autre instance Mobilizon.",
"I participate": "Je participe",
"I want to allow people to participate without an account.": "Je veux permettre aux gens de participer sans avoir un compte.",
"I want to approve every participation request": "Je veux approuver chaque demande de participation",
"Identity {displayName} created": "Identité {displayName} créée",
"Identity {displayName} deleted": "Identité {displayName} supprimée",
@ -148,12 +171,17 @@
"Impossible to login, your email or password seems incorrect.": "Impossible de se connecter, votre email ou bien votre mot de passe semble incorrect.",
"In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.": "D'ici là, veuillez considérer que le logiciel n'est pas (encore) fini. Plus d'informations {onBlog}.",
"Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.": "Installer Mobilizon permettra à des collectifs de sémanciper des outils des géants du web en créant <b>leur propre plateforme dévènements</b>.",
"Instance Description": "Description de l'instance ",
"Instance Name": "Nom de l'instance",
"Instance Terms Source": "Source des conditions d'utilisation de l'instance",
"Instance Terms URL": "URL des conditions générales de l'instance",
"Instance Terms": "Conditions générales de l'instance",
"Instances": "Instances",
"Join {instance}, a Mobilizon instance": "Rejoignez {instance}, une instance Mobilizon",
"Last published event": "Dernier évènement publié",
"Last week": "La semaine dernière",
"Learn more": "En apprendre plus",
"Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon",
"Learn more": "En apprendre plus",
"Leave event": "Annuler ma participation à l'évènement",
"Leaving event \"{title}\"": "Annuler ma participation à l'évènement",
"Let's create a new common": "Créons un nouveau Common",
@ -163,11 +191,13 @@
"Locality": "Commune",
"Log in": "Se connecter",
"Log out": "Se déconnecter",
"Login": "Se connecter",
"Login on Mobilizon!": "Se connecter sur Mobilizon !",
"Login on {instance}": "Se connecter sur {instance}",
"Login": "Se connecter",
"Manage participations": "Gérer les participations",
"Mark as resolved": "Marquer comme résolu",
"Members": "Membres",
"Mobilizon is a federated network. You can interact with this event from a different server.": "Mobilizon est un réseau fédéré. Vous pouvez interagir avec cet événement depuis un serveur différent.",
"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 est un logiciel libre qui permettra à des communautés de <b>créer leurs propres espaces</b> de publication dévènements, afin de mieux sémanciper des géants du web.",
"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 est en cours de développement, nous ajouterons de nouvelles fonctionnalités à ce site lors de mises à jour régulières, jusqu'à la publication de <b>la version 1 du logiciel au premier semestre 2020</b>.",
"Mobilizons licence": "La licence de Mobilizon",
@ -199,31 +229,34 @@
"Number of places": "Nombre de places",
"OK": "OK",
"Old password": "Ancien mot de passe",
"On {date}": "Le {date}",
"On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}",
"On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
"On {date}": "Le {date}",
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
"Only alphanumeric characters and underscores are supported.": "Seuls les caractères alphanumériques et les tirets bas sont acceptés.",
"Open": "Ouvert",
"Opened reports": "Signalements ouverts",
"Organized": "Organisés",
"Or": "Ou",
"Organized by {name}": "Organisé par {name}",
"Organized": "Organisés",
"Organizer": "Organisateur",
"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",
"Participate using your email address": "Participer en utilisant votre adresse email",
"Participate": "Participer",
"Participation approval": "Validation des participations",
"Participation requested!": "Participation demandée !",
"Password": "Mot de passe",
"Password (confirmation)": "Mot de passe (confirmation)",
"Password change": "Changement de mot de passe",
"Password reset": "Réinitialisation du mot de passe",
"Password": "Mot de passe",
"Past events": "Événements passés",
"Pending": "En attente",
"Pick an identity": "Choisissez une identité",
@ -236,6 +269,7 @@
"Post a comment": "Ajouter un commentaire",
"Post a reply": "Envoyer une réponse",
"Postal Code": "Code postal",
"Privacy Policy": "Politique de confidentialité",
"Private event": "Événement privé",
"Private feeds": "Flux privés",
"Public RSS/Atom Feed": "Flux RSS/Atom public",
@ -247,39 +281,46 @@
"Published events": "Événements publiés",
"RSS/Atom Feed": "Flux RSS/Atom",
"Read Framasofts statement of intent on the Framablog": "Lire la note dintention de Framasoft sur le Framablog",
"Redirecting to event…": "Redirection vers l'événement…",
"Region": "Région",
"Register": "S'inscrire",
"Register an account on Mobilizon!": "S'inscrire sur Mobilizon !",
"Register for an event by choosing one of your identities": "S'inscrire à un évènement en choisissant une de vos identités",
"Register": "S'inscrire",
"Registration is allowed, anyone can register.": "Les inscriptions sont autorisées, n'importe qui peut s'inscrire.",
"Registration is closed.": "Les inscriptions sont fermées.",
"Registration is currently closed.": "Les inscriptions sont actuellement fermées.",
"Registrations are restricted by whitelisting.": "Les inscriptions sont restreintes par liste blanche.",
"Reject": "Rejetter",
"Rejected": "Rejetés",
"Rejected participations": "Participations rejetées",
"Rejected": "Rejetés",
"Reopen": "Réouvrir",
"Reply": "Répondre",
"Report": "Signalement",
"Report this comment": "Signaler ce commentaire",
"Report this event": "Signaler cet évènement",
"Reported": "Signalée",
"Reported by": "Signalée par",
"Report": "Signalement",
"Reported by someone on {domain}": "Signalé par quelqu'un depuis {domain}",
"Reported by {reporter}": "Signalé par {reporter}",
"Reported by": "Signalée par",
"Reported identity": "Identité signalée",
"Reported": "Signalée",
"Reports": "Signalements",
"Requests": "Requêtes",
"Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
"Reset my password": "Réinitialiser mon mot de passe",
"Resolved": "Résolu",
"Save": "Enregistrer",
"Resource provided is not an URL": "La ressource fournie n'est pas une URL",
"Save draft": "Enregistrer le brouillon",
"Search": "Rechercher",
"Save": "Enregistrer",
"Search events, groups, etc.": "Rechercher des évènements, des groupes, etc.",
"Search results: \"{search}\"": "Résultats de recherche : « {search} »",
"Search": "Rechercher",
"Searching…": "Recherche en cours…",
"Send email": "Envoyer un email",
"Send me an email to reset my password": "Envoyez-moi un email pour réinitialiser mon mot de passe",
"Send me the confirmation email once again": "Envoyez-moi l'email de confirmation encore une fois",
"Send the report": "Envoyer le signalement",
"Set an URL to a page with your own terms.": "Entrez une URL vers une page web avec vos propres conditions d'utilisation.",
"Settings": "Paramètres",
"Share this event": "Partager l'évènement",
"Show map": "Afficher la carte",
"Show remaining number of places": "Afficher le nombre de places restantes",
@ -291,21 +332,27 @@
"Status": "Statut",
"Street": "Rue",
"Tentative: Will be confirmed later": "Provisoire : sera confirmé plus tard",
"Terms": "Conditions d'utilisation",
"The actual number of participants may differ, as this event is hosted on another instance.": "Le nombre réel de participants peut être différent, car cet événement provient d'une autre instance.",
"The content came from another server. Transfer an anonymous copy of the report?": "Le contenu provient d'une autre instance. Transférer une copie anonyme du signalement ?",
"The current identity doesn't have any permission on this event. You should probably change it.": "L'identité actuelle n'a pas de permissions sur cet évènement. Vous devriez probablement en changer.",
"The draft event has been updated": "L'évènement brouillon a été mis à jour",
"The event has been created as a draft": "L'évènement a été créé en tant que brouillon",
"The event has been published": "L'évènement a été publié",
"The event has been updated": "L'évènement a été mis à jour",
"The event has been updated and published": "L'évènement a été mis à jour et publié",
"The event has been updated": "L'évènement a été mis à jour",
"The event organizer didn't add any description.": "L'organisateur de l'évènement n'a pas ajouté de description.",
"The event title will be ellipsed.": "Le titre de l'évènement sera ellipsé.",
"The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
"The password was successfully changed": "Le mot de passe a été changé avec succès",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.",
"The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.": "Le compte utilisateur avec lequel vous essayez de vous connectez n'a pas été confirmé. Vérifiez la boite de réception de votre adresse email et éventuellement le dossier des messages indésirables.",
"The {default_terms} will be used. They will be translated in the user's language.": "Les {default_terms} seront utilisées. Elles seront traduites dans la langue de l'utilisateur⋅ice.",
"There are {participants} participants.": "Il n'y a qu'un⋅e participant⋅e. | Il y a {participants} participants.",
"These events may interest you": "Ces évènements peuvent vous intéresser",
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur⋅ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par email.",
"This email is already registered as participant for this event": "Cet email est déjà enregistré comme participant pour cet événement",
"This information is saved only on your computer. Click for details": "Cette information est sauvegardée uniquement sur votre appareil. Cliquez pour plus de details",
"This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "Cette installation (appelée “instance“) peut facilement {interconnect}, grâce à {protocol}.",
"This instance isn't opened to registrations, but you can register on other instances.": "Cette instance n'autorise pas les inscriptions, mais vous pouvez vous enregistrer sur d'autres instances.",
"This is a demonstration site to test the beta version of Mobilizon.": "Ceci est un site de démonstration permettant de tester la version bêta de Mobilizon.",
@ -317,11 +364,12 @@
"To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de lidentité « {preferredUsername} »",
"Transfer to {outsideDomain}": "Transférer à {outsideDomain}",
"Type": "Type",
"URL": "URL",
"Unfortunately, this instance isn't opened to registrations": "Malheureusement, cette instance n'est pas ouverte aux inscriptions",
"Unfortunately, your participation request was rejected by the organizers.": "Malheureusement, votre demande de participation a été refusée par les organisateur⋅ices.",
"Unknown": "Inconnu",
"Unknown actor": "Acteur inconnu",
"Unknown error.": "Erreur inconnue.",
"Unknown": "Inconnu",
"Unsaved changes": "Modifications non enregistrées",
"Upcoming": "À venir",
"Update event {name}": "Mettre à jour l'évènement {name}",
@ -339,6 +387,7 @@
"Warning": "Attention",
"We just sent an email to {email}": "Nous venons d'envoyer un email à {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>.": "Nous voulons développer un <b>commun numérique</b>, que tout le monde pourra sapproprier, conçu dans <b>le respect de la vie privée et de laction militante</b>.",
"We will redirect you to your instance in order to interact with this event": "Nous vous redirigerons vers votre instance pour interagir avec cet événement",
"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.": "On ne changera pas le monde depuis Facebook. Loutil dont nous rêvons, les entreprises du capitalisme de surveillance sont incapables de le produire, car elles ne sauraient pas en tirer profit. Cest loccasion de faire mieux quelles, en faisant autrement.",
"Website / URL": "Site web / URL",
"Welcome back {username}!": "Bon retour {username} !",
@ -351,6 +400,8 @@
"You and one other person are going to this event": "Vous êtes le ou la seule à vous rendre à cet évènement | Vous et une autre personne vous rendez à cet évènement | Vous et {approved} autres personnes vous rendez à cet évènement.",
"You are already a participant of this event.": "Vous participez déjà à cet évènement.",
"You are already logged-in.": "Vous êtes déjà connecté.",
"You are participating in this event anonymously but didn't confirm participation": "Vous participez à cet événement anonymement mais vous n'avez pas confirmé votre participation",
"You are participating in this event anonymously": "Vous participez à cet événement anonymement",
"You can add tags by hitting the Enter key or by adding a comma": "Vous pouvez ajouter des tags en appuyant sur la touche Entrée ou bien en ajoutant une virgule",
"You can try another search term or drag and drop the marker on the map": "Vous pouvez essayer avec d'autres termes de recherche ou bien glisser et déposer le marqueur sur la carte",
"You can't remove your last identity.": "Vous ne pouvez pas supprimer votre dernière identité.",
@ -362,25 +413,34 @@
"You have one event tomorrow.": "Vous n'avez pas d'évènement demain | Vous avez un évènement demain. | Vous avez {count} évènements demain",
"You may also ask to {resend_confirmation_email}.": "Vous pouvez aussi demander à {resend_confirmation_email}.",
"You need to login.": "Vous devez vous connecter.",
"You will be redirected to the original instance": "Vous allez être redirigé⋅e vers l'instance d'origine",
"You wish to participate to the following event": "Vous souhaitez participer à l'événement suivant",
"Your account has been validated": "Votre compte a été validé",
"Your account is being validated": "Votre compte est en cours de validation",
"Your account is nearly ready, {username}": "Votre compte est presque prêt, {username}",
"Your email is not whitelisted, you can't register.": "Votre email n'est pas sur la liste blanche, vous ne pouvez pas vous inscrire.",
"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.": "Votre email sera uniquement utilisé pour confirmer que vous êtes bien une personne réelle et vous envoyer des éventuelles mises à jour pour cet événement. Il ne sera PAS transmis à d'autres instances ou à l'organisateur de l'événement.",
"Your federated identity profile@instance": "Votre identité fédérée profil@instance",
"Your federated identity": "Votre identité fédérée",
"Your local administrator resumed its policy:": "Votre administrateur local a résumé sa politique ainsi :",
"Your participation has been confirmed": "Votre participation a été confirmée",
"Your participation has been rejected": "Votre participation a été rejettée",
"Your participation has been requested": "Votre participation a été demandée",
"Your participation has been validated": "Votre participation a été validée",
"Your participation is being validated": "Votre participation est en cours de validation",
"Your participation status has been changed": "Le statut de votre participation a été mis à jour",
"[This comment has been deleted]": "[Ce commentaire a été supprimé]",
"[deleted]": "[supprimé]",
"a decentralised federation protocol": "un protocole de fédération décentralisée",
"as {identity}": "en tant que {identity}",
"default Mobilizon terms": "conditions d'utilisation par défaut de Mobilizon.org",
"e.g. 10 Rue Jangot": "par exemple : 10 Rue Jangot",
"firstDayOfWeek": "1",
"iCal Feed": "Flux iCal",
"interconnect with others like it": "sinterconnecter simplement avec dautres",
"its source code is public": "son code source est public",
"on our blog": "sur notre blog",
"profile@instance": "profil@instance",
"resend confirmation email": "réenvoyer l'email de confirmation",
"respect of the fundamental freedoms": "le respect des libertés fondamentales",
"with another identity…": "avec une autre identité…",

View File

@ -1,14 +1,82 @@
import { mixins } from 'vue-class-component';
import { Component, Vue } from 'vue-property-decorator';
import { IEvent, IParticipant } from '@/types/event.model';
import { DELETE_EVENT } from '@/graphql/event';
import { IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { DELETE_EVENT, EVENT_PERSON_PARTICIPATION, FETCH_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { RouteName } from '@/router';
import { IPerson } from '@/types/actor';
import { IActor, IPerson } from '@/types/actor';
@Component
export default class EventMixin extends mixins(Vue) {
async openDeleteEventModal(event: IEvent, currentActor: IPerson) {
protected async leaveEvent(
event: IEvent,
actorId: number,
token: String|null = null,
anonymousParticipationConfirmed: boolean|null = null,
) {
try {
const { data } = await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
mutation: LEAVE_EVENT,
variables: {
eventId: event.id,
actorId,
token,
},
update: (store, { data }) => {
if (data == null) return;
let participation;
if (!token) {
const participationCachedData = store.readQuery<{ person: IPerson }>({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: event.id, actorId },
});
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error('Cannot update participation cache, because of null value.');
return;
}
participation = person.participations[0];
person.participations = [];
store.writeQuery({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: event.id, actorId },
data: { person },
});
}
const eventCachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: event.uuid } });
if (eventCachedData == null) return;
const { event: eventCached } = eventCachedData;
if (eventCached === null) {
console.error('Cannot update event cache, because of null value.');
return;
}
if (participation && participation.role === ParticipantRole.NOT_APPROVED) {
eventCached.participantStats.notApproved = eventCached.participantStats.notApproved - 1;
} else if (anonymousParticipationConfirmed === false) {
eventCached.participantStats.notConfirmed = eventCached.participantStats.notApproved - 1;
} else {
eventCached.participantStats.going = eventCached.participantStats.going - 1;
eventCached.participantStats.participant = eventCached.participantStats.participant - 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: event.uuid }, data: { event: eventCached } });
},
});
if (data) {
this.participationCancelledMessage();
}
} catch (error) {
console.error(error);
}
}
private participationCancelledMessage() {
this.$notifier.success(this.$t('You have cancelled your participation') as string);
}
protected async openDeleteEventModal(event: IEvent, currentActor: IPerson) {
const participantsLength = event.participantStats.participant;
const prefix = participantsLength
? this.$tc('There are {participants} participants.', event.participantStats.participant, {

View File

@ -3,10 +3,12 @@ import Dashboard from '@/views/Admin/Dashboard.vue';
import Follows from '@/views/Admin/Follows.vue';
import Followings from '@/components/Admin/Followings.vue';
import Followers from '@/components/Admin/Followers.vue';
import Settings from '@/views/Admin/Settings.vue';
export enum AdminRouteName {
DASHBOARD = 'Dashboard',
RELAYS = 'Relays',
ADMIN_SETTINGS = 'ADMIN_SETTINGS',
RELAY_FOLLOWINGS = 'Followings',
RELAY_FOLLOWERS = 'Followers',
}
@ -19,6 +21,13 @@ export const adminRoutes: RouteConfig[] = [
props: true,
meta: { requiredAuth: true },
},
{
path: '/admin/settings',
name: AdminRouteName.ADMIN_SETTINGS,
component: Settings,
props: true,
meta: { requiredAuth: true },
},
{
path: '/admin/relays',
name: AdminRouteName.RELAYS,

View File

@ -1,7 +1,10 @@
import EventList from '@/views/Event/EventList.vue';
import Location from '@/views/Location.vue';
import { RouteConfig } from 'vue-router';
import { RouteName } from '@/router/index';
import ParticipationWithAccount from '@/components/Participation/ParticipationWithAccount.vue';
import UnloggedParticipation from '@/components/Participation/UnloggedParticipation.vue';
import ParticipationWithoutAccount from '@/components/Participation/ParticipationWithoutAccount.vue';
import ConfirmParticipation from '@/components/Participation/ConfirmParticipation.vue';
// tslint:disable:space-in-parens
const participations = () => import(/* webpackChunkName: "participations" */ '@/views/Event/Participants.vue');
@ -19,6 +22,10 @@ export enum EventRouteName {
EDIT_EVENT = 'EditEvent',
PARTICIPATIONS = 'Participations',
EVENT = 'Event',
EVENT_PARTICIPATE_WITH_ACCOUNT = 'EVENT_PARTICIPATE_WITH_ACCOUNT',
EVENT_PARTICIPATE_WITHOUT_ACCOUNT = 'EVENT_PARTICIPATE_WITHOUT_ACCOUNT',
EVENT_PARTICIPATE_LOGGED_OUT = 'EVENT_PARTICIPATE_LOGGED_OUT',
EVENT_PARTICIPATE_CONFIRM = 'EVENT_PARTICIPATE_CONFIRM',
LOCATION = 'Location',
TAG = 'Tag',
}
@ -75,6 +82,30 @@ export const eventRoutes: RouteConfig[] = [
props: true,
meta: { requiredAuth: false },
},
{
path: '/events/:uuid/participate',
name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT,
component: UnloggedParticipation,
props: true,
},
{
path: '/events/:uuid/participate/with-account',
name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
component: ParticipationWithAccount,
props: true,
},
{
path: '/events/:uuid/participate/without-account',
name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT,
component: ParticipationWithoutAccount,
props: true,
},
{
path: '/participation/email/confirm/:token',
name: EventRouteName.EVENT_PARTICIPATE_CONFIRM,
component: ConfirmParticipation,
props: true,
},
{
path: '/tag/:tag',
name: EventRouteName.TAG,

View File

@ -19,6 +19,8 @@ enum GlobalRouteName {
ABOUT = 'About',
PAGE_NOT_FOUND = 'PageNotFound',
SEARCH = 'Search',
TERMS = 'TERMS',
INTERACT = 'INTERACT',
}
function scrollBehavior(to, from, savedPosition) {
@ -79,6 +81,18 @@ const router = new Router({
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue'),
meta: { requiredAuth: false },
},
{
path: '/terms',
name: RouteName.TERMS,
component: () => import(/* webpackChunkName: "cookies" */ '@/views/Terms.vue'),
meta: { requiredAuth: false },
},
{
path: '/interact',
name: RouteName.INTERACT,
component: () => import(/* webpackChunkName: "cookies" */ '@/views/Interact.vue'),
meta: { requiredAuth: false },
},
{
path: '/404',
name: RouteName.PAGE_NOT_FOUND,

View File

@ -0,0 +1,128 @@
import { IEvent } from '@/types/event.model';
const ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY = 'ANONYMOUS_PARTICIPATIONS';
interface IAnonymousParticipation {
token: String;
expiration: Date;
confirmed: boolean;
}
class AnonymousParticipationNotFoundError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = AnonymousParticipationNotFoundError.name;
}
}
/**
* Fetch existing anonymous participations saved inside this browser
*/
function getLocalAnonymousParticipations(): Map<String, IAnonymousParticipation> {
return jsonToMap(localStorage.getItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY) || mapToJson(new Map()));
}
function mapToJson(map): string {
return JSON.stringify([...map]);
}
function jsonToMap(jsonStr): Map<String, IAnonymousParticipation> {
return new Map(JSON.parse(jsonStr));
}
/**
* Purge participations which expiration has been reached
* @param participations Map
*/
function purgeOldParticipations(participations: Map<String, IAnonymousParticipation>): Map<String, IAnonymousParticipation> {
for (const [hashedUUID, { expiration }] of participations) {
if (expiration < new Date()) {
participations.delete(hashedUUID);
}
}
return participations;
}
/**
* Insert a participation in the list of anonymous participations
* @param hashedUUID
* @param participation
*/
function insertLocalAnonymousParticipation(hashedUUID: String, participation: IAnonymousParticipation) {
const participations = purgeOldParticipations(getLocalAnonymousParticipations());
participations.set(hashedUUID, participation);
localStorage.setItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY, mapToJson(participations));
}
function buildExpiration(event: IEvent): Date {
const expiration = event.endsOn || event.beginsOn;
expiration.setMonth(expiration.getMonth() + 3);
expiration.setDate(1);
return expiration;
}
async function addLocalUnconfirmedAnonymousParticipation(event: IEvent, cancellationToken: string) {
/**
* We hash the event UUID so that we can't know which events an anonymous user goes by looking up it's localstorage
*/
const hashedUUID = await digestMessage(event.uuid);
/**
* We round expiration to first day of next 3 months so that it's difficult to find event from date
*/
const expiration = buildExpiration(event);
insertLocalAnonymousParticipation(hashedUUID, { token: cancellationToken, expiration, confirmed: false });
}
async function confirmLocalAnonymousParticipation(uuid: String) {
const participations = purgeOldParticipations(getLocalAnonymousParticipations());
const hashedUUID = await digestMessage(uuid);
const participation = participations.get(hashedUUID);
if (participation) {
participation.confirmed = true;
participations.set(hashedUUID, participation);
localStorage.setItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY, mapToJson(participations));
}
}
async function isParticipatingInThisEvent(eventUUID: String): Promise<boolean> {
const participation = await getParticipation(eventUUID);
return participation !== undefined && participation.confirmed;
}
async function getParticipation(eventUUID: String): Promise<IAnonymousParticipation> {
const hashedUUID = await digestMessage(eventUUID);
const participation = purgeOldParticipations(getLocalAnonymousParticipations()).get(hashedUUID);
if (participation) {
return participation;
}
throw new AnonymousParticipationNotFoundError('Participation not found');
}
async function getLeaveTokenForParticipation(eventUUID: String): Promise<String> {
return (await getParticipation(eventUUID)).token;
}
async function removeAnonymousParticipation(eventUUID: String): Promise<void> {
const hashedUUID = await digestMessage(eventUUID);
const participations = purgeOldParticipations(getLocalAnonymousParticipations());
participations.delete(hashedUUID);
localStorage.setItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY, mapToJson(participations));
}
async function digestMessage(message): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
export {
addLocalUnconfirmedAnonymousParticipation,
confirmLocalAnonymousParticipation,
getLeaveTokenForParticipation,
isParticipatingInThisEvent,
removeAnonymousParticipation,
AnonymousParticipationNotFoundError,
};

View File

@ -7,3 +7,18 @@ export interface IDashboard {
numberOfComments: number;
numberOfReports: number;
}
export enum InstanceTermsType {
DEFAULT = 'DEFAULT',
URL = 'URL',
CUSTOM = 'CUSTOM',
}
export interface IAdminSettings {
instanceName: string;
instanceDescription: string;
instanceTerms: string;
instanceTermsType: InstanceTermsType;
instanceTermsUrl: string|null;
registrationsOpen: boolean;
}

View File

@ -1,3 +1,5 @@
import { InstanceTermsType } from '@/types/admin.model';
export interface IConfig {
name: string;
description: string;
@ -11,6 +13,33 @@ export interface IConfig {
longitude: number;
accuracyRadius: number;
};
anonymous: {
participation: {
allowed: boolean;
validation: {
email: {
enabled: boolean;
confirmationRequired: boolean;
},
captcha: {
enabled: boolean;
},
}
}
eventCreation: {
allowed: boolean;
validation: {
email: {
enabled: boolean;
confirmationRequired: boolean;
},
captcha: {
enabled: boolean,
},
}
}
actorId,
};
maps: {
tiles: {
endpoint: string;
@ -21,4 +50,9 @@ export interface IConfig {
provider: string;
autocomplete: boolean;
};
terms: {
bodyHtml: string;
type: InstanceTermsType;
url: string;
};
}

View File

@ -31,6 +31,7 @@ export enum EventVisibilityJoinOptions {
export enum ParticipantRole {
NOT_APPROVED = 'NOT_APPROVED',
NOT_CONFIRMED = 'NOT_CONFIRMED',
REJECTED = 'REJECTED',
PARTICIPANT = 'PARTICIPANT',
MODERATOR = 'MODERATOR',
@ -58,6 +59,7 @@ export interface IParticipant {
role: ParticipantRole;
actor: IActor;
event: IEvent;
metadata: { cancellationToken?: string };
}
export class Participant implements IParticipant {
@ -65,6 +67,7 @@ export class Participant implements IParticipant {
event!: IEvent;
actor!: IActor;
role: ParticipantRole = ParticipantRole.NOT_APPROVED;
metadata = {};
constructor(hash?: IParticipant) {
if (!hash) return;
@ -73,6 +76,7 @@ export class Participant implements IParticipant {
this.event = new EventModel(hash.event);
this.actor = new Actor(hash.actor);
this.role = hash.role;
this.metadata = hash.metadata;
}
}
@ -96,6 +100,7 @@ export enum CommentModeration {
export interface IEventParticipantStats {
notApproved: number;
notConfirmed: number;
rejected: number;
participant: number;
creator: number;
@ -146,6 +151,7 @@ export interface IEventOptions {
maximumAttendeeCapacity: number;
remainingAttendeeCapacity: number;
showRemainingAttendeeCapacity: boolean;
anonymousParticipation: boolean;
offers: IOffer[];
participationConditions: IParticipationCondition[];
attendees: string[];
@ -160,6 +166,7 @@ export class EventOptions implements IEventOptions {
maximumAttendeeCapacity = 0;
remainingAttendeeCapacity = 0;
showRemainingAttendeeCapacity = false;
anonymousParticipation = false;
offers: IOffer[] = [];
participationConditions: IParticipationCondition[] = [];
attendees: string[] = [];
@ -197,7 +204,7 @@ export class EventModel implements IEvent {
publishAt = new Date();
participantStats = { notApproved: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
participantStats = { notApproved: 0, notConfirmed: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
participants: IParticipant[] = [];
relatedEvents: IEvent[] = [];

View File

@ -32,3 +32,11 @@ $navbar-height: 4rem;
// Footer
$footer-padding: 3rem 1.5rem 4rem;
$footer-background-color: $primary;
$body-background-color: #f8f7fa;
$fullhd-enabled: false;
$hero-body-padding-medium: 6rem 1.5rem;
main > .container {
background: $white;
}

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li class="is-active">

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<h1 class="title">{{ $t('Administration') }}</h1>
<div class="tile is-ancestor" v-if="dashboard">
<div class="tile is-vertical is-4">
@ -45,6 +45,13 @@
</article>
</router-link>
</div>
<div class="tile is-parent">
<router-link :to="{ name: RouteName.ADMIN_SETTINGS }">
<article class="tile is-child box">
<p class="subtitle">{{ $t('Settings') }}</p>
</article>
</router-link>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child box">

View File

@ -1,5 +1,5 @@
<template>
<div class="container">
<section class="section container">
<h1 class="title">{{ $t('Instances') }}</h1>
<div class="tabs is-boxed">
<ul>
@ -18,7 +18,7 @@
</ul>
</div>
<router-view></router-view>
</div>
</section>
</template>
<script lang="ts">

View File

@ -0,0 +1,94 @@
<template>
<section class="container section" v-if="adminSettings">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.DASHBOARD }">{{ $t('Dashboard') }}</router-link></li>
<li class="is-active"><router-link :to="{ name: RouteName.ADMIN_SETTINGS }" aria-current="page">{{ $t('Admin settings') }}</router-link></li>
</ul>
</nav>
<form @submit.prevent="updateSettings">
<b-field :label="$t('Instance Name')">
<b-input v-model="adminSettings.instanceName" />
</b-field>
<b-field :label="$t('Instance Description')">
<b-input type="textarea" v-model="adminSettings.instanceDescription" />
</b-field>
<b-field :label="$t('Allow registrations')">
<b-switch v-model="adminSettings.registrationsOpen">
<p class="content" v-if="adminSettings.registrationsOpen">{{ $t('Registration is allowed, anyone can register.')}}</p>
<p class="content" v-else>{{ $t('Registration is closed.')}}</p>
</b-switch>
</b-field>
<b-field :label="$t('Instance Terms Source')">
<div class="columns">
<div class="column is-one-quarter-desktop">
<b-field>
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.DEFAULT">{{ $t('Default Mobilizon.org terms')}}</b-radio>
</b-field>
<b-field>
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.URL">{{ $t('Custom URL')}}</b-radio>
</b-field>
<b-field>
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.CUSTOM">{{ $t('Custom text')}}</b-radio>
</b-field>
</div>
<div class="column">
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.DEFAULT">
<b>{{ $t('Default')}}</b>
<i18n tag="p" class="content" path="The {default_terms} will be used. They will be translated in the user's language.">
<a slot="default_terms" href="https://mobilizon.org/terms" target="_blank" rel="noopener">{{ $t('default Mobilizon terms')}}</a>
</i18n>
</div>
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.URL">
<b>{{ $t('URL')}}</b>
<p class="content">{{ $t("Set an URL to a page with your own terms.") }}</p>
</div>
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM">
<b>{{ $t('Custom')}}</b>
<p class="content">{{ $t("Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.") }}</p>
</div>
</div>
</div>
</b-field>
<b-field :label="$t('Instance Terms URL')" v-if="adminSettings.instanceTermsType === InstanceTermsType.URL">
<b-input type="URL" v-model="adminSettings.instanceTermsUrl" />
</b-field>
<b-field :label="$t('Instance Terms')" v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM">
<b-input type="textarea" v-model="adminSettings.instanceTerms" />
</b-field>
<b-button native-type="submit" type="is-primary">{{ $t('Save')}}</b-button>
</form>
</section>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS } from '@/graphql/admin';
import { IAdminSettings, InstanceTermsType } from '@/types/admin.model';
import { RouteName } from '@/router';
@Component({
apollo: {
adminSettings: ADMIN_SETTINGS,
},
})
export default class Settings extends Vue {
adminSettings!: IAdminSettings;
InstanceTermsType = InstanceTermsType;
RouteName = RouteName;
async updateSettings() {
try {
await this.$apollo.mutate({
mutation: SAVE_ADMIN_SETTINGS,
variables: {
...this.adminSettings,
},
});
this.$notifier.success(this.$t('Admin settings successfully saved.') as string);
} catch (e) {
console.error(e);
this.$notifier.error(this.$t('Failed to save admin settings') as string);
}
}
}
</script>

View File

@ -8,8 +8,7 @@
{{ $t('Update event {name}', { name: event.title }) }}
</h1>
<div class="columns is-centered">
<form class="column is-two-thirds-desktop" ref="form">
<form ref="form">
<h2 class="subtitle">
{{ $t('General information') }}
</h2>
@ -76,6 +75,17 @@
</b-radio>
</div> -->
<div class="field" v-if="config && config.anonymous.participation.allowed">
<label class="label">{{ $t('Anonymous participations') }}</label>
<b-switch v-model="event.options.anonymousParticipation">
{{ $t('I want to allow people to participate without an account.') }}
<small v-if="config.anonymous.participation.validation.email.confirmationRequired">
<br>
{{ $t('Anonymous participants will be asked to confirm their participation through e-mail.') }}
</small>
</b-switch>
</div>
<div class="field">
<label class="label">{{ $t('Participation approval') }}</label>
<b-switch v-model="needsApproval">
@ -165,7 +175,6 @@
</b-field>
</form>
</div>
</div>
<b-modal :active.sync="dateSettingsIsOpen" has-modal-card trap-focus>
<form action="">
<div class="modal-card" style="width: auto">
@ -227,6 +236,10 @@
<style lang="scss" scoped>
@import "@/variables.scss";
main section > .container {
background: $white;
}
h2.subtitle {
margin: 10px 0;
@ -239,8 +252,7 @@
section {
& > .container {
margin-bottom: 2rem;
padding: 1rem;
padding: 2rem 1.5rem;
}
nav.navbar {
@ -288,18 +300,17 @@ import { buildFileFromIPicture, buildFileVariable, readFileAsync } from '@/utils
import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
import { RouteName } from '@/router';
import 'intersection-observer';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
@Component({
components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor: EditorComponent },
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
tags: {
query: TAGS,
},
currentActor: CURRENT_ACTOR_CLIENT,
tags: TAGS,
config: CONFIG,
},
metaInfo() {
return {
@ -319,6 +330,7 @@ export default class EditEvent extends Vue {
currentActor = new Person();
tags: ITag[] = [];
event: IEvent = new EventModel();
config!: IConfig;
unmodifiedEvent!: IEvent;
pictureFile: File | null = null;

View File

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

View File

@ -1,10 +1,10 @@
<template>
<div class="container">
<div class="section container">
<h1 class="title">{{ $t('Explore') }}</h1>
<section class="hero">
<div class="hero-body">
<form @submit.prevent="submit()">
<b-field :label="$t('Event')" grouped label-position="on-border">
<b-field :label="$t('Event')" grouped group-multiline label-position="on-border">
<b-input icon="magnify" type="search" size="is-large" expanded v-model="searchTerm" :placeholder="$t('For instance: London, Taekwondo, Architecture…')" />
<p class="control">
<b-button @click="submit" type="is-info" size="is-large" v-bind:disabled="searchTerm.trim().length === 0">{{ $t('Search') }}</b-button>
@ -17,7 +17,7 @@
<b-loading :active.sync="$apollo.loading"></b-loading>
<h3 class="title">{{ $t('Featured events') }}</h3>
<div v-if="events.length > 0" class="columns is-multiline">
<div class="column is-one-quarter-desktop" v-for="event in events" :key="event.uuid">
<div class="column is-one-third-desktop" v-for="event in events" :key="event.uuid">
<EventCard
:event="event"
/>
@ -66,6 +66,16 @@ export default class Explore extends Vue {
</script>
<style scoped lang="scss">
@import "@/variables.scss";
main > .container {
background: $white;
.hero-body {
padding: 1rem 1.5rem;
}
}
h1.title {
margin-top: 1.5rem;
}

View File

@ -1,5 +1,5 @@
<template>
<main class="container">
<section class="section container">
<h1 class="title">
{{ $t('My events') }}
</h1>
@ -10,7 +10,7 @@
</h2>
<transition-group name="list" tag="p">
<div v-for="month in monthlyFutureParticipations" :key="month[0]">
<h3>{{ month[0] }}</h3>
<h3 class="upcoming-month">{{ month[0] }}</h3>
<EventListCard
v-for="participation in month[1]"
:key="participation.id"
@ -64,7 +64,7 @@
<b-message v-if="futureParticipations.length === 0 && pastParticipations.length === 0 && $apollo.loading === false" type="is-danger">
{{ $t('No events found') }}
</b-message>
</main>
</section>
</template>
<script lang="ts">
@ -212,13 +212,15 @@ export default class MyEvents extends Vue {
<style lang="scss" scoped>
@import "../../variables";
main > .container {
background: $white;
}
.participation {
margin: 1rem auto;
}
section {
margin: 3rem auto;
& > h2 {
display: block;
color: $primary;
@ -231,5 +233,9 @@ export default class MyEvents extends Vue {
margin-top: 2rem;
font-weight: bold;
}
.upcoming-month {
text-transform: capitalize;
}
}
</style>

View File

@ -3,15 +3,19 @@
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
<b-tab-item>
<template slot="header">
<b-icon icon="account-multiple"></b-icon>
<b-icon icon="account-multiple" />
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
</template>
<template>
<section v-if="participantsAndCreators.length > 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"
@ -24,7 +28,7 @@
</b-tab-item>
<b-tab-item :disabled="participantStats.notApproved === 0">
<template slot="header">
<b-icon icon="account-multiple-plus"></b-icon>
<b-icon icon="account-multiple-plus" />
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
</template>
<template>
@ -75,7 +79,8 @@ import { PARTICIPANTS, UPDATE_PARTICIPANT } from '@/graphql/event';
import ParticipantCard from '@/components/Account/ParticipantCard.vue';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
@Component({
components: {
@ -85,6 +90,7 @@ import { IPerson } from '@/types/actor';
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
config: CONFIG,
event: {
query: PARTICIPANTS,
variables() {
@ -159,6 +165,7 @@ export default class Participants extends Vue {
queue: IParticipant[] = [];
rejected: IParticipant[] = [];
event!: IEvent;
config!: IConfig;
ParticipantRole = ParticipantRole;
currentActor!: IPerson;
@ -179,6 +186,10 @@ export default class Participants extends Vue {
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;

View File

@ -1,6 +1,6 @@
<template>
<div>
<section class="hero is-medium is-light is-bold" v-if="config && (!currentUser.id || !currentActor.id)">
<section class="hero is-light is-bold" v-if="config && (!currentUser.id || !currentActor.id)">
<div class="hero-body">
<div class="container">
<h1 class="title">{{ $t('Gather ⋅ Organize ⋅ Mobilize') }}</h1>
@ -24,7 +24,7 @@
</div>
</div>
</section>
<div class="container" v-if="config">
<div class="container section" v-if="config">
<section v-if="currentActor.id">
<b-message type="is-info" v-if="welcomeBack">
{{ $t('Welcome back {username}!', { username: currentActor.displayName() }) }}
@ -318,14 +318,20 @@ export default class Home extends Vue {
<style lang="scss" scoped>
@import "@/variables.scss";
main > div > .container {
background: $white;
}
.section {
padding: 1rem 1.5rem;
}
.search-autocomplete {
border: 1px solid #dbdbdb;
color: rgba(0, 0, 0, 0.87);
}
.events-featured {
margin: 25px auto;
.columns {
margin: 1rem auto 3rem;
}

57
js/src/views/Interact.vue Normal file
View File

@ -0,0 +1,57 @@
<template>
<div class="container section">
<b-notification v-if="$apollo.queries.searchEvents.loading">{{ $t('Redirecting to event') }}</b-notification>
<b-notification v-if="$apollo.queries.searchEvents.skip" type="is-danger">{{ $t('Resource provided is not an URL') }}</b-notification>
</div>
</template>
<script lang="ts">
import {
Component,
Vue,
} from 'vue-property-decorator';
import { RouteName } from '@/router';
import { SEARCH_EVENTS } from '@/graphql/search';
import { IEvent } from '@/types/event.model';
@Component({
apollo: {
searchEvents: {
query: SEARCH_EVENTS,
variables() {
return {
searchText: this.$route.query.url,
};
},
skip() {
try {
const url = this.$route.query.url as string;
new URL(url);
return false;
} catch (e) {
if (e instanceof TypeError) {
return true;
}
}
},
async result({ data }) {
if (data.searchEvents && data.searchEvents.total > 0 && data.searchEvents.elements.length > 0) {
const event = data.searchEvents.elements[0];
return await this.$router.replace({ name: RouteName.EVENT, params: { uuid: event.uuid } });
}
},
},
},
})
export default class Interact extends Vue {
searchEvents!: IEvent[];
RouteName = RouteName;
}
</script>
<style lang="scss">
@import "@/variables.scss";
main > .container {
background: $white;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.DASHBOARD }">Dashboard</router-link></li>
@ -9,7 +9,7 @@
<ul v-if="actionLogs.length > 0">
<li v-for="log in actionLogs">
<div class="box">
<img class="image" :src="log.actor.avatar.url" />
<img class="image" :src="log.actor.avatar.url" v-if="log.actor.avatar" />
<span>@{{ log.actor.preferredUsername }}</span>
<span v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED">
closed <router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link>

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="container section">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<div class="container" v-if="report">
<nav class="breadcrumb" aria-label="breadcrumbs">
@ -133,7 +133,8 @@
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
<p>{{ note.content }}</p>
<router-link :to="{ name: RouteName.PROFILE, params: { name: note.moderator.preferredUsername } }">
<img alt="" class="image" :src="note.moderator.avatar.url" /> @{{ note.moderator.preferredUsername }}
<img alt="" class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
@{{ note.moderator.preferredUsername }}
</router-link><br />
<small><a :href="`#note-${note.id}`" v-if="note.insertedAt">{{ note.insertedAt | formatDateTimeString }}</a></small>
</div>

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="container section">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.DASHBOARD }">{{ $t('Dashboard') }}</router-link></li>

View File

@ -1,5 +1,5 @@
<template>
<section class="container has-text-centered not-found">
<section class="section container has-text-centered not-found">
<div class="columns is-vertical">
<div class="column is-centered">
<img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture">

64
js/src/views/Terms.vue Normal file
View File

@ -0,0 +1,64 @@
<template>
<div class="container section">
<h2 class="title">{{ $t('Privacy Policy')}}</h2>
<div class="content" v-html="config.terms.bodyHtml" />
</div>
</template>
<script lang="ts">
import {
Component,
Vue, Watch,
} from 'vue-property-decorator';
import { TERMS } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { RouteName } from '@/router';
import { InstanceTermsType } from '@/types/admin.model';
@Component({
apollo: {
config: {
query: TERMS,
variables() {
return {
locale: this.locale,
};
},
skip() {
return !this.locale;
},
},
},
})
export default class Terms extends Vue {
config!: IConfig;
locale: string|null = null;
created() {
this.locale = this.$i18n.locale;
}
@Watch('config', { deep: true })
watchConfig(config: IConfig) {
if (config.terms.type) {
console.log(this.config.terms);
this.redirectToUrl();
}
}
redirectToUrl() {
if (this.config.terms.type === InstanceTermsType.URL) {
window.location.replace(this.config.terms.url);
}
}
RouteName = RouteName;
}
</script>
<style lang="scss">
@import "@/variables.scss";
main > .container {
background: $white;
}
</style>

View File

@ -1,14 +1,13 @@
<template>
<div class="container">
<b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info">
{{ $t('You need to login.') }}
</b-message>
<section v-if="!currentUser.isLoggedIn">
<section class="section container" v-if="!currentUser.isLoggedIn">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">
{{ $t('Welcome back!') }}
</h1>
<b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info">
{{ $t('You need to login.') }}
</b-message>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
<span v-if="error === LoginError.USER_NOT_CONFIRMED">
<span>{{ $t("The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.") }}</span>
@ -64,11 +63,6 @@
</div>
</div>
</section>
<b-message v-else title="Error" type="is-error">
{{ $t('You are already logged-in.') }}
</b-message>
</div>
</template>
<script lang="ts">

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: RouteName.UPDATE_IDENTITY }">{{ $t('My account') }}</router-link></li>

View File

@ -1,5 +1,5 @@
<template>
<section class="container columns is-mobile is-centered">
<section class="section container columns is-mobile is-centered">
<div class="card column is-half-desktop">
<h1>
{{ $t('Password reset') }}

View File

@ -1,5 +1,5 @@
<template>
<div class="container">
<div class="section container">
<section class="hero">
<div class="hero-body">
<h1 class="title">

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">

View File

@ -1,5 +1,5 @@
<template>
<section class="container">
<section class="section container">
<h1 class="title" v-if="loading">
{{ $t('Your account is being validated') }}
</h1>

View File

@ -102,9 +102,9 @@ const errorLink = onError(({ graphQLErrors, networkError, forward, operation })
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
});
for (const message of messages) {
Snackbar.open({ message, type: 'is-danger', position: 'is-bottom' });
}
// for (const message of messages) {
// Snackbar.open({ message, type: 'is-danger', position: 'is-bottom' });
// }
}
if (networkError) {

View File

@ -2654,6 +2654,11 @@ builtin-status-codes@^3.0.0:
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
bulma-divider@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/bulma-divider/-/bulma-divider-0.2.0.tgz#a9b4d9fe8b270c7cb7573023c575062bc62616f3"
integrity sha512-REe3k56GECRfDaqFjC8cwLhV4RxXmV0RubuzDJqwior9wlJcdHlN0qfW0tvUX+qphikaTQegIeRuhjRIAqkjkw==
bulma@0.7.5:
version "0.7.5"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.5.tgz#35066c37f82c088b68f94450be758fc00a967208"

View File

@ -426,19 +426,25 @@ defmodule Mobilizon.Federation.ActivityPub do
# TODO Refactor me for federation
with {:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity, check_attendee_capacity(event)},
role <-
additional
|> Map.get(:metadata, %{})
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
{:ok, %Participant{} = participant} <-
Mobilizon.Events.create_participant(%{
role: :not_approved,
role: role,
event_id: event.id,
actor_id: actor.id,
url: Map.get(additional, :url)
url: Map.get(additional, :url),
metadata: Map.get(additional, :metadata)
}),
join_data <- Convertible.model_to_as(participant),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant),
{:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
:ok <- maybe_federate(activity) do
if event.local && Mobilizon.Events.get_default_participant_role(event) === :participant do
if event.local && Mobilizon.Events.get_default_participant_role(event) === :participant &&
role == :participant do
accept(
:join,
participant,
@ -464,19 +470,24 @@ defmodule Mobilizon.Federation.ActivityPub do
end
end
def leave(object, actor, local \\ true)
def leave(object, actor, local \\ true, additional \\ %{})
# TODO: If we want to use this for exclusion we need to have an extra field
# for the actor that excluded the participant
def leave(
%Event{id: event_id, url: event_url} = _event,
%Actor{id: actor_id, url: actor_url} = _actor,
local
local,
additional
) do
with {:only_organizer, false} <-
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
{:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id),
Mobilizon.Events.get_participant(
event_id,
actor_id,
Map.get(additional, :metadata, %{})
),
{:ok, %Participant{} = participant} <-
Events.delete_participant(participant),
leave_data <- %{
@ -604,6 +615,7 @@ defmodule Mobilizon.Federation.ActivityPub do
digest = Signature.build_digest(json)
date = Signature.generate_date_header()
# request_target = Signature.generate_request_target("POST", path)
signature =
@ -823,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub do
@spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any()
defp reject_join(%Participant{} = participant, additional) do
with {:ok, %Participant{} = participant} <-
Events.update_participant(participant, %{approved: false, role: :rejected}),
Events.update_participant(participant, %{role: :rejected}),
Absinthe.Subscription.publish(Endpoint, participant.actor,
event_person_participation_changed: participant.actor.id
),
@ -908,6 +920,18 @@ defmodule Mobilizon.Federation.ActivityPub do
args
end
# Check that we can only allow anonymous participation if our instance allows it
{_, options} =
Map.get_and_update(
Map.get(args, :options, %{anonymous_participation: false}),
:anonymous_participation,
fn value ->
{value, value && Mobilizon.Config.anonymous_participation?()}
end
)
args = Map.put(args, :options, options)
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
end

View File

@ -17,8 +17,6 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
alias Mobilizon.GraphQL.API.Follows
alias Mobilizon.Web.Endpoint
require Logger
def init do
@ -30,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
@spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()}
def get_actor do
with {:ok, %Actor{} = actor} <-
Actors.get_or_create_instance_actor_by_url("#{Endpoint.url()}/relay") do
Actors.get_or_create_internal_actor("relay") do
actor
end
end

View File

@ -73,6 +73,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
"joinMode" => %{
"@id" => "mz:joinMode",
"@type" => "mz:joinModeType"
},
"anonymousParticipationEnabled" => %{
"@id" => "mz:anonymousParticipationEnabled",
"@type" => "sc:Boolean"
}
}
]

View File

@ -121,6 +121,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
"repliesModerationOption" => event.options.comment_moderation,
"commentsEnabled" => event.options.comment_moderation == :allow_all,
"anonymousParticipationEnabled" => event.options.anonymous_participation,
# "draft" => event.draft,
"ical:status" => event.status |> to_string |> String.upcase(),
"id" => event.url,
@ -142,6 +143,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
defp get_options(object) do
%{
maximum_attendee_capacity: object["maximumAttendeeCapacity"],
anonymous_participation: object["anonymousParticipationEnabled"],
comment_moderation:
Map.get(
object,

View File

@ -10,11 +10,9 @@ defmodule Mobilizon.Federation.WebFinger do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.WebFinger.XmlBuilder
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes
require Jason
require Logger
@ -69,6 +67,10 @@ defmodule Mobilizon.Federation.WebFinger do
"rel" => "https://webfinger.net/rel/profile-page/",
"type" => "text/html",
"href" => actor.url
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Routes.page_url(Endpoint, :interact, uri: nil)}{uri}"
}
]
}

View File

@ -4,22 +4,26 @@ defmodule Mobilizon.GraphQL.API.Participations do
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Web.Email.Participation
@spec join(Event.t(), Actor.t()) :: {:ok, Participant.t()}
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor) do
with {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, activity, participant} <- ActivityPub.join(event, actor, true) do
@spec join(Event.t(), Actor.t(), map()) :: {:ok, Activity.t(), Participant.t()}
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor, args \\ %{}) do
with {:error, :participant_not_found} <-
Mobilizon.Events.get_participant(event_id, actor_id, args),
{:ok, activity, participant} <-
ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) do
{:ok, activity, participant}
end
end
def leave(%Event{} = event, %Actor{} = actor) do
with {:ok, activity, participant} <- ActivityPub.leave(event, actor, true) do
@spec leave(Event.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
def leave(%Event{} = event, %Actor{} = actor, args \\ %{}) do
with {:ok, activity, participant} <-
ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) do
{:ok, activity, participant}
end
end
@ -27,14 +31,23 @@ defmodule Mobilizon.GraphQL.API.Participations do
@doc """
Update participation status
"""
def update(%Participant{} = participation, %Actor{} = moderator, :participant) do
accept(participation, moderator)
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
def update(%Participant{} = participation, %Actor{} = moderator, :participant),
do: accept(participation, moderator)
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
def update(%Participant{} = participation, %Actor{} = _moderator, :not_approved) do
with {:ok, %Participant{} = participant} <-
Events.update_participant(participation, %{role: :not_approved}) do
{:ok, nil, participant}
end
end
def update(%Participant{} = participation, %Actor{} = moderator, :rejected) do
reject(participation, moderator)
end
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
def update(%Participant{} = participation, %Actor{} = moderator, :rejected),
do: reject(participation, moderator)
@spec accept(Participant.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
defp accept(
%Participant{} = participation,
%Actor{} = moderator
@ -51,6 +64,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
end
end
@spec reject(Participant.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
defp reject(
%Participant{} = participation,
%Actor{} = moderator

View File

@ -5,18 +5,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.{Actors, Admin, Config, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.ActionLog
alias Mobilizon.Events
alias Mobilizon.Admin.{ActionLog, Setting}
alias Mobilizon.Config
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Service.Statistics
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
alias Mobilizon.Federation.ActivityPub.Relay
def list_action_logs(
_parent,
%{page: page, limit: limit},
@ -132,6 +132,43 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, "You need to be logged-in and an administrator to access dashboard statistics"}
end
def get_settings(_parent, _args, %{
context: %{current_user: %User{role: role}}
})
when is_admin(role) do
{:ok,
%{
instance_description: Config.instance_description(),
instance_name: Config.instance_name(),
registrations_open: Config.instance_registrations_open?(),
instance_terms: Config.instance_terms(),
instance_terms_type: Config.instance_terms_type(),
instance_terms_url: Config.instance_terms_url()
}}
end
def get_settings(_parent, _args, _resolution) do
{:error, "You need to be logged-in and an administrator to access admin settings"}
end
def save_settings(_parent, args, %{
context: %{current_user: %User{role: role}}
})
when is_admin(role) do
with {:ok, res} <- Admin.save_settings("instance", args) do
res =
res |> Enum.map(fn {key, %Setting{value: value}} -> {key, value} end) |> Enum.into(%{})
Config.clear_config_cache()
{:ok, res}
end
end
def save_settings(_parent, _args, _resolution) do
{:error, "You need to be logged-in and an administrator to save admin settings"}
end
def list_relay_followers(
_parent,
%{page: page, limit: limit},

View File

@ -25,15 +25,72 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
_ -> nil
end
{:ok,
data = Map.merge(config_cache(), %{location: location, country_code: country_code})
{:ok, data}
end
def terms(_parent, %{locale: locale}, _resolution) do
type = Config.instance_terms_type()
{url, body_html} =
case type do
"URL" -> {Config.instance_terms_url(), nil}
"DEFAULT" -> {nil, Config.generate_terms(locale)}
_ -> {nil, Config.instance_terms(locale)}
end
{:ok, %{body_html: body_html, type: type, url: url}}
end
defp config_cache do
case Cachex.fetch(:config, "full_config", fn _key ->
case build_config_cache() do
value when not is_nil(value) -> {:commit, value}
err -> {:ignore, err}
end
end) do
{status, value} when status in [:ok, :commit] -> value
_err -> nil
end
end
defp build_config_cache do
%{
name: Config.instance_name(),
registrations_open: Config.instance_registrations_open?(),
registrations_whitelist: Config.instance_registrations_whitelist?(),
demo_mode: Config.instance_demo_mode?(),
description: Config.instance_description(),
location: location,
country_code: country_code,
anonymous: %{
participation: %{
allowed: Config.anonymous_participation?(),
validation: %{
email: %{
enabled: Config.anonymous_participation_email_required?(),
confirmation_required:
Config.anonymous_event_creation_email_confirmation_required?()
},
captcha: %{
enabled: Config.anonymous_event_creation_email_captcha_required?()
}
}
},
event_creation: %{
allowed: Config.anonymous_event_creation?(),
validation: %{
email: %{
enabled: Config.anonymous_event_creation_email_required?(),
confirmation_required:
Config.anonymous_event_creation_email_confirmation_required?()
},
captcha: %{
enabled: Config.anonymous_event_creation_email_captcha_required?()
}
}
},
actor_id: Config.anonymous_actor_id()
},
geocoding: %{
provider: Config.instance_geocoding_provider(),
autocomplete: Config.instance_geocoding_autocomplete()
@ -44,6 +101,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
attribution: Config.instance_maps_tiles_attribution()
}
}
}}
}
end
end

View File

@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
alias Mobilizon.{Actors, Admin, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Event, EventParticipantStats, Participant}
alias Mobilizon.Events.{Event, EventParticipantStats}
alias Mobilizon.Users.User
alias Mobilizon.GraphQL.API
@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
def list_events(_parent, %{page: page, limit: limit}, _resolution)
when limit < @event_max_limit do
{:ok, Mobilizon.Events.list_events(page, limit)}
{:ok, Events.list_events(page, limit)}
end
def list_events(_parent, %{page: _page, limit: _limit}, _resolution) do
@ -31,7 +31,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
%{uuid: uuid},
%{context: %{current_user: %User{id: user_id}}} = _resolution
) do
case {:has_event, Mobilizon.Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do
case {:has_event, Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do
{:has_event, %Event{} = event} ->
{:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))}
@ -45,7 +45,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
end
def find_event(parent, %{uuid: uuid} = args, resolution) do
case {:has_event, Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid)} do
case {:has_event, Events.get_public_event_by_uuid_with_preload(uuid)} do
{:has_event, %Event{} = event} ->
{:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))}
@ -65,7 +65,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission, Mobilizon.Events.moderator_for_event?(event_id, actor_id)} do
{:actor_approve_permission, Events.moderator_for_event?(event_id, actor_id)} do
roles =
case roles do
"" ->
@ -78,7 +78,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|> Enum.map(&String.to_existing_atom/1)
end
{:ok, Mobilizon.Events.list_participants_for_event(event_id, roles, page, limit)}
{:ok, Events.list_participants_for_event(event_id, roles, page, limit)}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
@ -142,118 +142,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
@doc """
Join an event for an actor
"""
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, _activity, participant} <- API.Participations.join(event, actor),
participant <-
participant
|> Map.put(:event, event)
|> Map.put(:actor, Person.proxify_pictures(actor)) do
{:ok, participant}
else
{:maximum_attendee_capacity, _} ->
{:error, "The event has already reached its maximum capacity"}
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :event_not_found} ->
{:error, "Event id not found"}
{:ok, %Participant{}} ->
{:error, "You are already a participant of this event"}
end
end
def actor_join_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to join an event"}
end
@doc """
Leave an event for an actor
"""
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:ok, _activity, _participant} <- API.Participations.leave(event, actor) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:only_organizer, true} ->
{:error, "You can't leave event because you're the only event creator participant"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
def actor_leave_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to leave an event"}
end
def update_participation(
_parent,
%{id: participation_id, moderator_actor_id: moderator_actor_id, role: new_role},
%{context: %{current_user: user}}
) do
# Check that moderator provided is rightly authenticated
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
# Check that participation already exists
{:has_participation, %Participant{role: old_role} = participation} <-
{:has_participation, Mobilizon.Events.get_participant(participation_id)},
{:same_role, false} <- {:same_role, new_role == old_role},
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission,
Mobilizon.Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
{:ok, _activity, participation} <-
API.Participations.update(participation, moderator_actor, new_role) do
{:ok, participation}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
{:has_participation, %Participant{role: role, id: id}} ->
{:error,
"Participant #{id} can't be approved since it's already a participant (with role #{role})"}
{:has_participation, nil} ->
{:error, "Participant not found"}
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
{:same_role, true} ->
{:error, "Participant already has role #{new_role}"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
@doc """
Create an event
"""

View File

@ -0,0 +1,262 @@
defmodule Mobilizon.GraphQL.Resolvers.Participant do
@moduledoc """
Handles the participation-related GraphQL calls.
"""
alias Mobilizon.{Actors, Config, Crypto, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.GraphQL.API.Participations
alias Mobilizon.GraphQL.Resolvers.Person
alias Mobilizon.Users.User
alias Mobilizon.Web.Email
alias Mobilizon.Web.Email.Checker
require Logger
@doc """
Join an event for an regular actor
"""
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{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)
_ ->
{:error, "Actor id is not owned by authenticated user"}
end
end
@doc """
Join an event for an anonymous actor
"""
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id} = args,
_resolution
) do
with {:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:anonymous_participation_enabled, true} <-
{:anonymous_participation_enabled,
event.local == true && Config.anonymous_participation?() &&
event.options.anonymous_participation == true},
{:anonymous_actor_id, true} <-
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
{:email_required, true} <-
{:email_required,
Config.anonymous_participation_email_required?() &&
args |> Map.get(:email) |> valid_email?()},
{:confirmation_token, {confirmation_token, role}} <-
{:confirmation_token,
if(Config.anonymous_participation_email_confirmation_required?(),
do: {Crypto.random_string(30), :not_confirmed},
else: {nil, :participant}
)},
# We only federate if the participation is not to be confirmed later
args <-
args
|> Map.put(:confirmation_token, confirmation_token)
|> Map.put(:cancellation_token, Crypto.random_string(30))
|> Map.put(:role, role)
|> Map.put(:local, role == :participant),
{:actor_not_found, %Actor{} = actor} <-
{:actor_not_found, Actors.get_actor_with_preload(actor_id)},
{:ok, %Participant{} = participant} <- do_actor_join_event(actor, event_id, args) do
if Config.anonymous_participation_email_required?() &&
Config.anonymous_participation_email_confirmation_required?() do
args
|> Map.get(:email)
|> Email.Participation.anonymous_participation_confirmation(participant)
|> Email.Mailer.deliver_later()
end
{:ok, participant}
else
{:error, err} ->
{:error, err}
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:anonymous_participation_enabled, false} ->
{:error, "Anonymous participation is not enabled"}
{:anonymous_actor_id, false} ->
{:error, "Actor ID provided is not the anonymous actor one"}
{:email_required, _} ->
{:error, "A valid email is required by your instance"}
{:actor_not_found, _} ->
Logger.error(
"The actor ID \"#{actor_id}\" provided by configuration doesn't match any actor in database"
)
{:error, "Internal Error"}
end
end
def actor_join_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to join an event"}
end
@spec do_actor_join_event(Actor.t(), integer | String.t(), map()) ::
{:ok, Participant.t()} | {:error, String.t()}
defp do_actor_join_event(actor, event_id, args \\ %{}) do
with {:has_event, {:ok, %Event{} = event}} <-
{:has_event, Events.get_event_with_preload(event_id)},
{:ok, _activity, participant} <- Participations.join(event, actor, args),
%Participant{} = participant <-
participant
|> Map.put(:event, event)
|> Map.put(:actor, Person.proxify_pictures(actor)) do
{:ok, participant}
else
{:maximum_attendee_capacity, _} ->
{:error, "The event has already reached its maximum capacity"}
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:error, :event_not_found} ->
{:error, "Event id not found"}
{:ok, %Participant{}} ->
{:error, "You are already a participant of this event"}
end
end
@doc """
Leave an event for an actor
"""
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id, token: token},
_resolution
) do
with {:anonymous_participation_enabled, true} <-
{:anonymous_participation_enabled, Config.anonymous_participation?()},
{:anonymous_actor_id, true} <-
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
%Actor{} = actor <- Actors.get_actor_with_preload(actor_id),
{:ok, _activity, %Participant{id: participant_id} = _participant} <-
Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:only_organizer, true} ->
{:error, "You can't leave event because you're the only event creator participant"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Events.get_event_with_preload(event_id)},
{:ok, _activity, _participant} <- Participations.leave(event, actor) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:only_organizer, true} ->
{:error, "You can't leave event because you're the only event creator participant"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
def actor_leave_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to leave an event"}
end
def update_participation(
_parent,
%{id: participation_id, moderator_actor_id: moderator_actor_id, role: new_role},
%{
context: %{
current_user: user
}
}
) do
# Check that moderator provided is rightly authenticated
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
# Check that participation already exists
{:has_participation, %Participant{role: old_role} = participation} <-
{:has_participation, Events.get_participant(participation_id)},
{:same_role, false} <- {:same_role, new_role == old_role},
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission,
Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
{:ok, _activity, participation} <-
Participations.update(participation, moderator_actor, new_role) do
{:ok, participation}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
{:has_participation, nil} ->
{:error, "Participant not found"}
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
{:same_role, true} ->
{:error, "Participant already has role #{new_role}"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
@spec confirm_participation_from_token(map(), map(), map()) ::
{:ok, Participant.t()} | {:error, String.t()}
def confirm_participation_from_token(
_parent,
%{confirmation_token: confirmation_token},
_context
) do
with {:has_participant,
%Participant{actor: actor, role: :not_confirmed, event: event} = participant} <-
{:has_participant, Events.get_participant_by_confirmation_token(confirmation_token)},
default_role <- Events.get_default_participant_role(event),
{:ok, _activity, %Participant{} = participant} <-
Participations.update(participant, actor, default_role) do
{:ok, participant}
else
{:has_participant, _} ->
{:error, "This token is invalid"}
end
end
@spec valid_email?(String.t() | nil) :: boolean
defp valid_email?(email) when is_nil(email), do: false
defp valid_email?(email) when is_bitstring(email) do
email
|> String.trim()
|> Checker.valid?()
end
end

View File

@ -60,6 +60,21 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
field(:number_of_reports, :integer, description: "The number of current opened reports")
end
object :admin_settings do
field(:instance_name, :string)
field(:instance_description, :string)
field(:instance_terms, :string)
field(:instance_terms_type, :instance_terms_type)
field(:instance_terms_url, :string)
field(:registrations_open, :boolean)
end
enum :instance_terms_type do
value(:url, as: "URL")
value(:default, as: "DEFAULT")
value(:custom, as: "CUSTOM")
end
object :admin_queries do
@desc "Get the list of action logs"
field :action_logs, type: list_of(:action_log) do
@ -72,6 +87,10 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
resolve(&Admin.get_dashboard/3)
end
field :admin_settings, type: :admin_settings do
resolve(&Admin.get_settings/3)
end
field :relay_followers, type: :paginated_follower_list do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
@ -115,5 +134,16 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
resolve(&Admin.reject_subscription/3)
end
field :save_admin_settings, type: :admin_settings do
arg(:instance_name, :string)
arg(:instance_description, :string)
arg(:instance_terms, :string)
arg(:instance_terms_type, :instance_terms_type)
arg(:instance_terms_url, :string)
arg(:registrations_open, :boolean)
resolve(&Admin.save_settings/3)
end
end
end

View File

@ -19,6 +19,18 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:location, :lonlat)
field(:geocoding, :geocoding)
field(:maps, :maps)
field(:anonymous, :anonymous)
field(:terms, :terms, description: "The instance's terms") do
arg(:locale, :string, default_value: "en")
resolve(&Config.terms/3)
end
end
object :terms do
field(:url, :string)
field(:type, :instance_terms_type)
field(:body_html, :string)
end
object :lonlat do
@ -41,6 +53,50 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:attribution, :string)
end
object :anonymous do
field(:participation, :anonymous_participation)
field(:event_creation, :anonymous_event_creation)
field(:actor_id, :id)
end
object :anonymous_participation do
field(:allowed, :boolean)
field(:validation, :anonymous_participation_validation)
end
object :anonymous_participation_validation do
field(:email, :anonymous_participation_validation_email)
field(:captcha, :anonymous_participation_validation_captcha)
end
object :anonymous_participation_validation_email do
field(:enabled, :boolean)
field(:confirmation_required, :boolean)
end
object :anonymous_participation_validation_captcha do
field(:enabled, :boolean)
end
object :anonymous_event_creation do
field(:allowed, :boolean)
field(:validation, :anonymous_event_creation_validation)
end
object :anonymous_event_creation_validation do
field(:email, :anonymous_event_creation_validation_email)
field(:captcha, :anonymous_event_creation_validation_captcha)
end
object :anonymous_event_creation_validation_email do
field(:enabled, :boolean)
field(:confirmation_required, :boolean)
end
object :anonymous_event_creation_validation_captcha do
field(:enabled, :boolean)
end
object :config_queries do
@desc "Get the instance config"
field :config, :config do

View File

@ -122,6 +122,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
)
field(:not_approved, :integer, description: "The number of not approved participants")
field(:not_confirmed, :integer, description: "The number of not confirmed participants")
field(:rejected, :integer, description: "The number of rejected participants")
field(:participant, :integer,
@ -177,6 +178,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "Whether or not to show the number of remaining seats for this event"
)
field(:anonymous_participation, :boolean,
description: "Whether or not to allow anonymous participation (if the server allows it)"
)
field(:offers, list_of(:event_offer), description: "The list of offers to show for this event")
field(:participation_conditions, list_of(:event_participation_condition),
@ -211,6 +216,11 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "Whether or not to show the number of remaining seats for this event"
)
field(:anonymous_participation, :boolean,
default_value: false,
description: "Whether or not to allow anonymous participation (if the server allows it)"
)
field(:offers, list_of(:event_offer_input),
description: "The list of offers to show for this event"
)

View File

@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
alias Mobilizon.{Actors, Events}
alias Mobilizon.GraphQL.Resolvers.Event
alias Mobilizon.GraphQL.Resolvers.Participant
@desc "Represents a participant to an event"
object :participant do
@ -29,10 +29,21 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
)
field(:role, :participant_role_enum, description: "The role of this actor at this event")
field(:metadata, :participant_metadata,
description: "The metadata associated to this participant"
)
end
object :participant_metadata do
field(:cancellation_token, :string,
description: "The eventual token to leave an event when user is anonymous"
)
end
enum :participant_role_enum do
value(:not_approved)
value(:not_confirmed)
value(:participant)
value(:moderator)
value(:administrator)
@ -52,16 +63,18 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
field :join_event, :participant do
arg(:event_id, non_null(:id))
arg(:actor_id, non_null(:id))
arg(:email, :string)
resolve(&Event.actor_join_event/3)
resolve(&Participant.actor_join_event/3)
end
@desc "Leave an event"
field :leave_event, :deleted_participant do
arg(:event_id, non_null(:id))
arg(:actor_id, non_null(:id))
arg(:token, :string)
resolve(&Event.actor_leave_event/3)
resolve(&Participant.actor_leave_event/3)
end
@desc "Accept a participation"
@ -70,7 +83,13 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:role, non_null(:participant_role_enum))
arg(:moderator_actor_id, non_null(:id))
resolve(&Event.update_participation/3)
resolve(&Participant.update_participation/3)
end
@desc "Confirm a participation"
field :confirm_participation, :participant do
arg(:confirmation_token, non_null(:string))
resolve(&Participant.confirm_participation_from_token/3)
end
end
end

View File

@ -20,6 +20,7 @@ defmodule Mobilizon do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@env Mix.env()
@spec named_version :: String.t()
def named_version, do: "#{@name} #{@version}"
@ -34,7 +35,8 @@ defmodule Mobilizon do
@spec start(:normal | {:takeover, node} | {:failover, node}, term) ::
{:ok, pid} | {:ok, pid, term} | {:error, term}
def start(_type, _args) do
children = [
children =
[
# supervisors
Storage.Repo,
Web.Endpoint,
@ -46,9 +48,10 @@ defmodule Mobilizon do
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
cachex_spec(:statistics, 10, 60, 60),
cachex_spec(:activity_pub, 2500, 3, 15),
internal_actor()
]
cachex_spec(:config, 10, 60, 60),
cachex_spec(:activity_pub, 2500, 3, 15)
] ++
task_children(@env)
Supervisor.start_link(children, strategy: :one_for_one, name: Mobilizon.Supervisor)
end
@ -92,11 +95,22 @@ defmodule Mobilizon do
defp fallback_options(nil), do: []
defp fallback_options(fallback), do: [fallback: fallback(default: fallback)]
defp internal_actor do
defp task_children(:test), do: []
defp task_children(_), do: [relay_actor(), anonymous_actor()]
defp relay_actor do
%{
id: :internal_actor_init,
id: :relay_actor_init,
start: {Task, :start_link, [&ActivityPub.Relay.init/0]},
restart: :temporary
}
end
defp anonymous_actor do
%{
id: :anonymous_actor_init,
start: {Task, :start_link, [&Mobilizon.Config.anonymous_actor_id/0]},
restart: :temporary
}
end
end

View File

@ -97,20 +97,6 @@ defmodule Mobilizon.Actors.Actor do
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
@remote_actor_creation_optional_attrs
@relay_creation_attrs [
:type,
:name,
:summary,
:url,
:keys,
:preferred_username,
:domain,
:inbox_url,
:followers_url,
:following_url,
:shared_inbox_url
]
@group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username]
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary]
@group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs
@ -277,16 +263,6 @@ defmodule Mobilizon.Actors.Actor do
|> validate_format(:preferred_username, ~r/[a-z0-9_]+/)
end
@doc """
Changeset for relay creation.
"""
@spec relay_creation_changeset(map) :: Ecto.Changeset.t()
def relay_creation_changeset(attrs) do
relay_creation_attrs = build_relay_creation_attrs(attrs)
cast(%__MODULE__{}, relay_creation_attrs, @relay_creation_attrs)
end
@doc """
Changeset for group creation
"""
@ -349,6 +325,10 @@ defmodule Mobilizon.Actors.Actor do
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
# Relay has a special URI
def build_url("relay", :page, _args),
do: Endpoint |> Routes.activity_pub_url(:relay) |> URI.decode()
def build_url(preferred_username, :page, args) do
Endpoint
|> Routes.page_url(:actor, preferred_username, args)
@ -362,24 +342,40 @@ defmodule Mobilizon.Actors.Actor do
|> URI.decode()
end
@spec build_relay_creation_attrs(map) :: map
defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do
%{
@spec build_relay_creation_attrs :: Ecto.Changeset.t()
def build_relay_creation_attrs do
data = %{
"name" => Config.get([:instance, :name], "Mobilizon"),
"summary" =>
Config.get(
[:instance, :description],
"An internal service actor for this Mobilizon instance"
),
"url" => url,
"keys" => Crypto.generate_rsa_2048_private_key(),
"preferred_username" => preferred_username,
"preferred_username" => "relay",
"domain" => nil,
"inbox_url" => "#{Endpoint.url()}/inbox",
"followers_url" => "#{url}/followers",
"following_url" => "#{url}/following",
"shared_inbox_url" => "#{Endpoint.url()}/inbox",
"type" => :Application
}
%__MODULE__{}
|> Ecto.Changeset.cast(data, @attrs)
|> build_urls()
|> put_change(:inbox_url, "#{Endpoint.url()}/inbox")
end
@spec build_anonymous_actor_creation_attrs :: Ecto.Changeset.t()
def build_anonymous_actor_creation_attrs do
data = %{
"name" => "Mobilizon Anonymous Actor",
"summary" => "A fake person for anonymous participations",
"keys" => Crypto.generate_rsa_2048_private_key(),
"preferred_username" => "anonymous",
"domain" => nil,
"type" => :Person
}
%__MODULE__{}
|> Ecto.Changeset.cast(data, @attrs)
|> build_urls()
end
end

View File

@ -499,17 +499,22 @@ defmodule Mobilizon.Actors do
|> Repo.insert()
end
@spec get_or_create_instance_actor_by_url(String.t(), String.t()) ::
{:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
def get_or_create_instance_actor_by_url(url, preferred_username \\ "relay") do
case get_actor_by_url(url) do
@spec get_or_create_internal_actor(String.t()) :: {:ok, Actor.t()}
def get_or_create_internal_actor(username) do
case username |> Actor.build_url(:page) |> get_actor_by_url() do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
%{url: url, preferred_username: preferred_username}
|> Actor.relay_creation_changeset()
case username do
"anonymous" ->
Actor.build_anonymous_actor_creation_attrs()
|> Repo.insert()
"relay" ->
Actor.build_relay_creation_attrs()
|> Repo.insert()
end
end
end

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.Admin do
alias Mobilizon.Actors.Actor
alias Mobilizon.{Admin, Users}
alias Mobilizon.Admin.ActionLog
alias Mobilizon.Admin.Setting
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.User
@ -18,6 +19,8 @@ defmodule Mobilizon.Admin do
"delete"
])
alias Ecto.Multi
@doc """
Creates a action_log.
"""
@ -71,4 +74,48 @@ defmodule Mobilizon.Admin do
end
defp stringify_struct(struct), do: struct
def get_admin_setting_value(group, name, fallback \\ nil)
when is_bitstring(group) and is_bitstring(name) do
case Repo.get_by(Setting, group: group, name: name) do
nil -> fallback
%Setting{value: ""} -> fallback
%Setting{value: nil} -> fallback
%Setting{value: value} -> value
end
end
def set_admin_setting_value(group, name, value) do
Setting
|> Setting.changeset(%{group: group, name: name, value: value})
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:group, :name])
end
def save_settings(group, args) do
Multi.new()
|> do_save_setting(group, args)
|> Repo.transaction()
end
defp do_save_setting(transaction, _group, args) when args == %{}, do: transaction
defp do_save_setting(transaction, group, args) do
key = hd(Map.keys(args))
{val, rest} = Map.pop(args, key)
transaction =
Multi.insert(
transaction,
key,
Setting.changeset(%Setting{}, %{
group: group,
name: Atom.to_string(key),
value: to_string(val)
}),
on_conflict: :replace_all,
conflict_target: [:group, :name]
)
do_save_setting(transaction, group, rest)
end
end

View File

@ -0,0 +1,27 @@
defmodule Mobilizon.Admin.Setting do
@moduledoc """
A Key-Value settings table for basic settings
"""
use Ecto.Schema
import Ecto.Changeset
@required_attrs [:group, :name]
@optional_attrs [:value]
@attrs @required_attrs ++ @optional_attrs
schema "admin_settings" do
field(:group, :string)
field(:name, :string)
field(:value, :string)
timestamps()
end
@doc false
def changeset(setting, attrs) do
setting
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
|> unique_constraint(:group, name: :admin_settings_group_name_index)
end
end

View File

@ -3,14 +3,44 @@ defmodule Mobilizon.Config do
Configuration wrapper.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
@spec instance_config :: keyword
def instance_config, do: Application.get_env(:mobilizon, :instance)
@spec instance_name :: String.t()
def instance_name, do: instance_config()[:name]
def instance_name,
do:
Mobilizon.Admin.get_admin_setting_value(
"instance",
"instance_name",
instance_config()[:name]
)
@spec instance_description :: String.t()
def instance_description, do: instance_config()[:description]
def instance_description,
do:
Mobilizon.Admin.get_admin_setting_value(
"instance",
"instance_description",
instance_config()[:description]
)
@spec instance_terms(String.t()) :: String.t()
def instance_terms(locale \\ "en") do
Mobilizon.Admin.get_admin_setting_value("instance", "instance_terms", generate_terms(locale))
end
@spec instance_terms :: String.t()
def instance_terms_type do
Mobilizon.Admin.get_admin_setting_value("instance", "instance_terms_type", "DEFAULT")
end
@spec instance_terms :: String.t()
def instance_terms_url do
Mobilizon.Admin.get_admin_setting_value("instance", "instance_terms_url")
end
@spec instance_version :: String.t()
def instance_version, do: Mix.Project.config()[:version]
@ -19,7 +49,15 @@ defmodule Mobilizon.Config do
def instance_hostname, do: instance_config()[:hostname]
@spec instance_registrations_open? :: boolean
def instance_registrations_open?, do: to_boolean(instance_config()[:registrations_open])
def instance_registrations_open?,
do:
to_boolean(
Mobilizon.Admin.get_admin_setting_value(
"instance",
"registrations_open",
instance_config()[:registrations_open]
)
)
@spec instance_registrations_whitelist :: list(String.t())
def instance_registrations_whitelist, do: instance_config()[:registration_email_whitelist]
@ -58,6 +96,51 @@ defmodule Mobilizon.Config do
def instance_maps_tiles_attribution,
do: Application.get_env(:mobilizon, :maps)[:tiles][:attribution]
@spec anonymous_participation? :: boolean
def anonymous_participation?,
do: Application.get_env(:mobilizon, :anonymous)[:participation][:allowed]
@spec anonymous_participation_email_required? :: boolean
def anonymous_participation_email_required?,
do: Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:email][:enabled]
@spec anonymous_participation_email_confirmation_required? :: boolean
def anonymous_participation_email_confirmation_required?,
do:
Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:email][
:confirmation_required
]
@spec anonymous_participation_email_captcha_required? :: boolean
def anonymous_participation_email_captcha_required?,
do:
Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:captcha][:enabled]
@spec anonymous_event_creation? :: boolean
def anonymous_event_creation?,
do: Application.get_env(:mobilizon, :anonymous)[:event_creation][:allowed]
@spec anonymous_event_creation_email_required? :: boolean
def anonymous_event_creation_email_required?,
do:
Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:email][:enabled]
@spec anonymous_event_creation_email_confirmation_required? :: boolean
def anonymous_event_creation_email_confirmation_required?,
do:
Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:email][
:confirmation_required
]
@spec anonymous_event_creation_email_captcha_required? :: boolean
def anonymous_event_creation_email_captcha_required?,
do:
Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:captcha][
:enabled
]
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
@spec get(module | atom) :: any
def get(key), do: get(key, nil)
@ -99,4 +182,38 @@ defmodule Mobilizon.Config do
@spec to_boolean(boolean | String.t()) :: boolean
defp to_boolean(boolean), do: "true" == String.downcase("#{boolean}")
defp get_cached_value(key) do
case Cachex.fetch(:config, key, fn key ->
case create_cache(key) do
value when not is_nil(value) -> {:commit, value}
err -> {:ignore, err}
end
end) do
{status, value} when status in [:ok, :commit] -> value
_err -> nil
end
end
@spec create_cache(atom()) :: integer()
defp create_cache(:anonymous_actor_id) do
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_create_internal_actor("anonymous") do
actor_id
end
end
def clear_config_cache do
Cachex.clear(:config)
end
def generate_terms(locale) do
import Mobilizon.Web.Gettext
put_locale(locale)
Phoenix.View.render_to_string(
Mobilizon.Web.APIView,
"terms.html",
[]
)
end
end

View File

@ -17,6 +17,7 @@ defmodule Mobilizon.Events.EventOptions do
maximum_attendee_capacity: integer,
remaining_attendee_capacity: integer,
show_remaining_attendee_capacity: boolean,
anonymous_participation: boolean,
attendees: [String.t()],
program: String.t(),
comment_moderation: CommentModeration.t(),
@ -31,6 +32,7 @@ defmodule Mobilizon.Events.EventOptions do
:maximum_attendee_capacity,
:remaining_attendee_capacity,
:show_remaining_attendee_capacity,
:anonymous_participation,
:attendees,
:program,
:comment_moderation,
@ -45,6 +47,7 @@ defmodule Mobilizon.Events.EventOptions do
field(:maximum_attendee_capacity, :integer)
field(:remaining_attendee_capacity, :integer)
field(:show_remaining_attendee_capacity, :boolean)
field(:anonymous_participation, :boolean)
field(:attendees, {:array, :string})
field(:program, :string)
field(:comment_moderation, CommentModeration)

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
@type t :: %__MODULE__{
not_approved: integer(),
not_confirmed: integer(),
rejected: integer(),
participant: integer(),
moderator: integer(),
@ -17,6 +18,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
@attrs [
:not_approved,
:not_confirmed,
:rejected,
:participant,
:moderator,
@ -29,6 +31,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
@derive Jason.Encoder
embedded_schema do
field(:not_approved, :integer, default: 0)
field(:not_confirmed, :integer, default: 0)
field(:rejected, :integer, default: 0)
field(:participant, :integer, default: 0)
field(:moderator, :integer, default: 0)
@ -47,6 +50,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
defp validate_stats(%Ecto.Changeset{} = changeset) do
changeset
|> validate_number(:not_approved, greater_than_or_equal_to: 0)
|> validate_number(:not_confirmed, greater_than_or_equal_to: 0)
|> validate_number(:rejected, greater_than_or_equal_to: 0)
|> validate_number(:participant, greater_than_or_equal_to: 0)
|> validate_number(:moderator, greater_than_or_equal_to: 0)

View File

@ -76,6 +76,7 @@ defmodule Mobilizon.Events do
defenum(ParticipantRole, :participant_role, [
:not_approved,
:not_confirmed,
:rejected,
:participant,
:moderator,
@ -661,9 +662,39 @@ defmodule Mobilizon.Events do
@doc """
Gets a single participation for an event and actor.
"""
@spec get_participant(integer | String.t(), integer | String.t()) ::
@spec get_participant(integer | String.t(), integer | String.t(), map()) ::
{:ok, Participant.t()} | {:error, :participant_not_found}
def get_participant(event_id, actor_id) do
def get_participant(event_id, actor_id, params \\ %{})
# This one if to check someone doesn't go to the same event twice
def get_participant(event_id, actor_id, %{email: email}) do
case Participant
|> where([p], event_id: ^event_id, actor_id: ^actor_id)
|> where([p], fragment("? ->>'email' = ?", p.metadata, ^email))
|> Repo.one() do
%Participant{} = participant ->
{:ok, participant}
nil ->
{:error, :participant_not_found}
end
end
# This one if for finding participants by their cancellation token when wanting to cancel a participation
def get_participant(event_id, actor_id, %{cancellation_token: cancellation_token}) do
case Participant
|> where([p], event_id: ^event_id, actor_id: ^actor_id)
|> where([p], fragment("? ->>'cancellation_token' = ?", p.metadata, ^cancellation_token))
|> Repo.one() do
%Participant{} = participant ->
{:ok, participant}
nil ->
{:error, :participant_not_found}
end
end
def get_participant(event_id, actor_id, %{}) do
case Repo.get_by(Participant, event_id: event_id, actor_id: actor_id) do
%Participant{} = participant ->
{:ok, participant}
@ -673,6 +704,14 @@ defmodule Mobilizon.Events do
end
end
@spec get_participant_by_confirmation_token(String.t()) :: Participant.t()
def get_participant_by_confirmation_token(confirmation_token) do
Participant
|> where([p], fragment("? ->>'confirmation_token' = ?", p.metadata, ^confirmation_token))
|> preload([p], [:actor, :event])
|> Repo.one()
end
@doc """
Gets a single participation for an event and actor.
@ -706,7 +745,7 @@ defmodule Mobilizon.Events do
@doc """
Returns the list of participants for an event.
Default behaviour is to not return :not_approved participants
Default behaviour is to not return :not_approved or :not_confirmed participants
"""
@spec list_participants_for_event(String.t(), list(atom()), integer | nil, integer | nil) ::
[Participant.t()]

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.Events.Participant do
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, ParticipantRole}
alias Mobilizon.Web.Email.Checker
alias Mobilizon.Web.Endpoint
@ -28,6 +29,12 @@ defmodule Mobilizon.Events.Participant do
field(:role, ParticipantRole, default: :participant)
field(:url, :string)
embeds_one :metadata, Metadata, on_replace: :delete do
field(:email, :string)
field(:confirmation_token, :string)
field(:cancellation_token, :string)
end
belongs_to(:event, Event, primary_key: true)
belongs_to(:actor, Actor, primary_key: true)
@ -55,11 +62,18 @@ defmodule Mobilizon.Events.Participant do
def changeset(%__MODULE__{} = participant, attrs) do
participant
|> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2)
|> ensure_url()
|> validate_required(@required_attrs)
|> unique_constraint(:actor_id, name: :participants_event_id_actor_id_index)
end
defp metadata_changeset(schema, params) do
schema
|> cast(params, [:email, :confirmation_token, :cancellation_token])
|> Checker.validate_changeset()
end
# If there's a blank URL that's because we're doing the first insert
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do

View File

@ -11,8 +11,7 @@ defmodule Mobilizon.Users.User do
alias Mobilizon.Crypto
alias Mobilizon.Events.FeedToken
alias Mobilizon.Users.UserRole
alias Mobilizon.Web.Email
alias Mobilizon.Web.Email.Checker
@type t :: %__MODULE__{
email: String.t(),
@ -79,7 +78,7 @@ defmodule Mobilizon.Users.User do
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
|> unique_constraint(:email, message: "This email is already used.")
|> validate_email()
|> Checker.validate_changeset()
|> validate_length(:password, min: 6, max: 200, message: "The chosen password is too short.")
if Map.has_key?(attrs, :default_actor) do
@ -171,25 +170,6 @@ defmodule Mobilizon.Users.User do
defp save_confirmation_token(%Ecto.Changeset{} = changeset), do: changeset
@spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_email(%Ecto.Changeset{} = changeset) do
changeset = validate_length(changeset, :email, min: 3, max: 250)
case changeset do
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
case Email.Checker.valid?(email) do
false ->
add_error(changeset, :email, "Email doesn't fit required format")
true ->
changeset
end
_ ->
changeset
end
end
@spec hash_password(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp hash_password(%Ecto.Changeset{} = changeset) do
case changeset do

View File

@ -2,7 +2,7 @@ defmodule Mobilizon.Web.GraphQLSocket do
use Phoenix.Socket
use Absinthe.Phoenix.Socket,
schema: Mobilizon.Web.Schema
schema: Mobilizon.GraphQL.Schema
alias Mobilizon.Users.User

View File

@ -4,6 +4,9 @@ defmodule Mobilizon.Web.PageController do
"""
use Mobilizon.Web, :controller
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Tombstone
alias Mobilizon.Web.Cache
plug(:put_layout, false)
@ -13,23 +16,32 @@ defmodule Mobilizon.Web.PageController do
def actor(conn, %{"name" => name}) do
{status, actor} = Cache.get_local_actor_by_name(name)
render_or_error(conn, &ok_status?/2, status, :actor, actor)
render_or_error(conn, &ok_status?/3, status, :actor, actor)
end
def event(conn, %{"uuid" => uuid}) do
{status, event} = Cache.get_public_event_by_uuid_with_preload(uuid)
render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event)
render_or_error(conn, &checks?/3, status, :event, event)
end
def comment(conn, %{"uuid" => uuid}) do
{status, comment} = Cache.get_comment_by_uuid_with_preload(uuid)
render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment)
render_or_error(conn, &checks?/3, status, :comment, comment)
end
def interact(conn, %{"uri" => uri}) do
case ActivityPub.fetch_object_from_url(uri) do
{:ok, %Event{uuid: uuid}} -> redirect(conn, to: "/events/#{uuid}")
{:ok, %Comment{uuid: uuid}} -> redirect(conn, to: "/comments/#{uuid}")
_ -> {:error, :not_found}
end
end
defp render_or_error(conn, check_fn, status, object_type, object) do
if check_fn.(status, object) do
case check_fn.(conn, status, object) do
true ->
case object do
%Mobilizon.Tombstone{} ->
%Tombstone{} ->
conn
|> put_status(:gone)
|> render(object_type, object: object)
@ -37,16 +49,32 @@ defmodule Mobilizon.Web.PageController do
_ ->
render(conn, object_type, object: object)
end
else
:remote ->
redirect(conn, external: object.url)
false ->
{:error, :not_found}
end
end
defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted]
defp is_visible?(%Mobilizon.Tombstone{}), do: true
defp is_visible?(%Tombstone{}), do: true
defp ok_status?(status), do: status in [:ok, :commit]
defp ok_status?(status, _), do: ok_status?(status)
defp ok_status?(_conn, status, _), do: ok_status?(status)
defp ok_status_and_is_visible?(status, o), do: ok_status?(status) and is_visible?(o)
defp ok_status_and_is_visible?(_conn, status, o),
do: ok_status?(status) and is_visible?(o)
defp checks?(conn, status, o) do
if ok_status_and_is_visible?(conn, status, o) do
if is_local?(o) == :remote && get_format(conn) == "activity-json", do: :remote, else: true
else
false
end
end
defp is_local?(%Event{local: local}), do: if(local, do: true, else: :remote)
defp is_local?(%Comment{local: local}), do: if(local, do: true, else: :remote)
end

View File

@ -10,4 +10,19 @@ defmodule Mobilizon.Web.Email.Checker do
"""
@spec valid?(String.t()) :: boolean
def valid?(email), do: email =~ @email_regex
@spec validate_changeset(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
def validate_changeset(%Ecto.Changeset{} = changeset, key \\ :email) do
changeset = Ecto.Changeset.validate_length(changeset, :email, min: 3, max: 250)
case Ecto.Changeset.fetch_change(changeset, key) do
{:ok, email} ->
if valid?(email),
do: changeset,
else: Ecto.Changeset.add_error(changeset, :email, "Email doesn't fit required format")
:error ->
changeset
end
end
end

View File

@ -2,26 +2,34 @@ defmodule Mobilizon.Web.Email.Participation do
@moduledoc """
Handles emails sent about participation.
"""
use Bamboo.Phoenix, view: Mobilizon.Web.EmailView
import Bamboo.Phoenix
import Mobilizon.Web.Gettext
alias Mobilizon.Actors.Actor
alias Mobilizon.Config
alias Mobilizon.Events.Participant
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext}
@doc """
Send emails to local user
"""
def send_emails_to_local_user(
%Participant{actor: %Actor{user_id: nil} = _actor} = _participation
),
do: :ok
%Participant{actor: %Actor{user_id: nil, id: actor_id} = _actor} = participation
) do
if actor_id == Config.anonymous_actor_id() do
%{email: email} = Map.get(participation, :metadata)
email
|> participation_updated(participation)
|> Email.Mailer.deliver_later()
end
:ok
end
@doc """
Send emails to local user
@ -29,7 +37,7 @@ defmodule Mobilizon.Web.Email.Participation do
def send_emails_to_local_user(
%Participant{actor: %Actor{user_id: user_id} = _actor} = participation
) do
with %User{} = user <- Mobilizon.Users.get_user!(user_id) do
with %User{} = user <- Users.get_user!(user_id) do
user
|> participation_updated(participation)
|> Email.Mailer.deliver_later()
@ -38,11 +46,21 @@ defmodule Mobilizon.Web.Email.Participation do
end
end
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
@spec participation_updated(String.t() | User.t(), Participant.t(), String.t()) ::
Bamboo.Email.t()
def participation_updated(user, participant, locale \\ "en")
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated(
%User{email: email},
%Participant{} = participant,
locale
),
do: participation_updated(email, participant, locale)
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated(
email,
%Participant{event: event, role: :rejected},
locale
) do
@ -61,9 +79,9 @@ defmodule Mobilizon.Web.Email.Participation do
|> render(:event_participation_rejected)
end
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated(
%User{email: email},
email,
%Participant{event: event, role: :participant},
locale
) do
@ -81,4 +99,26 @@ defmodule Mobilizon.Web.Email.Participation do
|> assign(:subject, subject)
|> render(:event_participation_approved)
end
@spec anonymous_participation_confirmation(String.t(), Participant.t(), String.t()) ::
Bamboo.Email.t()
def anonymous_participation_confirmation(
email,
%Participant{event: event, role: :not_confirmed} = participant,
locale \\ "en"
) do
Gettext.put_locale(locale)
subject =
gettext(
"Confirm your participation to event %{title}",
title: event.title
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:participant, participant)
|> assign(:subject, subject)
|> render(:anonymous_participation_confirmation)
end
end

View File

@ -58,8 +58,6 @@ defmodule Mobilizon.Web.Router do
)
end
## FEDERATION
scope "/.well-known", Mobilizon.Web do
pipe_through(:well_known)
@ -110,8 +108,10 @@ defmodule Mobilizon.Web.Router do
end
## MOBILIZON
forward("/graphiql", Absinthe.Plug.GraphiQL, schema: Mobilizon.Web.Schema)
scope "/graphiql" do
pipe_through(:graphql)
forward("/", Absinthe.Plug.GraphiQL, schema: Mobilizon.GraphQL.Schema)
end
scope "/", Mobilizon.Web do
pipe_through(:browser)
@ -125,6 +125,12 @@ defmodule Mobilizon.Web.Router do
# This is a hack to ease link generation into emails
get("/moderation/reports/:id", PageController, :index, as: "moderation_report")
get("/participation/email/confirm/:token", PageController, :index,
as: "participation_email_confirmation"
)
get("/interact", PageController, :interact)
end
scope "/proxy/", Mobilizon.Web do

View File

@ -0,0 +1,163 @@
<h3><%= pgettext("terms", "What information do we collect?") %></h3>
<ul>
<li>
<em><%= pgettext("terms", "Basic account information") %></em>
<p><%= pgettext(
"terms",
"We collect information from you when you register on this server and gather data when you participate in the
platform by reading, writing, and interacting with content shared here. If you register on this server, you will
be asked to enter an e-mail address, a password and at least an username. Your e-mail address will be verified by
an email containing a unique link. If that link is visited, we know that you control the e-mail address. You may
also enter additional profile information such as a display name and biography, and upload a profile picture and
header image. The username, display name, biography, profile picture and header image are always listed publicly.
You may, however, visit this server without registering."
) %>
</p>
</li>
<li>
<em><%= pgettext("terms", "Published events and comments") %></em>
<p>
<%= pgettext(
"terms",
"Your events and comments are delivered to other instances that follow your own, meaning they are delivered to
different servers and copies are stored there. When you delete events or comments, this is likewise delivered to
these other instances. The action of joining an event is federated as well. Please keep in mind that the operators
of the server and any receiving server may view such messages, and that recipients may screenshot, copy or
otherwise re-share them."
) %>
<em><%= pgettext("terms", "Do not share any dangerous information over Mobilizon.") %></em>
</p>
</li>
<li>
<em><%= pgettext("terms", "IPs and other metadata") %></em>
<p>
<%=
pgettext(
"terms",
"We also may retain server logs which include the IP address of every request to our server."
)
%>
</p>
</li>
</ul>
<h3><%= pgettext("terms", "What do we use your information for?") %></h3>
<p><%=
pgettext(
"terms",
"Any of the information we collect from you may be used in the following ways:"
)
%></p>
<ul>
<li><%= pgettext("terms", "To provide the core functionality of Mobilizon. Depending on this instance's policy you may only be able to
interact with other people's content and post your own content if you are logged in.") %></li>
<li><%= pgettext("terms", "To aid moderation of the community, for example comparing your IP address with other known ones to determine ban
evasion or other violations.") %></li>
<li><%= pgettext("terms", "The email address you provide may be used to send you information, updates and notifications about other people
interacting with your content or sending you messages and to respond to inquiries, and/or other requests or
questions.") %></li>
</ul>
<h3 class="title"><%= pgettext("terms", "How do we protect your information?") %></h3>
<p>
<%=
pgettext(
"terms",
"We implement a variety of security measures to maintain the safety of your personal information when you enter,
submit, or access your personal information. Among other things, your browser session, as well as the traffic between
your applications and the API, are secured with SSL/TLS, and your password is hashed using a strong one-way
algorithm."
)
%>
</p>
<h3 class="title"><%= pgettext("terms", "What is our data retention policy?") %></h3>
<p><%= pgettext("terms", "We will make a good faith effort to:") %></p>
<ul>
<li><%=
pgettext(
"terms",
"Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more
than 90 days."
)%>
</li>
<li>
<%=
pgettext(
"terms",
"Retain the IP addresses associated with registered users no more than 12 months."
)
%>
</li>
</ul>
<p>
<%=
pgettext(
"terms",
"You can request and download an archive of your content, including your posts, media attachments, profile picture,
and header image."
)
%>
</p>
<p><%= pgettext("terms", "You may irreversibly delete your account at any time.") %></p>
<h3 class="title"><%= pgettext("terms", "Do we use cookies?") %></h3>
<p><%=
pgettext("terms", "We store the following information on your device when you connect:")
%>
</p>
<ul>
<li><%= pgettext("terms", "An internal user ID") %></li>
<li><%= pgettext("terms", "An internal ID for your current selected identity") %></li>
<li><%= pgettext("terms", "Tokens to authenticate you") %></li>
</ul>
<p><%= pgettext("terms", "If you delete these informations, you need to login again.") %></p>
<p><%=
pgettext(
"terms",
"If you're not connected, we don't store any information on your device, unless you participate in an event
anonymously. In that case we store the hash of the UUID and participation status in your browser so that we may
display participation status. Deleting these informations will only stop displaying participation status in your
browser."
)
%>
</p>
<em>
<%= pgettext("terms", "Note: These informations are stored in your localStorage and not your cookies.") %>
</em>
<h3 class="title"><%=
pgettext("terms", "Do we disclose any information to outside parties?")
%></h3>
<p>
<%=
pgettext(
"terms",
"We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This
does not include trusted third parties who assist us in operating our site, conducting our business, or servicing
you, so long as those parties agree to keep this information confidential. We may also release your information
when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or
others rights, property, or safety."
)
%>
</p>
<p>
<%=
pgettext(
"terms",
"Your content may be downloaded by other servers in the network. Your content is delivered to the servers
following your instance, and direct messages are delivered to the servers of the recipients, in so far as these
recipients reside on a different server than this one."
)
%>
</p>
<h3 class="title"><%=
pgettext("terms", "Site usage by children")
%></h3>
<p><%= pgettext("terms", "If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (<a href=\"https://en.wikipedia.org/wiki/General_Data_Protection_Regulation\">General Data Protection Regulation</a>) do not use this site.") |> raw %></p>
<p><%= pgettext("terms", "If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (<a href=\"https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act\">Children's Online Privacy Protection Act</a>) do not use this site.") |> raw %></p>
<p><%= pgettext("terms", "Law requirements can be different if this server is in another jurisdiction.") %></p>
<h3 class="title"><%=
pgettext("terms", "Changes to our Privacy Policy")
%></h3>
<p><%= pgettext("terms", "If we decide to change our privacy policy, we will post those changes on this page.") %></p>
<p><%= pgettext("terms", "This document is CC-BY-SA. It was last updated January 16, 2020.") %></p>
<p><%= pgettext("terms", "Originally adapted from the <a href=\"https://mastodon.social/terms\">Mastodon</a> and <a href=\"https://github.com/discourse/discourse\">Discourse</a> privacy policies.") |> raw %></p>

View File

@ -0,0 +1,81 @@
<!-- HERO -->
<tr>
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Participation confirmation" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "You requested to participate in event %{title}", title: @participant.event.title %>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "If you didn't request this email, you can simply ignore it." %>
</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#ffffff" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#424056"><a href="<%= participation_email_confirmation_url(Mobilizon.Web.Endpoint, :index, @participant.metadata.confirmation_token) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #424056; display: inline-block;">
<%= gettext "Confirm my participation" %>
</a></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
<p style="margin: 0">
<%= gettext "If you need to cancel your participation, just access the event page through link above and click on the participation button." %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View File

@ -0,0 +1,11 @@
<%= gettext "Participation confirmation" %>
==
<%= gettext "You requested to participate in event %{title}.", title: @participant.event.title %>
<%= gettext "If you didn't request this email, you can simply ignore it." %>
<%= participation_email_confirmation_url(Mobilizon.Web.Endpoint, :index, @participant.metadata.confirmation_token) %>
<%= gettext "If you need to cancel your participation, just access the previous link and click on the participation button." %>

View File

@ -8,7 +8,7 @@
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "All good!" %>
</h1>

View File

@ -8,7 +8,7 @@
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Sorry!" %>
</h1>

View File

@ -8,7 +8,7 @@
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Event updated!" %>
</h1>

View File

@ -8,7 +8,7 @@
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Trouble signing in?" %>
</h1>

View File

@ -8,7 +8,7 @@
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "Nearly here!" %>
</h1>

View File

@ -8,7 +8,7 @@
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "New report on %{instance}", instance: @instance[:name] %>
</h1>

View File

@ -0,0 +1,7 @@
defmodule Mobilizon.Web.APIView do
@moduledoc """
View for our the API terms
"""
use Mobilizon.Web, :view
import Mobilizon.Web.Gettext
end

76
mix.exs
View File

@ -162,7 +162,7 @@ defmodule Mobilizon.Mixfile do
Mobilizon.Web,
Mobilizon.Service.Geospatial,
Mobilizon.Web.Resolvers,
Mobilizon.Web.Schema,
Mobilizon.GraphQL.Schema,
Mobilizon.Service
]
]
@ -284,43 +284,43 @@ defmodule Mobilizon.Mixfile do
],
GraphQL: [
Mobilizon.Web.GraphQLSocket,
Mobilizon.Web.Resolvers.Address,
Mobilizon.Web.Resolvers.Admin,
Mobilizon.Web.Resolvers.Comment,
Mobilizon.Web.Resolvers.Config,
Mobilizon.Web.Resolvers.Event,
Mobilizon.Web.Resolvers.FeedToken,
Mobilizon.Web.Resolvers.Group,
Mobilizon.Web.Resolvers.Member,
Mobilizon.Web.Resolvers.Person,
Mobilizon.Web.Resolvers.Picture,
Mobilizon.Web.Resolvers.Report,
Mobilizon.Web.Resolvers.Search,
Mobilizon.Web.Resolvers.Tag,
Mobilizon.Web.Resolvers.User,
Mobilizon.Web.Schema,
Mobilizon.Web.Schema.ActorInterface,
Mobilizon.Web.Schema.Actors.ApplicationType,
Mobilizon.Web.Schema.Actors.FollowerType,
Mobilizon.Web.Schema.Actors.GroupType,
Mobilizon.Web.Schema.Actors.MemberType,
Mobilizon.Web.Schema.Actors.PersonType,
Mobilizon.Web.Schema.AddressType,
Mobilizon.Web.Schema.AdminType,
Mobilizon.Web.Schema.CommentType,
Mobilizon.Web.Schema.ConfigType,
Mobilizon.Web.Schema.EventType,
Mobilizon.Web.Schema.Events.FeedTokenType,
Mobilizon.Web.Schema.Events.ParticipantType,
Mobilizon.Web.Schema.PictureType,
Mobilizon.Web.Schema.ReportType,
Mobilizon.Web.Schema.SearchType,
Mobilizon.Web.Schema.SortType,
Mobilizon.Web.Schema.TagType,
Mobilizon.Web.Schema.UserType,
Mobilizon.Web.Schema.Utils,
Mobilizon.Web.Schema.Custom.Point,
Mobilizon.Web.Schema.Custom.UUID
Mobilizon.GraphQL.Resolvers.Address,
Mobilizon.GraphQL.Resolvers.Admin,
Mobilizon.GraphQL.Resolvers.Comment,
Mobilizon.GraphQL.Resolvers.Config,
Mobilizon.GraphQL.Resolvers.Event,
Mobilizon.GraphQL.Resolvers.FeedToken,
Mobilizon.GraphQL.Resolvers.Group,
Mobilizon.GraphQL.Resolvers.Member,
Mobilizon.GraphQL.Resolvers.Person,
Mobilizon.GraphQL.Resolvers.Picture,
Mobilizon.GraphQL.Resolvers.Report,
Mobilizon.GraphQL.Resolvers.Search,
Mobilizon.GraphQL.Resolvers.Tag,
Mobilizon.GraphQL.Resolvers.User,
Mobilizon.GraphQL.Schema,
Mobilizon.GraphQL.Schema.ActorInterface,
Mobilizon.GraphQL.Schema.Actors.ApplicationType,
Mobilizon.GraphQL.Schema.Actors.FollowerType,
Mobilizon.GraphQL.Schema.Actors.GroupType,
Mobilizon.GraphQL.Schema.Actors.MemberType,
Mobilizon.GraphQL.Schema.Actors.PersonType,
Mobilizon.GraphQL.Schema.AddressType,
Mobilizon.GraphQL.Schema.AdminType,
Mobilizon.GraphQL.Schema.CommentType,
Mobilizon.GraphQL.Schema.ConfigType,
Mobilizon.GraphQL.Schema.EventType,
Mobilizon.GraphQL.Schema.Events.FeedTokenType,
Mobilizon.GraphQL.Schema.Events.ParticipantType,
Mobilizon.GraphQL.Schema.PictureType,
Mobilizon.GraphQL.Schema.ReportType,
Mobilizon.GraphQL.Schema.SearchType,
Mobilizon.GraphQL.Schema.SortType,
Mobilizon.GraphQL.Schema.TagType,
Mobilizon.GraphQL.Schema.UserType,
Mobilizon.GraphQL.Schema.Utils,
Mobilizon.GraphQL.Schema.Custom.Point,
Mobilizon.GraphQL.Schema.Custom.UUID
],
ActivityPub: [
Mobilizon.Federation.ActivityPub,

View File

@ -57,7 +57,7 @@
"geohax": {:hex, :geohax, "0.3.0", "c2e7d8cc6cdf4158120b50fcbe03a296da561d2089eb7ad68d84b6f5d3df5607", [:mix], [], "hexpm"},
"geolix": {:hex, :geolix, "1.0.0", "b225d930fb0418871ce7d89dabf293bd80eb5bd66db1887f80510c122f4ef271", [:mix], [{:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"},
"geolix_adapter_mmdb2": {:hex, :geolix_adapter_mmdb2, "0.2.0", "608d468878ef38ef640bb798699caa05dee2b16c7b175cc1fd53eb30a607b8d7", [:mix], [{:geolix, "~> 1.0", [hex: :geolix, repo: "hexpm", optional: false]}, {:mmdb2_decoder, "~> 1.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}], "hexpm"},
"gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm"},
"guardian": {:hex, :guardian, "2.0.0", "5d3e537832b7cf35c8674da92457b7be671666a2eff4bf0f2ccfcfb3a8c67a0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"guardian_db": {:hex, :guardian_db, "2.0.2", "6247303fda5ed90e19ea1d2e4c5a65b13f58cc12810f95f71b6ffb50ef2d057f", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
"guardian_phoenix": {:hex, :guardian_phoenix, "2.0.1", "89a817265af09a6ddf7cb1e77f17ffca90cea2db10ff888375ef34502b2731b1", [:mix], [{:guardian, "~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},

View File

@ -15,347 +15,613 @@ msgstr ""
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 3.9.1\n"
#: lib/mobilizon_web/templates/email/password_reset.html.eex:48
#: lib/mobilizon_web/templates/email/password_reset.text.eex:12
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:48
#: lib/web/templates/email/password_reset.text.eex:12
msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one."
msgstr ""
#: lib/service/export/feed.ex:169
#, elixir-format
#: lib/service/export/feed.ex:169
msgid "Feed for %{email} on Mobilizon"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:155
#: lib/mobilizon_web/templates/email/email.text.eex:16
#, elixir-format
#: lib/web/templates/email/email.html.eex:155
#: lib/web/templates/email/email.text.eex:16
msgid "%{instance} is a Mobilizon server."
msgstr "%{instance} هو خادم موبيليزون Mobilizon."
#: lib/mobilizon_web/templates/email/report.html.eex:41
#, elixir-format
#: lib/web/templates/email/report.html.eex:41
msgid "%{reporter_name} (%{reporter_username}) reported the following content."
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:52
#, elixir-format
#: lib/web/templates/email/report.html.eex:52
msgid "%{title} by %{creator}"
msgstr "%{title} لِـ %{creator}"
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:58
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:58
msgid "Activate my account"
msgstr "تنشيط حسابي"
#: lib/mobilizon_web/templates/email/email.html.eex:124
#: lib/mobilizon_web/templates/email/email.text.eex:14
#, elixir-format
#: lib/web/templates/email/email.html.eex:124
#: lib/web/templates/email/email.text.eex:14
msgid "Ask the community on Framacolibri"
msgstr "أطلب مِن المجتمَع على Framacolibri"
#: lib/mobilizon_web/templates/email/report.html.eex:66
#: lib/mobilizon_web/templates/email/report.text.eex:13
#, elixir-format
#: lib/web/templates/email/report.html.eex:66
#: lib/web/templates/email/report.text.eex:13
msgid "Comments"
msgstr "التعليقات"
#: lib/mobilizon_web/templates/email/report.html.eex:50
#: lib/mobilizon_web/templates/email/report.text.eex:6
#, elixir-format
#: lib/web/templates/email/report.html.eex:50
#: lib/web/templates/email/report.text.eex:6
msgid "Event"
msgstr "الفعالية"
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:45
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:45
msgid "If you didn't request this, please ignore this email."
msgstr ""
#: lib/mobilizon_web/email/user.ex:45
#, elixir-format
#: lib/web/email/user.ex:48
msgid "Instructions to reset your password on %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:156
#, elixir-format
#: lib/web/templates/email/email.html.eex:156
msgid "Learn more about Mobilizon."
msgstr "تعلّم المزيد عن Mobilizon."
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:13
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:13
msgid "Nearly here!"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:121
#: lib/mobilizon_web/templates/email/email.text.eex:12
#, elixir-format
#: lib/web/templates/email/email.html.eex:121
#: lib/web/templates/email/email.text.eex:12
msgid "Need some help? Something not working properly?"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:13
#, elixir-format
#: lib/web/templates/email/report.html.eex:13
msgid "New report on %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:84
#: lib/mobilizon_web/templates/email/report.text.eex:22
#, elixir-format
#: lib/web/templates/email/report.html.eex:84
#: lib/web/templates/email/report.text.eex:22
msgid "Reason"
msgstr "السبب"
#: lib/mobilizon_web/templates/email/password_reset.html.eex:61
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:61
msgid "Reset Password"
msgstr "تصفير الكلمة السرية"
#: lib/mobilizon_web/templates/email/password_reset.html.eex:41
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:41
msgid "Resetting your password is easy. Just press the button below and follow the instructions. We'll have you up and running in no time."
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.html.eex:13
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:13
msgid "Trouble signing in?"
msgstr "هل واجهتك صعوبات في الولوج؟"
#: lib/mobilizon_web/templates/email/report.html.eex:104
#, elixir-format
#: lib/web/templates/email/report.html.eex:104
msgid "View the report"
msgstr "إعرض التقرير"
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:38
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:38
msgid "You created an account on %{host} with this email address. You are one click away from activating it."
msgstr ""
#: lib/mobilizon_web/email/user.ex:25
#, elixir-format
#: lib/web/email/user.ex:28
msgid "Instructions to confirm your Mobilizon account on %{instance}"
msgstr ""
#: lib/mobilizon_web/email/admin.ex:23
#, elixir-format
#: lib/web/email/admin.ex:23
msgid "New report on Mobilizon instance %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:1
#, elixir-format
#: lib/web/templates/email/registration_confirmation.text.eex:1
msgid "Activate your account"
msgstr "فعّل حسابي"
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:13
#, elixir-format
#: lib/web/templates/email/event_participation_approved.html.eex:13
msgid "All good!"
msgstr "كل شيء على ما يرام!"
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:45
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:7
#, elixir-format
#: lib/web/templates/email/event_participation_approved.html.eex:45
#: lib/web/templates/email/event_participation_approved.text.eex:7
msgid "An organizer just approved your participation. You're now going to this event!"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:58
#: lib/mobilizon_web/templates/email/event_updated.html.eex:101
#, elixir-format
#: lib/web/templates/email/event_participation_approved.html.eex:58
#: lib/web/templates/email/event_updated.html.eex:101
msgid "Go to event page"
msgstr "الإنتقال إلى صفحة الفعالية"
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:70
#: lib/mobilizon_web/templates/email/event_updated.html.eex:113 lib/mobilizon_web/templates/email/event_updated.text.eex:21
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:70
#: lib/web/templates/email/event_participation_approved.html.eex:70 lib/web/templates/email/event_updated.html.eex:113
#: lib/web/templates/email/event_updated.text.eex:21
msgid "If you need to cancel your participation, just access the event page through link above and click on the participation button."
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:11
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:11
#: lib/web/templates/email/event_participation_approved.text.eex:11
msgid "If you need to cancel your participation, just access the previous link and click on the participation button."
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:16
#, elixir-format
#: lib/web/templates/email/email.text.eex:16
msgid "Learn more about Mobilizon:"
msgstr "معرفة المزيد عن Mobilizon:"
#: lib/mobilizon_web/templates/email/report.text.eex:1
#, elixir-format
#: lib/web/templates/email/report.text.eex:1
msgid "New report from %{reporter} on %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:1
#, elixir-format
#: lib/web/templates/email/event_participation_approved.text.eex:1
msgid "Participation approved"
msgstr "تم قبول المشاركة"
#: lib/mobilizon_web/templates/email/event_participation_rejected.text.eex:1
#, elixir-format
#: lib/web/templates/email/event_participation_rejected.text.eex:1
msgid "Participation rejected"
msgstr "تم رفض المشاركة"
#: lib/mobilizon_web/templates/email/password_reset.text.eex:1
#, elixir-format
#: lib/web/templates/email/password_reset.text.eex:1
msgid "Password reset"
msgstr "تصفير الكلمة السرية"
#: lib/mobilizon_web/templates/email/password_reset.text.eex:7
#, elixir-format
#: lib/web/templates/email/password_reset.text.eex:7
msgid "Resetting your password is easy. Just click the link below and follow the instructions. We'll have you up and running in no time."
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_rejected.html.eex:13
#, elixir-format
#: lib/web/templates/email/event_participation_rejected.html.eex:13
msgid "Sorry!"
msgstr "المعذرة!"
#: lib/mobilizon_web/templates/email/event_participation_rejected.html.eex:45
#: lib/mobilizon_web/templates/email/event_participation_rejected.text.eex:7
#, elixir-format
#: lib/web/templates/email/event_participation_rejected.html.eex:45
#: lib/web/templates/email/event_participation_rejected.text.eex:7
msgid "Unfortunately, the organizers rejected your participation."
msgstr "لسوء الحظ ، لقد رفض المُنظّمون طلب مشاركتك."
#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:5
#, elixir-format
#: lib/web/templates/email/registration_confirmation.text.eex:5
msgid "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email."
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:38
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:38
#: lib/web/templates/email/event_participation_approved.html.eex:38
msgid "You requested to participate in event %{title}"
msgstr "لقد قمتَ بتقديم طلب للمشاركة في فعالية %{title}"
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:5
#: lib/mobilizon_web/templates/email/event_participation_rejected.html.eex:38 lib/mobilizon_web/templates/email/event_participation_rejected.text.eex:5
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:5
#: lib/web/templates/email/event_participation_approved.text.eex:5 lib/web/templates/email/event_participation_rejected.html.eex:38
#: lib/web/templates/email/event_participation_rejected.text.eex:5
msgid "You requested to participate in event %{title}."
msgstr "لقد قمتَ بتقديم طلب للمشاركة في فعالية %{title}."
#: lib/mobilizon_web/email/participation.ex:73
#, elixir-format
#: lib/web/email/participation.ex:73
msgid "Your participation to event %{title} has been approved"
msgstr ""
#: lib/mobilizon_web/email/participation.ex:52
#, elixir-format
#: lib/web/email/participation.ex:52
msgid "Your participation to event %{title} has been rejected"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:82
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:82
msgid "Ending of event"
msgstr "نهاية الفعالية"
#: lib/mobilizon_web/email/event.ex:30
#, elixir-format
#: lib/web/email/event.ex:35
msgid "Event %{title} has been updated"
msgstr "تم تحديث الفعالية %{title}"
#: lib/mobilizon_web/templates/email/event_updated.html.eex:13
#: lib/mobilizon_web/templates/email/event_updated.text.eex:1
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:13
#: lib/web/templates/email/event_updated.text.eex:1
msgid "Event updated!"
msgstr "تم تحديث الفعالية!"
#: lib/mobilizon_web/templates/email/event_updated.text.eex:16
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:16
msgid "New date and time for ending of event: %{ends_on}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:12
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:12
msgid "New date and time for start of event: %{begins_on}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:8
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:8
msgid "New title: %{title}"
msgstr "العنوان الجديد: %{title}"
#: lib/mobilizon_web/templates/email/event_updated.html.eex:72
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:72
msgid "Start of event"
msgstr "بداية الفعالية"
#: lib/mobilizon_web/templates/email/event_updated.text.eex:5
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:5
msgid "The event %{title} was just updated"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:38
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:38
msgid "The event %{title} was updated"
msgstr "تم تحديث الفعالية %{title}"
#: lib/mobilizon_web/templates/email/event_updated.html.eex:62
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:62
msgid "Title"
msgstr "العنوان"
#: lib/mobilizon_web/templates/email/event_updated.text.eex:19
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:19
msgid "View the updated event on: %{link}"
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.html.eex:38
#: lib/mobilizon_web/templates/email/password_reset.text.eex:5
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:38
#: lib/web/templates/email/password_reset.text.eex:5
msgid "You requested a new password for your account on %{instance}."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:95
#, elixir-format
#: lib/web/templates/email/email.html.eex:95
msgid "In the meantime, please consider that the software is not (yet) finished. More information %{a_start}on our blog%{a_end}."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:94
#, elixir-format
#: lib/web/templates/email/email.html.eex:94
msgid "Mobilizon is under development, we will add new features to this site during regular updates, until the release of %{b_start}version 1 of the software in the first half of 2020%{b_end}."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:91
#: lib/mobilizon_web/templates/email/email.text.eex:6
#, elixir-format
#: lib/web/templates/email/email.html.eex:91
#: lib/web/templates/email/email.text.eex:6
msgid "This is a demonstration site to test the beta version of Mobilizon."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:89
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "Warning"
msgstr "تنبيه"
#: lib/mobilizon_web/templates/email/event_updated.html.eex:54
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:54
msgid "Event has been cancelled"
msgstr "تم إلغاء الفعالية"
#: lib/mobilizon_web/templates/email/event_updated.html.eex:50
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:50
msgid "Event has been confirmed"
msgstr "تم تأكيد الفعالية"
#: lib/mobilizon_web/templates/email/event_updated.html.eex:52
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:52
msgid "Event status has been set as tentative"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:92
#, elixir-format
#: lib/web/templates/email/email.html.eex:92
msgid "%{b_start}Please do not use it in any real way%{b_end}"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:39
#, elixir-format
#: lib/web/templates/email/report.html.eex:39
msgid "Someone on %{instance} reported the following content."
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:10
#, elixir-format
#: lib/web/templates/email/email.text.eex:10
msgid "In the meantime, please consider that the software is not (yet) finished. More information on our blog:"
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:9
#, elixir-format
#: lib/web/templates/email/email.text.eex:9
msgid "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020."
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:7
#, elixir-format
#: lib/web/templates/email/email.text.eex:7
msgid "Please do not use it in any real way"
msgstr ""
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:58
msgid "Confirm my participation"
msgstr ""
#, elixir-format
#: lib/web/email/participation.ex:95
msgid "Confirm your participation to event %{title}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:45
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:7
msgid "If you didn't request this email, you can simply ignore it."
msgstr ""
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:13
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:1
msgid "Participation confirmation"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:108
msgctxt "terms"
msgid "An internal ID for your current selected identity"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:107
msgctxt "terms"
msgid "An internal user ID"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:45
msgctxt "terms"
msgid "Any of the information we collect from you may be used in the following ways:"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:4
msgctxt "terms"
msgid "Basic account information"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:28
msgctxt "terms"
msgid "Do not share any dangerous information over Mobilizon."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:126
msgctxt "terms"
msgid "Do we disclose any information to outside parties?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:101
msgctxt "terms"
msgid "Do we use cookies?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:59
msgctxt "terms"
msgid "How do we protect your information?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:32
msgctxt "terms"
msgid "IPs and other metadata"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:111
msgctxt "terms"
msgid "If you delete these informations, you need to login again."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:113
msgctxt "terms"
msgid "If you're not connected, we don't store any information on your device, unless you participate in an event\n anonymously. In that case we store the hash of the UUID and participation status in your browser so that we may\n display participation status. Deleting these informations will only stop displaying participation status in your\n browser."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:123
msgctxt "terms"
msgid "Note: These informations are stored in your localStorage and not your cookies."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:18
msgctxt "terms"
msgid "Published events and comments"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:83
msgctxt "terms"
msgid "Retain the IP addresses associated with registered users no more than 12 months."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:109
msgctxt "terms"
msgid "Tokens to authenticate you"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:35
msgctxt "terms"
msgid "We also may retain server logs which include the IP address of every request to our server."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:5
msgctxt "terms"
msgid "We collect information from you when you register on this server and gather data when you participate in the\n platform by reading, writing, and interacting with content shared here. If you register on this server, you will\n be asked to enter an e-mail address, a password and at least an username. Your e-mail address will be verified by\n an email containing a unique link. If that link is visited, we know that you control the e-mail address. You may\n also enter additional profile information such as a display name and biography, and upload a profile picture and\n header image. The username, display name, biography, profile picture and header image are always listed publicly.\n You may, however, visit this server without registering."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:130
msgctxt "terms"
msgid "We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This\n does not include trusted third parties who assist us in operating our site, conducting our business, or servicing\n you, so long as those parties agree to keep this information confidential. We may also release your information\n when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or\n others rights, property, or safety."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:62
msgctxt "terms"
msgid "We implement a variety of security measures to maintain the safety of your personal information when you enter,\n submit, or access your personal information. Among other things, your browser session, as well as the traffic between\n your applications and the API, are secured with SSL/TLS, and your password is hashed using a strong one-way\n algorithm."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:103
msgctxt "terms"
msgid "We store the following information on your device when you connect:"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:72
msgctxt "terms"
msgid "We will make a good faith effort to:"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:43
msgctxt "terms"
msgid "What do we use your information for?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:71
msgctxt "terms"
msgid "What is our data retention policy?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:92
msgctxt "terms"
msgid "You can request and download an archive of your content, including your posts, media attachments, profile picture,\n and header image."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:100
msgctxt "terms"
msgid "You may irreversibly delete your account at any time."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:142
msgctxt "terms"
msgid "Your content may be downloaded by other servers in the network. Your content is delivered to the servers\n following your instance, and direct messages are delivered to the servers of the recipients, in so far as these\n recipients reside on a different server than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:20
msgctxt "terms"
msgid "Your events and comments are delivered to other instances that follow your own, meaning they are delivered to\n different servers and copies are stored there. When you delete events or comments, this is likewise delivered to\n these other instances. The action of joining an event is federated as well. Please keep in mind that the operators\n of the server and any receiving server may view such messages, and that recipients may screenshot, copy or\n otherwise re-share them."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:159
msgctxt "terms"
msgid "Changes to our Privacy Policy"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:154
msgctxt "terms"
msgid "If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (<a href=\"https://en.wikipedia.org/wiki/General_Data_Protection_Regulation\">General Data Protection Regulation</a>) do not use this site."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:155
msgctxt "terms"
msgid "If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (<a href=\"https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act\">Children's Online Privacy Protection Act</a>) do not use this site."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:161
msgctxt "terms"
msgid "If we decide to change our privacy policy, we will post those changes on this page."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:156
msgctxt "terms"
msgid "Law requirements can be different if this server is in another jurisdiction."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:163
msgctxt "terms"
msgid "Originally adapted from the <a href=\"https://mastodon.social/terms\">Mastodon</a> and <a href=\"https://github.com/discourse/discourse\">Discourse</a> privacy policies."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:75
msgctxt "terms"
msgid "Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more\n than 90 days."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:152
msgctxt "terms"
msgid "Site usage by children"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:55
msgctxt "terms"
msgid "The email address you provide may be used to send you information, updates and notifications about other people\n interacting with your content or sending you messages and to respond to inquiries, and/or other requests or\n questions."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:162
msgctxt "terms"
msgid "This document is CC-BY-SA. It was last updated January 16, 2020."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:53
msgctxt "terms"
msgid "To aid moderation of the community, for example comparing your IP address with other known ones to determine ban\n evasion or other violations."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:51
msgctxt "terms"
msgid "To provide the core functionality of Mobilizon. Depending on this instance's policy you may only be able to\n interact with other people's content and post your own content if you are logged in."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:1
msgctxt "terms"
msgid "What information do we collect?"
msgstr ""

View File

@ -12,347 +12,613 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 2.4.0\n"
#: lib/mobilizon_web/templates/email/password_reset.html.eex:48
#: lib/mobilizon_web/templates/email/password_reset.text.eex:12
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:48
#: lib/web/templates/email/password_reset.text.eex:12
msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one."
msgstr ""
#: lib/service/export/feed.ex:169
#, elixir-format
#: lib/service/export/feed.ex:169
msgid "Feed for %{email} on Mobilizon"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:155
#: lib/mobilizon_web/templates/email/email.text.eex:16
#, elixir-format
#: lib/web/templates/email/email.html.eex:155
#: lib/web/templates/email/email.text.eex:16
msgid "%{instance} is a Mobilizon server."
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:41
#, elixir-format
#: lib/web/templates/email/report.html.eex:41
msgid "%{reporter_name} (%{reporter_username}) reported the following content."
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:52
#, elixir-format
#: lib/web/templates/email/report.html.eex:52
msgid "%{title} by %{creator}"
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:58
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:58
msgid "Activate my account"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:124
#: lib/mobilizon_web/templates/email/email.text.eex:14
#, elixir-format
#: lib/web/templates/email/email.html.eex:124
#: lib/web/templates/email/email.text.eex:14
msgid "Ask the community on Framacolibri"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:66
#: lib/mobilizon_web/templates/email/report.text.eex:13
#, elixir-format
#: lib/web/templates/email/report.html.eex:66
#: lib/web/templates/email/report.text.eex:13
msgid "Comments"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:50
#: lib/mobilizon_web/templates/email/report.text.eex:6
#, elixir-format
#: lib/web/templates/email/report.html.eex:50
#: lib/web/templates/email/report.text.eex:6
msgid "Event"
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:45
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:45
msgid "If you didn't request this, please ignore this email."
msgstr ""
#: lib/mobilizon_web/email/user.ex:45
#, elixir-format
#: lib/web/email/user.ex:48
msgid "Instructions to reset your password on %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:156
#, elixir-format
#: lib/web/templates/email/email.html.eex:156
msgid "Learn more about Mobilizon."
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:13
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:13
msgid "Nearly here!"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:121
#: lib/mobilizon_web/templates/email/email.text.eex:12
#, elixir-format
#: lib/web/templates/email/email.html.eex:121
#: lib/web/templates/email/email.text.eex:12
msgid "Need some help? Something not working properly?"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:13
#, elixir-format
#: lib/web/templates/email/report.html.eex:13
msgid "New report on %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:84
#: lib/mobilizon_web/templates/email/report.text.eex:22
#, elixir-format
#: lib/web/templates/email/report.html.eex:84
#: lib/web/templates/email/report.text.eex:22
msgid "Reason"
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.html.eex:61
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:61
msgid "Reset Password"
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.html.eex:41
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:41
msgid "Resetting your password is easy. Just press the button below and follow the instructions. We'll have you up and running in no time."
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.html.eex:13
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:13
msgid "Trouble signing in?"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:104
#, elixir-format
#: lib/web/templates/email/report.html.eex:104
msgid "View the report"
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:38
#, elixir-format
#: lib/web/templates/email/registration_confirmation.html.eex:38
msgid "You created an account on %{host} with this email address. You are one click away from activating it."
msgstr ""
#: lib/mobilizon_web/email/user.ex:25
#, elixir-format
#: lib/web/email/user.ex:28
msgid "Instructions to confirm your Mobilizon account on %{instance}"
msgstr ""
#: lib/mobilizon_web/email/admin.ex:23
#, elixir-format
#: lib/web/email/admin.ex:23
msgid "New report on Mobilizon instance %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:1
#, elixir-format
#: lib/web/templates/email/registration_confirmation.text.eex:1
msgid "Activate your account"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:13
#, elixir-format
#: lib/web/templates/email/event_participation_approved.html.eex:13
msgid "All good!"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:45
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:7
#, elixir-format
#: lib/web/templates/email/event_participation_approved.html.eex:45
#: lib/web/templates/email/event_participation_approved.text.eex:7
msgid "An organizer just approved your participation. You're now going to this event!"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:58
#: lib/mobilizon_web/templates/email/event_updated.html.eex:101
#, elixir-format
#: lib/web/templates/email/event_participation_approved.html.eex:58
#: lib/web/templates/email/event_updated.html.eex:101
msgid "Go to event page"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:70
#: lib/mobilizon_web/templates/email/event_updated.html.eex:113 lib/mobilizon_web/templates/email/event_updated.text.eex:21
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:70
#: lib/web/templates/email/event_participation_approved.html.eex:70 lib/web/templates/email/event_updated.html.eex:113
#: lib/web/templates/email/event_updated.text.eex:21
msgid "If you need to cancel your participation, just access the event page through link above and click on the participation button."
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:11
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:11
#: lib/web/templates/email/event_participation_approved.text.eex:11
msgid "If you need to cancel your participation, just access the previous link and click on the participation button."
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:16
#, elixir-format
#: lib/web/templates/email/email.text.eex:16
msgid "Learn more about Mobilizon:"
msgstr ""
#: lib/mobilizon_web/templates/email/report.text.eex:1
#, elixir-format
#: lib/web/templates/email/report.text.eex:1
msgid "New report from %{reporter} on %{instance}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:1
#, elixir-format
#: lib/web/templates/email/event_participation_approved.text.eex:1
msgid "Participation approved"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_rejected.text.eex:1
#, elixir-format
#: lib/web/templates/email/event_participation_rejected.text.eex:1
msgid "Participation rejected"
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.text.eex:1
#, elixir-format
#: lib/web/templates/email/password_reset.text.eex:1
msgid "Password reset"
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.text.eex:7
#, elixir-format
#: lib/web/templates/email/password_reset.text.eex:7
msgid "Resetting your password is easy. Just click the link below and follow the instructions. We'll have you up and running in no time."
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_rejected.html.eex:13
#, elixir-format
#: lib/web/templates/email/event_participation_rejected.html.eex:13
msgid "Sorry!"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_rejected.html.eex:45
#: lib/mobilizon_web/templates/email/event_participation_rejected.text.eex:7
#, elixir-format
#: lib/web/templates/email/event_participation_rejected.html.eex:45
#: lib/web/templates/email/event_participation_rejected.text.eex:7
msgid "Unfortunately, the organizers rejected your participation."
msgstr ""
#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:5
#, elixir-format
#: lib/web/templates/email/registration_confirmation.text.eex:5
msgid "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email."
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.html.eex:38
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:38
#: lib/web/templates/email/event_participation_approved.html.eex:38
msgid "You requested to participate in event %{title}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_participation_approved.text.eex:5
#: lib/mobilizon_web/templates/email/event_participation_rejected.html.eex:38 lib/mobilizon_web/templates/email/event_participation_rejected.text.eex:5
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:5
#: lib/web/templates/email/event_participation_approved.text.eex:5 lib/web/templates/email/event_participation_rejected.html.eex:38
#: lib/web/templates/email/event_participation_rejected.text.eex:5
msgid "You requested to participate in event %{title}."
msgstr ""
#: lib/mobilizon_web/email/participation.ex:73
#, elixir-format
#: lib/web/email/participation.ex:73
msgid "Your participation to event %{title} has been approved"
msgstr ""
#: lib/mobilizon_web/email/participation.ex:52
#, elixir-format
#: lib/web/email/participation.ex:52
msgid "Your participation to event %{title} has been rejected"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:82
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:82
msgid "Ending of event"
msgstr ""
#: lib/mobilizon_web/email/event.ex:30
#, elixir-format
#: lib/web/email/event.ex:35
msgid "Event %{title} has been updated"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:13
#: lib/mobilizon_web/templates/email/event_updated.text.eex:1
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:13
#: lib/web/templates/email/event_updated.text.eex:1
msgid "Event updated!"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:16
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:16
msgid "New date and time for ending of event: %{ends_on}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:12
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:12
msgid "New date and time for start of event: %{begins_on}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:8
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:8
msgid "New title: %{title}"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:72
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:72
msgid "Start of event"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:5
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:5
msgid "The event %{title} was just updated"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:38
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:38
msgid "The event %{title} was updated"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:62
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:62
msgid "Title"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.text.eex:19
#, elixir-format
#: lib/web/templates/email/event_updated.text.eex:19
msgid "View the updated event on: %{link}"
msgstr ""
#: lib/mobilizon_web/templates/email/password_reset.html.eex:38
#: lib/mobilizon_web/templates/email/password_reset.text.eex:5
#, elixir-format
#: lib/web/templates/email/password_reset.html.eex:38
#: lib/web/templates/email/password_reset.text.eex:5
msgid "You requested a new password for your account on %{instance}."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:95
#, elixir-format
#: lib/web/templates/email/email.html.eex:95
msgid "In the meantime, please consider that the software is not (yet) finished. More information %{a_start}on our blog%{a_end}."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:94
#, elixir-format
#: lib/web/templates/email/email.html.eex:94
msgid "Mobilizon is under development, we will add new features to this site during regular updates, until the release of %{b_start}version 1 of the software in the first half of 2020%{b_end}."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:91
#: lib/mobilizon_web/templates/email/email.text.eex:6
#, elixir-format
#: lib/web/templates/email/email.html.eex:91
#: lib/web/templates/email/email.text.eex:6
msgid "This is a demonstration site to test the beta version of Mobilizon."
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:89
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "Warning"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:54
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:54
msgid "Event has been cancelled"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:50
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:50
msgid "Event has been confirmed"
msgstr ""
#: lib/mobilizon_web/templates/email/event_updated.html.eex:52
#, elixir-format
#: lib/web/templates/email/event_updated.html.eex:52
msgid "Event status has been set as tentative"
msgstr ""
#: lib/mobilizon_web/templates/email/email.html.eex:92
#, elixir-format
#: lib/web/templates/email/email.html.eex:92
msgid "%{b_start}Please do not use it in any real way%{b_end}"
msgstr ""
#: lib/mobilizon_web/templates/email/report.html.eex:39
#, elixir-format
#: lib/web/templates/email/report.html.eex:39
msgid "Someone on %{instance} reported the following content."
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:10
#, elixir-format
#: lib/web/templates/email/email.text.eex:10
msgid "In the meantime, please consider that the software is not (yet) finished. More information on our blog:"
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:9
#, elixir-format
#: lib/web/templates/email/email.text.eex:9
msgid "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020."
msgstr ""
#: lib/mobilizon_web/templates/email/email.text.eex:7
#, elixir-format
#: lib/web/templates/email/email.text.eex:7
msgid "Please do not use it in any real way"
msgstr ""
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:58
msgid "Confirm my participation"
msgstr ""
#, elixir-format
#: lib/web/email/participation.ex:95
msgid "Confirm your participation to event %{title}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:45
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:7
msgid "If you didn't request this email, you can simply ignore it."
msgstr ""
#, elixir-format
#: lib/web/templates/email/anonymous_participation_confirmation.html.eex:13
#: lib/web/templates/email/anonymous_participation_confirmation.text.eex:1
msgid "Participation confirmation"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:108
msgctxt "terms"
msgid "An internal ID for your current selected identity"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:107
msgctxt "terms"
msgid "An internal user ID"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:45
msgctxt "terms"
msgid "Any of the information we collect from you may be used in the following ways:"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:4
msgctxt "terms"
msgid "Basic account information"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:28
msgctxt "terms"
msgid "Do not share any dangerous information over Mobilizon."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:126
msgctxt "terms"
msgid "Do we disclose any information to outside parties?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:101
msgctxt "terms"
msgid "Do we use cookies?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:59
msgctxt "terms"
msgid "How do we protect your information?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:32
msgctxt "terms"
msgid "IPs and other metadata"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:111
msgctxt "terms"
msgid "If you delete these informations, you need to login again."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:113
msgctxt "terms"
msgid "If you're not connected, we don't store any information on your device, unless you participate in an event\n anonymously. In that case we store the hash of the UUID and participation status in your browser so that we may\n display participation status. Deleting these informations will only stop displaying participation status in your\n browser."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:123
msgctxt "terms"
msgid "Note: These informations are stored in your localStorage and not your cookies."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:18
msgctxt "terms"
msgid "Published events and comments"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:83
msgctxt "terms"
msgid "Retain the IP addresses associated with registered users no more than 12 months."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:109
msgctxt "terms"
msgid "Tokens to authenticate you"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:35
msgctxt "terms"
msgid "We also may retain server logs which include the IP address of every request to our server."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:5
msgctxt "terms"
msgid "We collect information from you when you register on this server and gather data when you participate in the\n platform by reading, writing, and interacting with content shared here. If you register on this server, you will\n be asked to enter an e-mail address, a password and at least an username. Your e-mail address will be verified by\n an email containing a unique link. If that link is visited, we know that you control the e-mail address. You may\n also enter additional profile information such as a display name and biography, and upload a profile picture and\n header image. The username, display name, biography, profile picture and header image are always listed publicly.\n You may, however, visit this server without registering."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:130
msgctxt "terms"
msgid "We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This\n does not include trusted third parties who assist us in operating our site, conducting our business, or servicing\n you, so long as those parties agree to keep this information confidential. We may also release your information\n when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or\n others rights, property, or safety."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:62
msgctxt "terms"
msgid "We implement a variety of security measures to maintain the safety of your personal information when you enter,\n submit, or access your personal information. Among other things, your browser session, as well as the traffic between\n your applications and the API, are secured with SSL/TLS, and your password is hashed using a strong one-way\n algorithm."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:103
msgctxt "terms"
msgid "We store the following information on your device when you connect:"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:72
msgctxt "terms"
msgid "We will make a good faith effort to:"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:43
msgctxt "terms"
msgid "What do we use your information for?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:71
msgctxt "terms"
msgid "What is our data retention policy?"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:92
msgctxt "terms"
msgid "You can request and download an archive of your content, including your posts, media attachments, profile picture,\n and header image."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:100
msgctxt "terms"
msgid "You may irreversibly delete your account at any time."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:142
msgctxt "terms"
msgid "Your content may be downloaded by other servers in the network. Your content is delivered to the servers\n following your instance, and direct messages are delivered to the servers of the recipients, in so far as these\n recipients reside on a different server than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:20
msgctxt "terms"
msgid "Your events and comments are delivered to other instances that follow your own, meaning they are delivered to\n different servers and copies are stored there. When you delete events or comments, this is likewise delivered to\n these other instances. The action of joining an event is federated as well. Please keep in mind that the operators\n of the server and any receiving server may view such messages, and that recipients may screenshot, copy or\n otherwise re-share them."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:159
msgctxt "terms"
msgid "Changes to our Privacy Policy"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:154
msgctxt "terms"
msgid "If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (<a href=\"https://en.wikipedia.org/wiki/General_Data_Protection_Regulation\">General Data Protection Regulation</a>) do not use this site."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:155
msgctxt "terms"
msgid "If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (<a href=\"https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act\">Children's Online Privacy Protection Act</a>) do not use this site."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:161
msgctxt "terms"
msgid "If we decide to change our privacy policy, we will post those changes on this page."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:156
msgctxt "terms"
msgid "Law requirements can be different if this server is in another jurisdiction."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:163
msgctxt "terms"
msgid "Originally adapted from the <a href=\"https://mastodon.social/terms\">Mastodon</a> and <a href=\"https://github.com/discourse/discourse\">Discourse</a> privacy policies."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:75
msgctxt "terms"
msgid "Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more\n than 90 days."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:152
msgctxt "terms"
msgid "Site usage by children"
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:55
msgctxt "terms"
msgid "The email address you provide may be used to send you information, updates and notifications about other people\n interacting with your content or sending you messages and to respond to inquiries, and/or other requests or\n questions."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:162
msgctxt "terms"
msgid "This document is CC-BY-SA. It was last updated January 16, 2020."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:53
msgctxt "terms"
msgid "To aid moderation of the community, for example comparing your IP address with other known ones to determine ban\n evasion or other violations."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:51
msgctxt "terms"
msgid "To provide the core functionality of Mobilizon. Depending on this instance's policy you may only be able to\n interact with other people's content and post your own content if you are logged in."
msgstr ""
#, elixir-format
#: lib/web/templates/api/terms.html.eex:1
msgctxt "terms"
msgid "What information do we collect?"
msgstr ""

Some files were not shown because too many files have changed in this diff Show More