Merge branch 'event-from-group' into 'master'

Allow to create an event from a group preconfigured with the organizer

Closes #464

See merge request framasoft/mobilizon!877
This commit is contained in:
Thomas Citharel 2021-03-29 09:19:48 +00:00
commit 0cccc9dd9c
21 changed files with 555 additions and 212 deletions

View file

@ -1,11 +1,11 @@
<template> <template>
<div class="list is-hoverable"> <div class="list is-hoverable">
<b-radio-button <b-radio-button
v-model="currentActor" v-model="selectedActor"
:native-value="availableActor" :native-value="availableActor"
class="list-item" class="list-item"
v-for="availableActor in actualAvailableActors" v-for="availableActor in actualAvailableActors"
:class="{ 'is-active': availableActor.id === currentActor.id }" :class="{ 'is-active': availableActor.id === selectedActor.id }"
:key="availableActor.id" :key="availableActor.id"
> >
<div class="media"> <div class="media">
@ -31,9 +31,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IPerson, IActor, Actor } from "@/types/actor"; import { IPerson, IActor, Actor } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor"; import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
LOGGED_USER_MEMBERSHIPS,
} from "@/graphql/actor";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums"; import { MemberRole } from "@/types/enums";
@ -41,29 +45,37 @@ import { MemberRole } from "@/types/enums";
@Component({ @Component({
apollo: { apollo: {
groupMemberships: { groupMemberships: {
query: PERSON_MEMBERSHIPS, query: LOGGED_USER_MEMBERSHIPS,
variables() { update: (data) => data.loggedUser.memberships,
return {
id: this.identity.id,
};
},
update: (data) => data.person.memberships,
skip() {
return !this.identity.id;
},
}, },
identities: IDENTITIES,
currentActor: CURRENT_ACTOR_CLIENT,
}, },
}) })
export default class OrganizerPicker extends Vue { export default class OrganizerPicker extends Vue {
@Prop() value!: IActor; @Prop() value!: IActor;
@Prop() identity!: IPerson;
@Prop({ required: false, default: false }) restrictModeratorLevel!: boolean; @Prop({ required: false, default: false }) restrictModeratorLevel!: boolean;
groupMemberships: Paginate<IMember> = { elements: [], total: 0 }; groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
currentActor: IActor = this.value; currentActor!: IPerson;
get selectedActor(): IActor | undefined {
if (this.value?.id) {
return this.value;
}
if (this.currentActor) {
return this.currentActor;
}
return undefined;
}
set selectedActor(actor: IActor | undefined) {
this.$emit("input", actor);
}
identities: IActor[] = [];
Actor = Actor; Actor = Actor;
@ -82,14 +94,12 @@ export default class OrganizerPicker extends Vue {
get actualAvailableActors(): IActor[] { get actualAvailableActors(): IActor[] {
return [ return [
this.identity, this.currentActor,
...this.identities.filter(
(identity: IActor) => identity.id !== this.currentActor?.id
),
...this.actualMemberships.map((member) => member.parent), ...this.actualMemberships.map((member) => member.parent),
]; ].filter((elem) => elem);
}
@Watch("currentActor")
async fetchMembersForGroup(): Promise<void> {
this.$emit("input", this.currentActor);
} }
} }
</script> </script>

View file

@ -1,30 +1,30 @@
<template> <template>
<div class="organizer-picker"> <div class="organizer-picker" v-if="selectedActor">
<!-- If we have a current actor (inline) --> <!-- If we have a current actor (inline) -->
<div <div
v-if="inline && currentActor.id" v-if="inline && selectedActor.id"
class="inline box" class="inline box"
@click="isComponentModalActive = true" @click="isComponentModalActive = true"
> >
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-48x48" v-if="currentActor.avatar"> <figure class="image is-48x48" v-if="selectedActor.avatar">
<img <img
class="image is-rounded" class="image is-rounded"
:src="currentActor.avatar.url" :src="selectedActor.avatar.url"
:alt="currentActor.avatar.alt" :alt="selectedActor.avatar.alt"
/> />
</figure> </figure>
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
</div> </div>
<div class="media-content" v-if="currentActor.name"> <div class="media-content" v-if="selectedActor.name">
<p class="is-4">{{ currentActor.name }}</p> <p class="is-4">{{ selectedActor.name }}</p>
<p class="is-6 has-text-grey"> <p class="is-6 has-text-grey">
{{ `@${currentActor.preferredUsername}` }} {{ `@${selectedActor.preferredUsername}` }}
</p> </p>
</div> </div>
<div class="media-content" v-else> <div class="media-content" v-else>
{{ `@${currentActor.preferredUsername}` }} {{ `@${selectedActor.preferredUsername}` }}
</div> </div>
<b-button type="is-text" @click="isComponentModalActive = true"> <b-button type="is-text" @click="isComponentModalActive = true">
{{ $t("Change") }} {{ $t("Change") }}
@ -33,45 +33,18 @@
</div> </div>
<!-- If we have a current actor --> <!-- If we have a current actor -->
<span <span
v-else-if="currentActor.id" v-else-if="selectedActor.id"
class="block" class="block"
@click="isComponentModalActive = true" @click="isComponentModalActive = true"
> >
<img <img
class="image is-48x48" class="image is-48x48"
v-if="currentActor.avatar" v-if="selectedActor.avatar"
:src="currentActor.avatar.url" :src="selectedActor.avatar.url"
:alt="currentActor.avatar.alt" :alt="selectedActor.avatar.alt"
/> />
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
</span> </span>
<!-- If we have no current actor -->
<div v-if="groupMemberships.total === 0 || !currentActor.id" class="box">
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="identity.avatar">
<img
class="image is-rounded"
:src="identity.avatar.url"
:alt="identity.avatar.alt"
/>
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content" v-if="identity.name">
<p class="is-4">{{ identity.name }}</p>
<p class="is-6 has-text-grey">
{{ `@${identity.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${identity.preferredUsername}` }}
</div>
<b-button type="is-text" @click="isComponentModalActive = true">
{{ $t("Change") }}
</b-button>
</div>
</div>
<b-modal :active.sync="isComponentModalActive" has-modal-card> <b-modal :active.sync="isComponentModalActive" has-modal-card>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
@ -81,20 +54,15 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<organizer-picker <organizer-picker
v-model="currentActor" v-model="selectedActor"
:identity.sync="identity"
@input="relay" @input="relay"
:restrict-moderator-level="true" :restrict-moderator-level="true"
/> />
</div> </div>
<div class="column"> <div class="column">
<div v-if="actorMembersForCurrentActor.length > 0"> <div v-if="actorMembers.length > 0">
<p>{{ $t("Add a contact") }}</p> <p>{{ $t("Add a contact") }}</p>
<p <p class="field" v-for="actor in actorMembers" :key="actor.id">
class="field"
v-for="actor in actorMembersForCurrentActor"
:key="actor.id"
>
<b-checkbox v-model="actualContacts" :native-value="actor.id"> <b-checkbox v-model="actualContacts" :native-value="actor.id">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
@ -138,79 +106,121 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { IActor, IGroup, IPerson } from "../../types/actor"; import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor";
import OrganizerPicker from "./OrganizerPicker.vue"; import OrganizerPicker from "./OrganizerPicker.vue";
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor"; import {
CURRENT_ACTOR_CLIENT,
LOGGED_USER_MEMBERSHIPS,
} from "../../graphql/actor";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";
import { GROUP_MEMBERS } from "@/graphql/member";
import { ActorType, MemberRole } from "@/types/enums";
const MEMBER_ROLES = [
MemberRole.CREATOR,
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
MemberRole.MEMBER,
];
@Component({ @Component({
components: { OrganizerPicker }, components: { OrganizerPicker },
apollo: { apollo: {
groupMemberships: { members: {
query: PERSON_MEMBERSHIPS_WITH_MEMBERS, query: GROUP_MEMBERS,
variables() { variables() {
return { return {
id: this.identity.id, name: usernameWithDomain(this.selectedActor),
page: this.membersPage,
limit: 10,
roles: MEMBER_ROLES.join(","),
}; };
}, },
update: (data) => data.person.memberships, update: (data) => data.group.members,
skip() { skip() {
return !this.identity.id; return (
!this.selectedActor || this.selectedActor.type !== ActorType.GROUP
);
}, },
}, },
currentActor: CURRENT_ACTOR_CLIENT,
userMemberships: {
query: LOGGED_USER_MEMBERSHIPS,
variables: {
page: 1,
limit: 100,
},
update: (data) => data.loggedUser.memberships,
},
}, },
}) })
export default class OrganizerPickerWrapper extends Vue { export default class OrganizerPickerWrapper extends Vue {
@Prop({ type: Object, required: true }) value!: IActor; @Prop({ type: Object, required: false }) value!: IActor;
@Prop({ default: true, type: Boolean }) inline!: boolean; @Prop({ default: true, type: Boolean }) inline!: boolean;
@Prop({ type: Object, required: true }) identity!: IPerson; currentActor!: IPerson;
isComponentModalActive = false; isComponentModalActive = false;
currentActor: IActor = this.value;
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
@Prop({ type: Array, required: false, default: () => [] }) @Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[]; contacts!: IActor[];
members: Paginate<IMember> = { elements: [], total: 0 };
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id); membersPage = 1;
@Watch("contacts") userMemberships: Paginate<IMember> = { elements: [], total: 0 };
updateActualContacts(contacts: IActor[]): void {
this.actualContacts = contacts.map(({ id }) => id); get actualContacts(): (string | undefined)[] {
return this.contacts.map(({ id }) => id);
} }
@Watch("value") set actualContacts(contactsIds: (string | undefined)[]) {
updateCurrentActor(value: IGroup): void { this.$emit(
this.currentActor = value; "update:contacts",
this.actorMembers.filter(({ id }) => contactsIds.includes(id))
);
}
@Watch("userMemberships")
setInitialActor(): void {
if (this.$route.query?.actorId) {
const actorId = this.$route.query?.actorId as string;
this.$router.replace({ query: undefined });
const actor = this.userMemberships.elements.find(
({ parent: { id }, role }) =>
actorId === id && MEMBER_ROLES.includes(role)
)?.parent as IActor;
this.selectedActor = actor;
}
}
get selectedActor(): IActor | undefined {
if (this.value?.id) {
return this.value;
}
if (this.currentActor) {
return this.currentActor;
}
return undefined;
}
set selectedActor(selectedActor: IActor | undefined) {
this.$emit("input", selectedActor);
} }
async relay(group: IGroup): Promise<void> { async relay(group: IGroup): Promise<void> {
this.currentActor = group; this.actualContacts = [];
this.selectedActor = group;
} }
pickActor(): void { pickActor(): void {
this.$emit(
"update:contacts",
this.actorMembersForCurrentActor.filter(({ id }) =>
this.actualContacts.includes(id)
)
);
this.$emit("input", this.currentActor);
this.isComponentModalActive = false; this.isComponentModalActive = false;
} }
get actorMembersForCurrentActor(): IActor[] { get actorMembers(): IActor[] {
const currentMembership = this.groupMemberships.elements.find( if (this.selectedActor?.type === ActorType.GROUP) {
({ parent: { id } }) => id === this.currentActor.id return this.members.elements.map(({ actor }: { actor: IActor }) => actor);
);
if (currentMembership) {
return currentMembership.parent.members.elements.map(
({ actor }: { actor: IActor }) => actor
);
} }
return []; return [];
} }

View file

@ -14,7 +14,7 @@
:to="{ name: RouteName.PREFERENCES }" :to="{ name: RouteName.PREFERENCES }"
/> />
<SettingMenuItem <SettingMenuItem
:title="this.$t('Email notifications')" :title="this.$t('Notifications')"
:to="{ name: RouteName.NOTIFICATIONS }" :to="{ name: RouteName.NOTIFICATIONS }"
/> />
</SettingMenuSection> </SettingMenuSection>

View file

@ -319,6 +319,7 @@ export const LOGGED_USER_MEMBERSHIPS = gql`
preferredUsername preferredUsername
domain domain
name name
type
avatar { avatar {
id id
url url
@ -359,6 +360,7 @@ export const IDENTITIES = gql`
id id
url url
} }
type
preferredUsername preferredUsername
name name
} }
@ -379,6 +381,7 @@ export const PERSON_MEMBERSHIPS = gql`
preferredUsername preferredUsername
name name
domain domain
type
avatar { avatar {
id id
url url
@ -397,55 +400,6 @@ export const PERSON_MEMBERSHIPS = gql`
} }
`; `;
export const PERSON_MEMBERSHIPS_WITH_MEMBERS = gql`
query PersonMembershipsWithMembers($id: ID!) {
person(id: $id) {
id
memberships {
total
elements {
id
role
parent {
id
preferredUsername
name
domain
avatar {
id
url
}
members {
total
elements {
id
role
actor {
id
preferredUsername
name
domain
avatar {
id
url
}
}
}
}
}
invitedBy {
id
preferredUsername
name
}
insertedAt
updatedAt
}
}
}
}
`;
export const PERSON_MEMBERSHIP_GROUP = gql` export const PERSON_MEMBERSHIP_GROUP = gql`
query PersonMembershipGroup($id: ID!, $group: String!) { query PersonMembershipGroup($id: ID!, $group: String!) {
person(id: $id) { person(id: $id) {

View file

@ -241,3 +241,17 @@ export const UPDATE_USER_LOCALE = gql`
} }
} }
`; `;
export const FEED_TOKENS_LOGGED_USER = gql`
query {
loggedUser {
id
feedTokens {
token
actor {
id
}
}
}
}
`;

View file

@ -969,5 +969,15 @@
"You replied to a comment on the event {event}.": "You replied to a comment on the event {event}.", "You replied to a comment on the event {event}.": "You replied to a comment on the event {event}.",
"{profile} replied to a comment on the event {event}.": "{profile} replied to a comment on the event {event}.", "{profile} replied to a comment on the event {event}.": "{profile} replied to a comment on the event {event}.",
"New post": "New post", "New post": "New post",
"Comment text can't be empty": "Comment text can't be empty" "Comment text can't be empty": "Comment text can't be empty",
"Notifications": "Notifications",
"Profile feeds": "Profile feeds",
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.",
"Regenerate new links": "Regenerate new links",
"Create new links": "Create new links",
"You'll need to change the URLs where there were previously entered.": "You'll need to change the URLs where there were previously entered.",
"Personal feeds": "Personal feeds",
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.",
"The event will show as attributed to this profile.": "The event will show as attributed to this profile.",
"You may show some members as contacts.": "You may show some members as contacts."
} }

View file

@ -1063,5 +1063,15 @@
"You replied to a comment on the event {event}.": "Vous avez répondu à un commentaire sur l'événement {event}.", "You replied to a comment on the event {event}.": "Vous avez répondu à un commentaire sur l'événement {event}.",
"{profile} replied to a comment on the event {event}.": "{profile} a répondu à un commentaire sur l'événement {event}.", "{profile} replied to a comment on the event {event}.": "{profile} a répondu à un commentaire sur l'événement {event}.",
"New post": "Nouveau billet", "New post": "Nouveau billet",
"Comment text can't be empty": "Le texte du commentaire ne peut être vide" "Comment text can't be empty": "Le texte du commentaire ne peut être vide",
"Notifications": "Notifications",
"Profile feeds": "Flux du profil",
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "Ces flux contiennent des informations sur les événements pour lesquels ce profil spécifique est un⋅e participant⋅e ou un⋅e créateur⋅ice. Vous devriez les garder privés. Vous pouvez trouver des flux pour l'ensemble de vos profils dans vos paramètres de notification.",
"Regenerate new links": "Regénérer de nouveaux liens",
"Create new links": "Créer de nouveaux liens",
"You'll need to change the URLs where there were previously entered.": "Vous devrez changer les URLs là où vous les avez entrées précédemment.",
"Personal feeds": "Flux personnels",
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "Ces flux contiennent des informations sur les événements pour lesquels n'importe lequel de vos profils est un⋅e participant⋅e ou un⋅e créateur⋅ice. Vous devriez les garder privés. Vous pouvez trouver des flux spécifiques à chaque profil sur la page d'édition des profils.",
"The event will show as attributed to this profile.": "L'événement sera affiché comme attribué à ce profil.",
"You may show some members as contacts.": "Vous pouvez afficher certain⋅es membres en tant que contacts."
} }

View file

@ -98,6 +98,77 @@
$t("Delete this identity") $t("Delete this identity")
}}</span> }}</span>
</div> </div>
<section v-if="isUpdate">
<div class="setting-title">
<h2>{{ $t("Profile feeds") }}</h2>
</div>
<p>
{{
$t(
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings."
)
}}
</p>
<div v-if="identity.feedTokens && identity.feedTokens.length > 0">
<div
class="buttons"
v-for="feedToken in identity.feedTokens"
:key="feedToken.token"
>
<b-tooltip
:label="$t('URL copied to clipboard')"
:active="showCopiedTooltip.atom"
always
type="is-success"
position="is-left"
>
<b-button
tag="a"
icon-left="rss"
@click="
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
"
:href="tokenToURL(feedToken.token, 'atom')"
target="_blank"
>{{ $t("RSS/Atom Feed") }}</b-button
>
</b-tooltip>
<b-tooltip
:label="$t('URL copied to clipboard')"
:active="showCopiedTooltip.ics"
always
type="is-success"
position="is-left"
>
<b-button
tag="a"
@click="
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
"
icon-left="calendar-sync"
:href="tokenToURL(feedToken.token, 'ics')"
target="_blank"
>{{ $t("ICS/WebCal Feed") }}</b-button
>
</b-tooltip>
<b-button
icon-left="refresh"
type="is-text"
@click="openRegenerateFeedTokensConfirmation"
>{{ $t("Regenerate new links") }}</b-button
>
</div>
</div>
<div v-else>
<b-button
icon-left="refresh"
type="is-text"
@click="generateFeedTokens"
>{{ $t("Create new links") }}</b-button
>
</div>
</section>
</div> </div>
</div> </div>
</template> </template>
@ -131,6 +202,10 @@ h1 {
.username-field + .field { .username-field + .field {
margin-bottom: 0; margin-bottom: 0;
} }
::v-deep .buttons > *:not(:last-child) .button {
margin-right: 0.5rem;
}
</style> </style>
<script lang="ts"> <script lang="ts">
@ -151,6 +226,11 @@ import RouteName from "../../../router/name";
import { buildFileVariable } from "../../../utils/image"; import { buildFileVariable } from "../../../utils/image";
import { changeIdentity } from "../../../utils/auth"; import { changeIdentity } from "../../../utils/auth";
import identityEditionMixin from "../../../mixins/identityEdition"; import identityEditionMixin from "../../../mixins/identityEdition";
import {
CREATE_FEED_TOKEN_ACTOR,
DELETE_FEED_TOKEN,
} from "@/graphql/feed_tokens";
import { IFeedToken } from "@/types/feedtoken.model";
@Component({ @Component({
components: { components: {
@ -191,6 +271,8 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
RouteName = RouteName; RouteName = RouteName;
showCopiedTooltip = { ics: false, atom: false };
get message(): string | null { get message(): string | null {
if (this.isUpdate) return null; if (this.isUpdate) return null;
return this.$t( return this.$t(
@ -353,6 +435,63 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
return MOBILIZON_INSTANCE_HOST; return MOBILIZON_INSTANCE_HOST;
} }
tokenToURL(token: string, format: string): string {
return `${window.location.origin}/events/going/${token}/${format}`;
}
copyURL(e: Event, url: string, format: "ics" | "atom"): void {
if (navigator.clipboard) {
e.preventDefault();
navigator.clipboard.writeText(url);
this.showCopiedTooltip[format] = true;
setTimeout(() => {
this.showCopiedTooltip[format] = false;
}, 2000);
}
}
async generateFeedTokens(): Promise<void> {
const newToken = await this.createNewFeedToken();
this.identity.feedTokens.push(newToken);
}
async regenerateFeedTokens(): Promise<void> {
if (this.identity?.feedTokens.length < 1) return;
await this.deleteFeedToken(this.identity.feedTokens[0].token);
const newToken = await this.createNewFeedToken();
this.identity.feedTokens.pop();
this.identity.feedTokens.push(newToken);
}
private async deleteFeedToken(token: string): Promise<void> {
await this.$apollo.mutate({
mutation: DELETE_FEED_TOKEN,
variables: { token },
});
}
private async createNewFeedToken(): Promise<IFeedToken> {
const { data } = await this.$apollo.mutate({
mutation: CREATE_FEED_TOKEN_ACTOR,
variables: { actor_id: this.identity?.id },
});
return data.createFeedToken;
}
openRegenerateFeedTokensConfirmation(): void {
this.$buefy.dialog.confirm({
type: "is-warning",
title: this.$t("Regenerate new links") as string,
message: this.$t(
"You'll need to change the URLs where there were previously entered."
) as string,
confirmText: this.$t("Regenerate new links") as string,
cancelText: this.$t("Cancel") as string,
onConfirm: () => this.regenerateFeedTokens(),
});
}
openDeleteIdentityConfirmation(): void { openDeleteIdentityConfirmation(): void {
this.$buefy.dialog.prompt({ this.$buefy.dialog.prompt({
type: "is-danger", type: "is-danger",

View file

@ -81,19 +81,21 @@
<subtitle>{{ $t("Organizers") }}</subtitle> <subtitle>{{ $t("Organizers") }}</subtitle>
<div v-if="config && config.features.groups"> <div v-if="config && config.features.groups && organizerActor.id">
<b-field> <b-field>
<organizer-picker-wrapper <organizer-picker-wrapper
v-model="event.attributedTo" v-model="organizerActor"
:contacts.sync="event.contacts" :contacts.sync="event.contacts"
:identity="event.organizerActor"
/> />
</b-field> </b-field>
<p v-if="!event.attributedTo.id || attributedToEqualToOrganizerActor"> <p v-if="!attributedToAGroup && organizerActorEqualToCurrentActor">
{{ {{
$t("The event will show as attributed to your personal profile.") $t("The event will show as attributed to your personal profile.")
}} }}
</p> </p>
<p v-else-if="!attributedToAGroup">
{{ $t("The event will show as attributed to this profile.") }}
</p>
<p v-else> <p v-else>
<span>{{ <span>{{
$t("The event will show as attributed to this group.") $t("The event will show as attributed to this group.")
@ -101,6 +103,7 @@
<span <span
v-if="event.contacts && event.contacts.length" v-if="event.contacts && event.contacts.length"
v-html=" v-html="
' ' +
$tc( $tc(
'<b>{contact}</b> will be displayed as contact.', '<b>{contact}</b> will be displayed as contact.',
event.contacts.length, event.contacts.length,
@ -114,6 +117,9 @@
) )
" "
/> />
<span v-else>
{{ $t("You may show some members as contacts.") }}
</span>
</p> </p>
</div> </div>
<subtitle>{{ $t("Who can view this event and participate") }}</subtitle> <subtitle>{{ $t("Who can view this event and participate") }}</subtitle>
@ -432,6 +438,7 @@ import Subtitle from "@/components/Utils/Subtitle.vue";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { formatList } from "@/utils/i18n"; import { formatList } from "@/utils/i18n";
import { import {
ActorType,
CommentModeration, CommentModeration,
EventJoinOptions, EventJoinOptions,
EventStatus, EventStatus,
@ -448,10 +455,11 @@ import {
import { EventModel, IEvent } from "../../types/event.model"; import { EventModel, IEvent } from "../../types/event.model";
import { import {
CURRENT_ACTOR_CLIENT, CURRENT_ACTOR_CLIENT,
IDENTITIES,
LOGGED_USER_DRAFTS, LOGGED_USER_DRAFTS,
LOGGED_USER_PARTICIPATIONS, LOGGED_USER_PARTICIPATIONS,
} from "../../graphql/actor"; } from "../../graphql/actor";
import { IPerson, Person, displayNameAndUsername } from "../../types/actor"; import { displayNameAndUsername, IActor, IGroup } from "../../types/actor";
import { TAGS } from "../../graphql/tags"; import { TAGS } from "../../graphql/tags";
import { ITag } from "../../types/tag.model"; import { ITag } from "../../types/tag.model";
import { import {
@ -480,6 +488,7 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
tags: TAGS, tags: TAGS,
config: CONFIG, config: CONFIG,
identities: IDENTITIES,
event: { event: {
query: FETCH_EVENT, query: FETCH_EVENT,
variables() { variables() {
@ -513,12 +522,14 @@ export default class EditEvent extends Vue {
@Prop({ type: Boolean, default: false }) isDuplicate!: boolean; @Prop({ type: Boolean, default: false }) isDuplicate!: boolean;
currentActor = new Person(); currentActor!: IActor;
tags: ITag[] = []; tags: ITag[] = [];
event: IEvent = new EventModel(); event: IEvent = new EventModel();
identities: IActor[] = [];
config!: IConfig; config!: IConfig;
unmodifiedEvent!: IEvent; unmodifiedEvent!: IEvent;
@ -573,16 +584,32 @@ export default class EditEvent extends Vue {
this.event.beginsOn = now; this.event.beginsOn = now;
this.event.endsOn = end; this.event.endsOn = end;
this.event.organizerActor = this.getDefaultActor();
} }
private getDefaultActor() { get organizerActor(): IActor {
if (this.event.organizerActor?.id) { if (this.event?.attributedTo?.id) {
return this.event.attributedTo;
}
if (this.event?.organizerActor?.id) {
return this.event.organizerActor; return this.event.organizerActor;
} }
return this.currentActor; return this.currentActor;
} }
set organizerActor(actor: IActor) {
if (actor?.type === ActorType.GROUP) {
this.event.attributedTo = actor as IGroup;
this.event.organizerActor = this.currentActor;
} else {
this.event.attributedTo = undefined;
this.event.organizerActor = actor;
}
}
get attributedToAGroup(): boolean {
return this.event.attributedTo?.id !== undefined;
}
async mounted(): Promise<void> { async mounted(): Promise<void> {
this.observer = new IntersectionObserver( this.observer = new IntersectionObserver(
(entries) => { (entries) => {
@ -724,8 +751,10 @@ export default class EditEvent extends Vue {
return !( return !(
this.eventId && this.eventId &&
this.event.organizerActor?.id !== undefined && this.event.organizerActor?.id !== undefined &&
this.currentActor.id !== this.event.organizerActor.id !this.identities
) as boolean; .map(({ id }) => id)
.includes(this.event.organizerActor?.id)
);
} }
get updateEventMessage(): string { get updateEventMessage(): string {
@ -752,8 +781,7 @@ export default class EditEvent extends Vue {
*/ */
private postCreateOrUpdate(store: any, updateEvent: IEvent) { private postCreateOrUpdate(store: any, updateEvent: IEvent) {
const resultEvent: IEvent = { ...updateEvent }; const resultEvent: IEvent = { ...updateEvent };
const organizerActor: IPerson = this.event.organizerActor as Person; resultEvent.organizerActor = this.event.organizerActor;
resultEvent.organizerActor = organizerActor;
resultEvent.relatedEvents = []; resultEvent.relatedEvents = [];
store.writeQuery({ store.writeQuery({
@ -766,12 +794,12 @@ export default class EditEvent extends Vue {
query: EVENT_PERSON_PARTICIPATION, query: EVENT_PERSON_PARTICIPATION,
variables: { variables: {
eventId: updateEvent.id, eventId: updateEvent.id,
name: organizerActor.preferredUsername, name: this.event.organizerActor?.preferredUsername,
}, },
data: { data: {
person: { person: {
__typename: "Person", __typename: "Person",
id: organizerActor.id, id: this.event?.organizerActor?.id,
participations: { participations: {
__typename: "PaginatedParticipantList", __typename: "PaginatedParticipantList",
total: 1, total: 1,
@ -782,7 +810,7 @@ export default class EditEvent extends Vue {
role: ParticipantRole.CREATOR, role: ParticipantRole.CREATOR,
actor: { actor: {
__typename: "Actor", __typename: "Actor",
id: organizerActor.id, id: this.event?.organizerActor?.id,
}, },
event: { event: {
__typename: "Event", __typename: "Event",
@ -819,28 +847,24 @@ export default class EditEvent extends Vue {
]; ];
} }
get attributedToEqualToOrganizerActor(): boolean { get organizerActorEqualToCurrentActor(): boolean {
return (this.event.organizerActor?.id !== undefined && return (
this.event.attributedTo?.id === this.event.organizerActor?.id) as boolean; this.currentActor?.id !== undefined &&
this.organizerActor?.id === this.currentActor?.id
);
} }
/** /**
* Build variables for Event GraphQL creation query * Build variables for Event GraphQL creation query
*/ */
private async buildVariables() { private async buildVariables() {
this.event.organizerActor = this.event.organizerActor?.id
? this.event.organizerActor
: this.currentActor;
let res = this.event.toEditJSON(); let res = this.event.toEditJSON();
if (this.event.organizerActor) { if (this.event.organizerActor) {
res = Object.assign(res, { res = Object.assign(res, {
organizerActorId: this.event.organizerActor.id, organizerActorId: this.event.organizerActor.id,
}); });
} }
const attributedToId = const attributedToId = this.event.attributedTo?.id
this.event.attributedTo &&
!this.attributedToEqualToOrganizerActor &&
this.event.attributedTo.id
? this.event.attributedTo.id ? this.event.attributedTo.id
: null; : null;
res = Object.assign(res, { attributedToId }); res = Object.assign(res, { attributedToId });

View file

@ -327,6 +327,7 @@
v-if="isCurrentActorAGroupModerator" v-if="isCurrentActorAGroupModerator"
:to="{ :to="{
name: RouteName.CREATE_EVENT, name: RouteName.CREATE_EVENT,
query: { actorId: group.id },
}" }"
class="button is-primary" class="button is-primary"
>{{ $t("+ Create an event") }}</router-link >{{ $t("+ Create an event") }}</router-link

View file

@ -9,7 +9,7 @@
</li> </li>
<li class="is-active"> <li class="is-active">
<router-link :to="{ name: RouteName.NOTIFICATIONS }">{{ <router-link :to="{ name: RouteName.NOTIFICATIONS }">{{
$t("Email notifications") $t("Notifications")
}}</router-link> }}</router-link>
</li> </li>
</ul> </ul>
@ -118,23 +118,108 @@
</b-select> </b-select>
</div> </div>
</section> </section>
<section>
<div class="setting-title">
<h2>{{ $t("Personal feeds") }}</h2>
</div>
<p>
{{
$t(
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page."
)
}}
</p>
<div v-if="feedTokens && feedTokens.length > 0">
<div
class="buttons"
v-for="feedToken in feedTokens"
:key="feedToken.token"
>
<b-tooltip
:label="$t('URL copied to clipboard')"
:active="showCopiedTooltip.atom"
always
type="is-success"
position="is-left"
>
<b-button
tag="a"
icon-left="rss"
@click="
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
"
:href="tokenToURL(feedToken.token, 'atom')"
target="_blank"
>{{ $t("RSS/Atom Feed") }}</b-button
>
</b-tooltip>
<b-tooltip
:label="$t('URL copied to clipboard')"
:active="showCopiedTooltip.ics"
always
type="is-success"
position="is-left"
>
<b-button
tag="a"
@click="
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
"
icon-left="calendar-sync"
:href="tokenToURL(feedToken.token, 'ics')"
target="_blank"
>{{ $t("ICS/WebCal Feed") }}</b-button
>
</b-tooltip>
<b-button
icon-left="refresh"
type="is-text"
@click="openRegenerateFeedTokensConfirmation"
>{{ $t("Regenerate new links") }}</b-button
>
</div>
</div>
<div v-else>
<b-button
icon-left="refresh"
type="is-text"
@click="generateFeedTokens"
>{{ $t("Create new links") }}</b-button
>
</div>
</section>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator"; import { Component, Vue, Watch } from "vue-property-decorator";
import { INotificationPendingEnum } from "@/types/enums"; import { INotificationPendingEnum } from "@/types/enums";
import { USER_SETTINGS, SET_USER_SETTINGS } from "../../graphql/user"; import {
USER_SETTINGS,
SET_USER_SETTINGS,
FEED_TOKENS_LOGGED_USER,
} from "../../graphql/user";
import { IUser } from "../../types/current-user.model"; import { IUser } from "../../types/current-user.model";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { IFeedToken } from "@/types/feedtoken.model";
import { CREATE_FEED_TOKEN, DELETE_FEED_TOKEN } from "@/graphql/feed_tokens";
@Component({ @Component({
apollo: { apollo: {
loggedUser: USER_SETTINGS, loggedUser: USER_SETTINGS,
feedTokens: {
query: FEED_TOKENS_LOGGED_USER,
update: (data) =>
data.loggedUser.feedTokens.filter(
(token: IFeedToken) => token.actor === null
),
},
}, },
}) })
export default class Notifications extends Vue { export default class Notifications extends Vue {
loggedUser!: IUser; loggedUser!: IUser;
feedTokens: IFeedToken[] = [];
notificationOnDay: boolean | undefined = true; notificationOnDay: boolean | undefined = true;
notificationEachWeek: boolean | undefined = false; notificationEachWeek: boolean | undefined = false;
@ -148,6 +233,8 @@ export default class Notifications extends Vue {
RouteName = RouteName; RouteName = RouteName;
showCopiedTooltip = { ics: false, atom: false };
mounted(): void { mounted(): void {
this.notificationPendingParticipationValues = { this.notificationPendingParticipationValues = {
[INotificationPendingEnum.NONE]: this.$t("Do not receive any mail"), [INotificationPendingEnum.NONE]: this.$t("Do not receive any mail"),
@ -176,6 +263,62 @@ export default class Notifications extends Vue {
refetchQueries: [{ query: USER_SETTINGS }], refetchQueries: [{ query: USER_SETTINGS }],
}); });
} }
tokenToURL(token: string, format: string): string {
return `${window.location.origin}/events/going/${token}/${format}`;
}
copyURL(e: Event, url: string, format: "ics" | "atom"): void {
if (navigator.clipboard) {
e.preventDefault();
navigator.clipboard.writeText(url);
this.showCopiedTooltip[format] = true;
setTimeout(() => {
this.showCopiedTooltip[format] = false;
}, 2000);
}
}
openRegenerateFeedTokensConfirmation(): void {
this.$buefy.dialog.confirm({
type: "is-warning",
title: this.$t("Regenerate new links") as string,
message: this.$t(
"You'll need to change the URLs where there were previously entered."
) as string,
confirmText: this.$t("Regenerate new links") as string,
cancelText: this.$t("Cancel") as string,
onConfirm: () => this.regenerateFeedTokens(),
});
}
async regenerateFeedTokens(): Promise<void> {
if (this.feedTokens.length < 1) return;
await this.deleteFeedToken(this.feedTokens[0].token);
const newToken = await this.createNewFeedToken();
this.feedTokens.pop();
this.feedTokens.push(newToken);
}
async generateFeedTokens(): Promise<void> {
const newToken = await this.createNewFeedToken();
this.feedTokens.push(newToken);
}
private async deleteFeedToken(token: string): Promise<void> {
await this.$apollo.mutate({
mutation: DELETE_FEED_TOKEN,
variables: { token },
});
}
private async createNewFeedToken(): Promise<IFeedToken> {
const { data } = await this.$apollo.mutate({
mutation: CREATE_FEED_TOKEN,
});
return data.createFeedToken;
}
} }
</script> </script>
@ -193,4 +336,8 @@ export default class Notifications extends Vue {
margin-left: 5px; margin-left: 5px;
} }
} }
::v-deep .buttons > *:not(:last-child) .button {
margin-right: 0.5rem;
}
</style> </style>

View file

@ -22,7 +22,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
) do ) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, feed_token} <- Events.create_feed_token(%{user_id: id, actor_id: actor_id}) do {:ok, feed_token} <- Events.create_feed_token(%{user_id: id, actor_id: actor_id}) do
{:ok, feed_token} {:ok, to_short_uuid(feed_token)}
else else
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, dgettext("errors", "Profile is not owned by authenticated user")}
@ -32,7 +32,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
@spec create_feed_token(any, map, map) :: {:ok, FeedToken.t()} @spec create_feed_token(any, map, map) :: {:ok, FeedToken.t()}
def create_feed_token(_parent, %{}, %{context: %{current_user: %User{id: id}}}) do def create_feed_token(_parent, %{}, %{context: %{current_user: %User{id: id}}}) do
with {:ok, feed_token} <- Events.create_feed_token(%{user_id: id}) do with {:ok, feed_token} <- Events.create_feed_token(%{user_id: id}) do
{:ok, feed_token} {:ok, to_short_uuid(feed_token)}
end end
end end
@ -50,7 +50,8 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
%{token: token}, %{token: token},
%{context: %{current_user: %User{id: id} = _user}} %{context: %{current_user: %User{id: id} = _user}}
) do ) do
with {:ok, token} <- Ecto.UUID.cast(token), with {:ok, token} <- ShortUUID.decode(token),
{:ok, token} <- Ecto.UUID.cast(token),
{:no_token, %FeedToken{actor: actor, user: %User{} = user} = feed_token} <- {:no_token, %FeedToken{actor: actor, user: %User{} = user} = feed_token} <-
{:no_token, Events.get_feed_token(token)}, {:no_token, Events.get_feed_token(token)},
{:token_from_user, true} <- {:token_from_user, id == user.id}, {:token_from_user, true} <- {:token_from_user, id == user.id},
@ -65,6 +66,9 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
:error -> :error ->
{:error, dgettext("errors", "Token is not a valid UUID")} {:error, dgettext("errors", "Token is not a valid UUID")}
{:error, "Invalid input"} ->
{:error, dgettext("errors", "Token is not a valid UUID")}
{:no_token, _} -> {:no_token, _} ->
{:error, dgettext("errors", "Token does not exist")} {:error, dgettext("errors", "Token does not exist")}
@ -77,4 +81,8 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
def delete_feed_token(_parent, _args, %{}) do def delete_feed_token(_parent, _args, %{}) do
{:error, dgettext("errors", "You are not allowed to delete a feed token if not connected")} {:error, dgettext("errors", "You are not allowed to delete a feed token if not connected")}
end end
defp to_short_uuid(%FeedToken{token: token} = feed_token) do
%FeedToken{feed_token | token: ShortUUID.encode!(token)}
end
end end

View file

@ -315,7 +315,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
context: %{current_user: user} context: %{current_user: user}
}) do }) do
with {:is_owned, %Actor{id: actor_id}} <- User.owns_actor(user, actor_id), with {:is_owned, %Actor{id: actor_id}} <- User.owns_actor(user, actor_id),
%Actor{id: group_id} <- Actors.get_actor_by_name(group, :Group), {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id), {:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id),
memberships <- %Page{ memberships <- %Page{
total: 1, total: 1,
@ -326,6 +326,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
{:error, :member_not_found} -> {:error, :member_not_found} ->
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
{:group, nil} ->
{:error, :group_not_found}
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, dgettext("errors", "Profile is not owned by authenticated user")}
end end

View file

@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
""" """
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1] import Absinthe.Resolution.Helpers, only: [dataloader: 2]
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.GraphQL.Resolvers.{Media, Person} alias Mobilizon.GraphQL.Resolvers.{Media, Person}
@ -53,7 +53,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
) )
field(:feed_tokens, list_of(:feed_token), field(:feed_tokens, list_of(:feed_token),
resolve: dataloader(Events), resolve:
dataloader(
Events,
callback: fn feed_tokens, _parent, _args ->
{:ok, Enum.map(feed_tokens, &Map.put(&1, :token, ShortUUID.encode!(&1.token)))}
end
),
description: "A list of the feed tokens for this person" description: "A list of the feed tokens for this person"
) )

View file

@ -31,7 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
description: "The actor that participates to the event" description: "The actor that participates to the event"
) )
field(:token, :string, description: "The role of this actor at this event") field(:token, :string, description: "A ShortUUID private token")
end end
@desc "Represents a deleted feed_token" @desc "Represents a deleted feed_token"

View file

@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
""" """
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1] import Absinthe.Resolution.Helpers, only: [dataloader: 2]
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.GraphQL.Resolvers.{Media, User} alias Mobilizon.GraphQL.Resolvers.{Media, User}
@ -43,7 +43,13 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
) )
field(:feed_tokens, list_of(:feed_token), field(:feed_tokens, list_of(:feed_token),
resolve: dataloader(Events), resolve:
dataloader(
Events,
callback: fn feed_tokens, _parent, _args ->
{:ok, Enum.map(feed_tokens, &Map.put(&1, :token, ShortUUID.encode!(&1.token)))}
end
),
description: "A list of the feed tokens for this user" description: "A list of the feed tokens for this user"
) )

View file

@ -25,8 +25,9 @@ defmodule Mobilizon.Service.Export.Common do
# Only events, not posts # Only events, not posts
@spec fetch_events_from_token(String.t()) :: String.t() @spec fetch_events_from_token(String.t()) :: String.t()
def fetch_events_from_token(token) do def fetch_events_from_token(token) do
with {:ok, _uuid} <- Ecto.UUID.cast(token), with {:ok, uuid} <- ShortUUID.decode(token),
%FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do {:ok, _uuid} <- Ecto.UUID.cast(uuid),
%FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(uuid) do
case actor do case actor do
%Actor{} = actor -> %Actor{} = actor ->
%{ %{

View file

@ -26,7 +26,7 @@ defmodule Mobilizon.Web.FeedController do
end end
def event(conn, %{"uuid" => uuid, "format" => "ics"}) do def event(conn, %{"uuid" => uuid, "format" => "ics"}) do
return_data(conn, "ics", "event_" <> uuid, "event.ics") return_data(conn, "ics", "event_" <> uuid, "event")
end end
def event(_conn, _) do def event(_conn, _) do
@ -34,7 +34,7 @@ defmodule Mobilizon.Web.FeedController do
end end
def going(conn, %{"token" => token, "format" => format}) when format in @formats do def going(conn, %{"token" => token, "format" => format}) when format in @formats do
return_data(conn, format, "token_" <> token, "events.#{format}") return_data(conn, format, "token_" <> token, "events")
end end
def going(_conn, _) do def going(_conn, _) do

View file

@ -186,7 +186,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
%{ %{
"feedTokens" => [ "feedTokens" => [
%{ %{
"token" => feed_token.token "token" => ShortUUID.encode!(feed_token.token)
} }
] ]
} }
@ -194,7 +194,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
mutation = """ mutation = """
mutation { mutation {
deleteFeedToken( deleteFeedToken(
token: "#{feed_token.token}", token: "#{ShortUUID.encode!(feed_token.token)}",
) { ) {
actor { actor {
id id
@ -270,7 +270,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
mutation = """ mutation = """
mutation { mutation {
deleteFeedToken( deleteFeedToken(
token: "#{feed_token.token}", token: "#{ShortUUID.encode!(feed_token.token)}",
) { ) {
actor { actor {
id id
@ -320,7 +320,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedTokenTest do
mutation = """ mutation = """
mutation { mutation {
deleteFeedToken( deleteFeedToken(
token: "#{uuid}" token: "#{ShortUUID.encode!(uuid)}"
) { ) {
actor { actor {
id id

View file

@ -58,7 +58,7 @@ defmodule Mobilizon.Service.ICalendarTest do
event = insert(:event) event = insert(:event)
insert(:participant, event: event, actor: actor, role: :participant) insert(:participant, event: event, actor: actor, role: :participant)
{:commit, ics} = ICalendarService.create_cache("token_#{token}") {:commit, ics} = ICalendarService.create_cache("token_#{ShortUUID.encode!(token)}")
assert ics =~ event.title assert ics =~ event.title
end end
end end

View file

@ -225,7 +225,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
conn conn
|> get( |> get(
Endpoint Endpoint
|> Routes.feed_url(:going, feed_token.token, "atom") |> Routes.feed_url(:going, ShortUUID.encode!(feed_token.token), "atom")
|> URI.decode() |> URI.decode()
) )
@ -260,7 +260,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
|> put_req_header("accept", "application/atom+xml") |> put_req_header("accept", "application/atom+xml")
|> get( |> get(
Endpoint Endpoint
|> Routes.feed_url(:going, feed_token.token, "atom") |> Routes.feed_url(:going, ShortUUID.encode!(feed_token.token), "atom")
|> URI.decode() |> URI.decode()
) )
@ -307,7 +307,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
|> put_req_header("accept", "text/calendar") |> put_req_header("accept", "text/calendar")
|> get( |> get(
Endpoint Endpoint
|> Routes.feed_url(:going, feed_token.token, "ics") |> Routes.feed_url(:going, ShortUUID.encode!(feed_token.token), "ics")
|> URI.decode() |> URI.decode()
) )
@ -338,7 +338,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
|> put_req_header("accept", "text/calendar") |> put_req_header("accept", "text/calendar")
|> get( |> get(
Endpoint Endpoint
|> Routes.feed_url(:going, feed_token.token, "ics") |> Routes.feed_url(:going, ShortUUID.encode!(feed_token.token), "ics")
|> URI.decode() |> URI.decode()
) )