Add anonymous and remote participations
This commit is contained in:
parent
17e0b3968f
commit
2ed9050a90
@ -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},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
1
js/src/assets/undraw_mail_2.svg
Normal file
1
js/src/assets/undraw_mail_2.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.6 KiB |
1
js/src/assets/undraw_profile.svg
Normal file
1
js/src/assets/undraw_profile.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.9 KiB |
@ -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;
|
||||
|
171
js/src/components/Event/EventListViewCard.vue
Normal file
171
js/src/components/Event/EventListViewCard.vue
Normal 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>
|
@ -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>
|
@ -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>
|
||||
|
59
js/src/components/Participation/ConfirmParticipation.vue
Normal file
59
js/src/components/Participation/ConfirmParticipation.vue
Normal 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>
|
82
js/src/components/Participation/ParticipationWithAccount.vue
Normal file
82
js/src/components/Participation/ParticipationWithAccount.vue
Normal 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>
|
109
js/src/components/Participation/ParticipationWithoutAccount.vue
Normal file
109
js/src/components/Participation/ParticipationWithoutAccount.vue
Normal 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>
|
104
js/src/components/Participation/UnloggedParticipation.vue
Normal file
104
js/src/components/Participation/UnloggedParticipation.vue
Normal 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>
|
22
js/src/components/Utils/VerticalDivider.vue
Normal file
22
js/src/components/Utils/VerticalDivider.vue
Normal 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>
|
@ -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}
|
||||
`;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -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) {
|
||||
|
@ -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>.",
|
||||
"Mobilizon’s licence": "Mobilizon’s 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 Framasoft’s statement of intent on the Framablog": "Read Framasoft’s 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 won’t change the world from Facebook. The tool we dream of, surveillance capitalism corporations won’t develop it, as they couldn’t profit from it. This is an opportunity to build something better, by taking another approach.": "We won’t change the world from Facebook. The tool we dream of, surveillance capitalism corporations won’t develop it, as they couldn’t 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…",
|
||||
|
@ -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 l’anniversaire entre ami·e·s à une marche pour le climat, aujourd’hui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment s’organiser, comment cliquer sur « je participe » sans <b>livrer des données intimes</b> à Facebook ou<b> s’enfermer</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>.",
|
||||
"Mobilizon’s 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 Framasoft’s statement of intent on the Framablog": "Lire la note d’intention 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 l’identité « {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 s’approprier, conçu dans <b>le respect de la vie privée et de l’action 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 won’t change the world from Facebook. The tool we dream of, surveillance capitalism corporations won’t develop it, as they couldn’t profit from it. This is an opportunity to build something better, by taking another approach.": "On ne changera pas le monde depuis Facebook. L’outil dont nous rêvons, les entreprises du capitalisme de surveillance sont incapables de le produire, car elles ne sauraient pas en tirer profit. C’est l’occasion de faire mieux qu’elles, 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": "s’interconnecter simplement avec d’autres",
|
||||
"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é…",
|
||||
|
@ -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, {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
128
js/src/services/AnonymousParticipationStorage.ts
Normal file
128
js/src/services/AnonymousParticipationStorage.ts
Normal 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,
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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[] = [];
|
||||
|
@ -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;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<section class="section container">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
94
js/src/views/Admin/Settings.vue
Normal file
94
js/src/views/Admin/Settings.vue
Normal 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>
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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
57
js/src/views/Interact.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
64
js/src/views/Terms.vue
Normal 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>
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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') }}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="section container">
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<h1 class="title">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -73,6 +73,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
"joinMode" => %{
|
||||
"@id" => "mz:joinMode",
|
||||
"@type" => "mz:joinModeType"
|
||||
},
|
||||
"anonymousParticipationEnabled" => %{
|
||||
"@id" => "mz:anonymousParticipationEnabled",
|
||||
"@type" => "sc:Boolean"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
|
262
lib/graphql/resolvers/participant.ex
Normal file
262
lib/graphql/resolvers/participant.ex
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
27
lib/mobilizon/admin/setting.ex
Normal file
27
lib/mobilizon/admin/setting.ex
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
163
lib/web/templates/api/terms.html.eex
Normal file
163
lib/web/templates/api/terms.html.eex
Normal 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>
|
@ -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>
|
@ -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." %>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
7
lib/web/views/api_view.ex
Normal file
7
lib/web/views/api_view.ex
Normal 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
76
mix.exs
@ -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,
|
||||
|
2
mix.lock
2
mix.lock
@ -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"},
|
||||
|
@ -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 ""
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user