Improve member adding and excluding flow

Allow to exclude a member

Send emails to the member when it's excluded

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-08-14 11:32:23 +02:00
parent ad13a57afc
commit 156eba0551
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
94 changed files with 2650 additions and 1862 deletions

View file

@ -67,14 +67,14 @@
"@types/vuedraggable": "^2.23.0", "@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0", "@typescript-eslint/parser": "^2.26.0",
"@vue/cli-plugin-babel": "~4.5.2", "@vue/cli-plugin-babel": "~4.5.3",
"@vue/cli-plugin-e2e-cypress": "~4.5.2", "@vue/cli-plugin-e2e-cypress": "~4.5.3",
"@vue/cli-plugin-eslint": "~4.5.2", "@vue/cli-plugin-eslint": "~4.5.3",
"@vue/cli-plugin-pwa": "~4.5.2", "@vue/cli-plugin-pwa": "~4.5.3",
"@vue/cli-plugin-router": "~4.5.2", "@vue/cli-plugin-router": "~4.5.3",
"@vue/cli-plugin-typescript": "~4.5.2", "@vue/cli-plugin-typescript": "~4.5.3",
"@vue/cli-plugin-unit-mocha": "~4.5.2", "@vue/cli-plugin-unit-mocha": "~4.5.3",
"@vue/cli-service": "~4.5.2", "@vue/cli-service": "~4.5.3",
"@vue/eslint-config-airbnb": "^5.0.2", "@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2", "@vue/eslint-config-typescript": "^5.0.2",

View file

@ -255,7 +255,7 @@ export default class Comment extends Vue {
get commentFromOrganizer(): boolean { get commentFromOrganizer(): boolean {
return ( return (
this.event.organizerActor !== undefined && this.event.organizerActor !== undefined &&
this.comment.actor && this.comment.actor != null &&
this.comment.actor.id === this.event.organizerActor.id this.comment.actor.id === this.event.organizerActor.id
); );
} }
@ -272,6 +272,7 @@ export default class Comment extends Vue {
} }
reportModal() { reportModal() {
if (!this.comment.actor) return;
this.$buefy.modal.open({ this.$buefy.modal.open({
parent: this, parent: this,
component: ReportModal, component: ReportModal,
@ -286,6 +287,7 @@ export default class Comment extends Vue {
async reportComment(content: string, forward: boolean) { async reportComment(content: string, forward: boolean) {
try { try {
if (!this.comment.actor) return;
await this.$apollo.mutate<IReport>({ await this.$apollo.mutate<IReport>({
mutation: CREATE_REPORT, mutation: CREATE_REPORT,
variables: { variables: {

View file

@ -106,6 +106,7 @@ export default class CommentTree extends Vue {
async createCommentForEvent(comment: IComment) { async createCommentForEvent(comment: IComment) {
try { try {
if (!comment.actor) return;
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: CREATE_COMMENT_FROM_EVENT, mutation: CREATE_COMMENT_FROM_EVENT,
variables: { variables: {

View file

@ -8,26 +8,102 @@
</div> </div>
<div class="body"> <div class="body">
<div class="meta"> <div class="meta">
<div class="name"> <span class="first-line name" v-if="!comment.deletedAt">
<span>@{{ comment.actor.preferredUsername }}</span> <strong>{{ comment.actor.name }}</strong>
</div> <small>@{{ usernameWithDomain(comment.actor) }}</small>
</span>
<a v-else class="name comment-link has-text-grey">
<span>{{ $t("[deleted]") }}</span>
</a>
<span class="icons" v-if="!comment.deletedAt">
<b-dropdown aria-role="list">
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
<b-dropdown-item
v-if="comment.actor.id === currentActor.id"
@click="toggleEditMode"
aria-role="menuitem"
>
<b-icon icon="pencil"></b-icon>
{{ $t("Edit") }}
</b-dropdown-item>
<b-dropdown-item
v-if="comment.actor.id === currentActor.id"
@click="$emit('delete-comment', comment)"
aria-role="menuitem"
>
<b-icon icon="delete"></b-icon>
{{ $t("Delete") }}
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" @click="isReportModalActive = true">
<b-icon icon="flag" />
{{ $t("Report") }}
</b-dropdown-item>
</b-dropdown>
</span>
<div class="post-infos"> <div class="post-infos">
<span :title="comment.insertedAt | formatDateTimeString"> <span :title="comment.updatedAt | formatDateTimeString">
{{ $timeAgo.format(comment.insertedAt, "twitter") || $t("Right now") }}</span {{ $timeAgo.format(new Date(comment.updatedAt), "twitter") || $t("Right now") }}</span
> >
</div> </div>
</div> </div>
<div class="description-content" v-html="comment.text"></div> <div
class="description-content"
v-html="comment.text"
v-if="!editMode && !comment.deletedAt"
></div>
<div v-else-if="!editMode">{{ $t("[This comment has been deleted]") }}</div>
<form v-else class="edition" @submit.prevent="updateComment">
<editor v-model="updatedComment" />
<div class="buttons">
<b-button
native-type="submit"
:disabled="['<p></p>', '', comment.text].includes(updatedComment)"
type="is-primary"
>{{ $t("Update") }}</b-button
>
<b-button native-type="button" @click="toggleEditMode">{{ $t("Cancel") }}</b-button>
</div>
</form>
</div> </div>
</article> </article>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IComment, CommentModel } from "../../types/comment.model"; import { IComment, CommentModel } from "../../types/comment.model";
import { usernameWithDomain, IPerson } from "../../types/actor";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
@Component @Component({
apollo: {
currentActor: CURRENT_ACTOR_CLIENT,
},
components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
})
export default class DiscussionComment extends Vue { export default class DiscussionComment extends Vue {
@Prop({ required: true, type: Object }) comment!: IComment; @Prop({ required: true, type: Object }) comment!: IComment;
editMode: boolean = false;
updatedComment: string = "";
currentActor!: IPerson;
usernameWithDomain = usernameWithDomain;
isReportModalActive: boolean = false;
toggleEditMode() {
this.updatedComment = this.comment.text;
this.editMode = !this.editMode;
}
updateComment() {
this.comment.text = this.updatedComment;
this.$emit("update-comment", this.comment);
this.toggleEditMode();
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -52,10 +128,20 @@ article.comment {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
strong {
display: block;
line-height: 1rem;
}
span { span {
color: #3c376e; color: #3c376e;
} }
} }
.icons {
display: inline;
cursor: pointer;
}
} }
div.description-content { div.description-content {
@ -108,5 +194,11 @@ article.comment {
padding-top: 1rem; padding-top: 1rem;
flex: 0; flex: 0;
} }
.edition {
.button {
margin-top: 0.75rem;
}
}
} }
</style> </style>

View file

@ -4,14 +4,25 @@
:to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }" :to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }"
> >
<div class="media-left"> <div class="media-left">
<figure class="image is-32x32" v-if="discussion.lastComment.actor.avatar"> <figure
class="image is-32x32"
v-if="discussion.lastComment.actor && discussion.lastComment.actor.avatar"
>
<img class="is-rounded" :src="discussion.lastComment.actor.avatar.url" alt /> <img class="is-rounded" :src="discussion.lastComment.actor.avatar.url" alt />
</figure> </figure>
<b-icon v-else size="is-medium" icon="account-circle" /> <b-icon v-else size="is-medium" icon="account-circle" />
</div> </div>
<div class="title-info-wrapper"> <div class="title-info-wrapper">
<div class="title-and-date">
<p class="discussion-minimalist-title">{{ discussion.title }}</p> <p class="discussion-minimalist-title">{{ discussion.title }}</p>
<div class="has-text-grey">{{ htmlTextEllipsis }}</div> <span :title="discussion.updatedAt | formatDateTimeString">
{{ $timeAgo.format(new Date(discussion.updatedAt), "twitter") || $t("Right now") }}</span
>
</div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }}
</div>
<div v-else class="has-text-grey">{{ $t("[This comment has been deleted]") }}</div>
</div> </div>
</router-link> </router-link>
</template> </template>
@ -28,7 +39,7 @@ export default class DiscussionListItem extends Vue {
get htmlTextEllipsis() { get htmlTextEllipsis() {
const element = document.createElement("div"); const element = document.createElement("div");
if (this.discussion.lastComment) { if (this.discussion.lastComment && this.discussion.lastComment.text) {
element.innerHTML = this.discussion.lastComment.text element.innerHTML = this.discussion.lastComment.text
.replace(/<br\s*\/?>/gi, " ") .replace(/<br\s*\/?>/gi, " ")
.replace(/<p>/gi, " "); .replace(/<p>/gi, " ");
@ -53,11 +64,17 @@ export default class DiscussionListItem extends Vue {
.title-info-wrapper { .title-info-wrapper {
flex: 2; flex: 2;
.title-and-date {
display: flex;
align-items: center;
.discussion-minimalist-title { .discussion-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
flex: 1;
}
} }
div.has-text-grey { div.has-text-grey {

View file

@ -446,6 +446,7 @@ export default class EditorComponent extends Vue {
/** We use this to programatically insert an actor mention when creating a reply to comment */ /** We use this to programatically insert an actor mention when creating a reply to comment */
replyToComment(comment: IComment) { replyToComment(comment: IComment) {
if (!comment.actor) return;
const actorModel = new Actor(comment.actor); const actorModel = new Actor(comment.actor);
if (!this.editor) return; if (!this.editor) return;
this.editor.commands.mention({ this.editor.commands.mention({

View file

@ -2,13 +2,9 @@
<div class="media"> <div class="media">
<div class="media-content"> <div class="media-content">
<div class="content"> <div class="content">
<p> <i18n tag="p" path="You have been invited by {invitedBy} to the following group:">
{{ <b slot="invitedBy">{{ member.invitedBy.name }}</b>
$t("You have been invited by {invitedBy} to the following group:", { </i18n>
invitedBy: member.invitedBy.name,
})
}}
</p>
</div> </div>
<div class="media subfield"> <div class="media subfield">
<div class="media-left"> <div class="media-left">
@ -43,7 +39,7 @@
</b-button> </b-button>
</div> </div>
<div class="level-item"> <div class="level-item">
<b-button type="is-danger" @click="$emit('decline', member.id)"> <b-button type="is-danger" @click="$emit('reject', member.id)">
{{ $t("Decline") }} {{ $t("Decline") }}
</b-button> </b-button>
</div> </div>

View file

@ -0,0 +1,50 @@
<template>
<section v-if="invitations && invitations.length > 0">
<InvitationCard
v-for="member in invitations"
:key="member.id"
:member="member"
@accept="acceptInvitation"
@reject="rejectInvitation"
/>
</section>
</template>
<script lang="ts">
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { IMember } from "@/types/actor";
import { Component, Prop, Vue } from "vue-property-decorator";
import InvitationCard from "@/components/Group/InvitationCard.vue";
@Component({
components: {
InvitationCard,
},
})
export default class Invitations extends Vue {
@Prop({ required: true, type: Array }) invitations!: IMember;
async acceptInvitation(id: string) {
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
mutation: ACCEPT_INVITATION,
variables: {
id,
},
});
if (data) {
this.$emit("acceptInvitation", data.acceptInvitation);
}
}
async rejectInvitation(id: string) {
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
mutation: REJECT_INVITATION,
variables: {
id,
},
});
if (data) {
this.$emit("rejectInvitation", data.rejectInvitation);
}
}
}
</script>

View file

@ -1,7 +1,4 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { DISCUSSION_BASIC_FIELDS_FRAGMENT } from "@/graphql/discussion";
import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "@/graphql/resources";
import { POST_BASIC_FIELDS } from "./post";
export const FETCH_PERSON = gql` export const FETCH_PERSON = gql`
query($username: String!) { query($username: String!) {
@ -349,6 +346,13 @@ export const PERSON_MEMBERSHIPS = gql`
url url
} }
} }
invitedBy {
id
preferredUsername
name
}
insertedAt
updatedAt
} }
} }
} }
@ -424,209 +428,6 @@ export const REGISTER_PERSON = gql`
} }
`; `;
export const LIST_GROUPS = gql`
query {
groups {
elements {
id
url
name
domain
summary
preferredUsername
suspended
avatar {
url
}
banner {
url
}
organizedEvents {
elements {
uuid
title
beginsOn
}
total
}
}
total
}
}
`;
export const FETCH_GROUP = gql`
query($name: String!) {
group(preferredUsername: $name) {
id
url
name
domain
summary
preferredUsername
suspended
visibility
physicalAddress {
description
street
locality
postalCode
region
country
geom
type
id
originId
}
avatar {
url
}
banner {
url
}
organizedEvents {
elements {
id
uuid
title
beginsOn
}
total
}
discussions {
total
elements {
...DiscussionBasicFields
}
}
posts {
total
elements {
...PostBasicFields
}
}
members {
elements {
role
actor {
id
name
domain
preferredUsername
avatar {
url
}
}
insertedAt
}
total
}
resources(page: 1, limit: 3) {
elements {
id
title
resourceUrl
summary
updatedAt
type
path
metadata {
...ResourceMetadataBasicFields
}
}
total
}
todoLists {
elements {
id
title
todos {
elements {
id
title
status
dueDate
assignedTo {
id
preferredUsername
}
}
total
}
}
total
}
}
}
${DISCUSSION_BASIC_FIELDS_FRAGMENT}
${POST_BASIC_FIELDS}
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;
export const CREATE_GROUP = gql`
mutation CreateGroup(
$creatorActorId: ID!
$preferredUsername: String!
$name: String!
$summary: String
$avatar: PictureInput
$banner: PictureInput
) {
createGroup(
creatorActorId: $creatorActorId
preferredUsername: $preferredUsername
name: $name
summary: $summary
banner: $banner
avatar: $avatar
) {
id
preferredUsername
name
summary
avatar {
url
}
banner {
url
}
}
}
`;
export const UPDATE_GROUP = gql`
mutation UpdateGroup(
$id: ID!
$name: String
$summary: String
$avatar: PictureInput
$banner: PictureInput
$visibility: GroupVisibility
$physicalAddress: AddressInput
) {
updateGroup(
id: $id
name: $name
summary: $summary
banner: $banner
avatar: $avatar
visibility: $visibility
physicalAddress: $physicalAddress
) {
id
preferredUsername
name
summary
avatar {
url
}
banner {
url
}
}
}
`;
export const SUSPEND_PROFILE = gql` export const SUSPEND_PROFILE = gql`
mutation SuspendProfile($id: ID!) { mutation SuspendProfile($id: ID!) {
suspendProfile(id: $id) { suspendProfile(id: $id) {

View file

@ -86,8 +86,8 @@ export const CREATE_COMMENT_FROM_EVENT = gql`
`; `;
export const DELETE_COMMENT = gql` export const DELETE_COMMENT = gql`
mutation DeleteComment($commentId: ID!, $actorId: ID!) { mutation DeleteComment($commentId: ID!) {
deleteComment(commentId: $commentId, actorId: $actorId) { deleteComment(commentId: $commentId) {
id id
} }
} }
@ -99,4 +99,5 @@ export const UPDATE_COMMENT = gql`
...CommentFields ...CommentFields
} }
} }
${COMMENT_FIELDS_FRAGMENT}
`; `;

View file

@ -5,6 +5,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql`
id id
title title
slug slug
updatedAt
lastComment { lastComment {
id id
text text
@ -15,6 +16,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql`
url url
} }
} }
deletedAt
} }
} }
`; `;
@ -110,6 +112,7 @@ export const GET_DISCUSSION = gql`
} }
insertedAt insertedAt
updatedAt updatedAt
deletedAt
} }
} }
...DiscussionFields ...DiscussionFields

215
js/src/graphql/group.ts Normal file
View file

@ -0,0 +1,215 @@
import gql from "graphql-tag";
import { DISCUSSION_BASIC_FIELDS_FRAGMENT } from "./discussion";
import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "./resources";
import { POST_BASIC_FIELDS } from "./post";
export const LIST_GROUPS = gql`
query {
groups {
elements {
id
url
name
domain
summary
preferredUsername
suspended
avatar {
url
}
banner {
url
}
organizedEvents {
elements {
uuid
title
beginsOn
}
total
}
}
total
}
}
`;
export const FETCH_GROUP = gql`
query($name: String!) {
group(preferredUsername: $name) {
id
url
name
domain
summary
preferredUsername
suspended
visibility
physicalAddress {
description
street
locality
postalCode
region
country
geom
type
id
originId
}
avatar {
url
}
banner {
url
}
organizedEvents {
elements {
id
uuid
title
beginsOn
}
total
}
discussions {
total
elements {
...DiscussionBasicFields
}
}
posts {
total
elements {
...PostBasicFields
}
}
members {
elements {
role
actor {
id
name
domain
preferredUsername
avatar {
url
}
}
insertedAt
}
total
}
resources(page: 1, limit: 3) {
elements {
id
title
resourceUrl
summary
updatedAt
type
path
metadata {
...ResourceMetadataBasicFields
}
}
total
}
todoLists {
elements {
id
title
todos {
elements {
id
title
status
dueDate
assignedTo {
id
preferredUsername
}
}
total
}
}
total
}
}
}
${DISCUSSION_BASIC_FIELDS_FRAGMENT}
${POST_BASIC_FIELDS}
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;
export const CREATE_GROUP = gql`
mutation CreateGroup(
$creatorActorId: ID!
$preferredUsername: String!
$name: String!
$summary: String
$avatar: PictureInput
$banner: PictureInput
) {
createGroup(
creatorActorId: $creatorActorId
preferredUsername: $preferredUsername
name: $name
summary: $summary
banner: $banner
avatar: $avatar
) {
id
preferredUsername
name
summary
avatar {
url
}
banner {
url
}
}
}
`;
export const UPDATE_GROUP = gql`
mutation UpdateGroup(
$id: ID!
$name: String
$summary: String
$avatar: PictureInput
$banner: PictureInput
$visibility: GroupVisibility
$physicalAddress: AddressInput
) {
updateGroup(
id: $id
name: $name
summary: $summary
banner: $banner
avatar: $avatar
visibility: $visibility
physicalAddress: $physicalAddress
) {
id
preferredUsername
name
summary
avatar {
url
}
banner {
url
}
}
}
`;
export const LEAVE_GROUP = gql`
mutation LeaveGroup($groupId: ID!) {
leaveGroup(groupId: $groupId) {
id
}
}
`;

View file

@ -1,23 +1,52 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
export const INVITE_MEMBER = gql` export const MEMBER_FRAGMENT = gql`
mutation InviteMember($groupId: ID!, $targetActorUsername: String!) { fragment MemberFragment on Member {
inviteMember(groupId: $groupId, targetActorUsername: $targetActorUsername) {
id id
role role
parent { parent {
id id
preferredUsername
domain
name
avatar {
url
}
} }
actor { actor {
id id
preferredUsername
domain
name
avatar {
url
} }
} }
insertedAt
} }
`; `;
export const INVITE_MEMBER = gql`
mutation InviteMember($groupId: ID!, $targetActorUsername: String!) {
inviteMember(groupId: $groupId, targetActorUsername: $targetActorUsername) {
...MemberFragment
}
}
${MEMBER_FRAGMENT}
`;
export const ACCEPT_INVITATION = gql` export const ACCEPT_INVITATION = gql`
mutation AcceptInvitation($id: ID!) { mutation AcceptInvitation($id: ID!) {
acceptInvitation(id: $id) { acceptInvitation(id: $id) {
...MemberFragment
}
}
${MEMBER_FRAGMENT}
`;
export const REJECT_INVITATION = gql`
mutation RejectInvitation($id: ID!) {
rejectInvitation(id: $id) {
id id
} }
} }
@ -33,6 +62,7 @@ export const GROUP_MEMBERS = gql`
preferredUsername preferredUsername
members(page: $page, limit: $limit, roles: $roles) { members(page: $page, limit: $limit, roles: $roles) {
elements { elements {
id
role role
actor { actor {
id id
@ -50,3 +80,11 @@ export const GROUP_MEMBERS = gql`
} }
} }
`; `;
export const REMOVE_MEMBER = gql`
mutation RemoveMember($groupId: ID!, $memberId: ID!) {
removeMember(groupId: $groupId, memberId: $memberId) {
id
}
}
`;

View file

@ -756,5 +756,9 @@
"No ongoing todos": "No ongoing todos", "No ongoing todos": "No ongoing todos",
"No discussions yet": "No discussions yet", "No discussions yet": "No discussions yet",
"Add / Remove…": "Add / Remove…", "Add / Remove…": "Add / Remove…",
"No public posts": "No public posts" "No public posts": "No public posts",
"You have been removed from this group's members.": "You have been removed from this group's members.",
"Since you are a new member, private content can take a few minutes to appear.": "Since you are a new member, private content can take a few minutes to appear.",
"Leave group": "Leave group",
"Remove": "Remove"
} }

View file

@ -757,5 +757,9 @@
"No ongoing todos": "Pas de todos en cours", "No ongoing todos": "Pas de todos en cours",
"No discussions yet": "Pas encore de discussions", "No discussions yet": "Pas encore de discussions",
"Add / Remove…": "Ajouter / Supprimer…", "Add / Remove…": "Ajouter / Supprimer…",
"No public posts": "Pas de billets publics" "No public posts": "Pas de billets publics",
"You have been removed from this group's members.": "Vous avez été exclu des membres de ce groupe.",
"Since you are a new member, private content can take a few minutes to appear.": "Étant donné que vous êtes un·e nouveau·elle membre, le contenu privé peut mettre quelques minutes à arriver.",
"Leave group": "Quitter le groupe",
"Remove": "Exclure"
} }

View file

@ -33,6 +33,8 @@ export interface IMember {
parent: IGroup; parent: IGroup;
actor: IActor; actor: IActor;
invitedBy?: IPerson; invitedBy?: IPerson;
insertedAt: string;
updatedAt: string;
} }
export class Group extends Actor implements IGroup { export class Group extends Actor implements IGroup {

View file

@ -7,7 +7,7 @@ export interface IComment {
url?: string; url?: string;
text: string; text: string;
local: boolean; local: boolean;
actor: IActor; actor: IActor | null;
inReplyToComment?: IComment; inReplyToComment?: IComment;
originComment?: IComment; originComment?: IComment;
replies: IComment[]; replies: IComment[];
@ -56,7 +56,7 @@ export class CommentModel implements IComment {
this.text = hash.text; this.text = hash.text;
this.inReplyToComment = hash.inReplyToComment; this.inReplyToComment = hash.inReplyToComment;
this.originComment = hash.originComment; this.originComment = hash.originComment;
this.actor = new Actor(hash.actor); this.actor = hash.actor ? new Actor(hash.actor) : new Actor();
this.event = new EventModel(hash.event); this.event = new EventModel(hash.event);
this.replies = hash.replies; this.replies = hash.replies;
this.updatedAt = hash.updatedAt; this.updatedAt = hash.updatedAt;

View file

@ -19,7 +19,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IGroup, IPerson } from "@/types/actor"; import { IGroup, IPerson } from "@/types/actor";
import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "@/graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { FETCH_GROUP } from "@/graphql/group";
import { CREATE_DISCUSSION } from "@/graphql/discussion"; import { CREATE_DISCUSSION } from "@/graphql/discussion";
import RouteName from "../../router/name"; import RouteName from "../../router/name";

View file

@ -77,6 +77,8 @@
v-for="comment in discussion.comments.elements" v-for="comment in discussion.comments.elements"
:key="comment.id" :key="comment.id"
:comment="comment" :comment="comment"
@update-comment="updateComment"
@delete-comment="deleteComment"
/> />
<b-button <b-button
v-if="discussion.comments.elements.length < discussion.comments.total" v-if="discussion.comments.elements.length < discussion.comments.total"
@ -87,7 +89,12 @@
<b-field :label="$t('Text')"> <b-field :label="$t('Text')">
<editor v-model="newComment" /> <editor v-model="newComment" />
</b-field> </b-field>
<b-button native-type="submit" type="is-primary">{{ $t("Reply") }}</b-button> <b-button
native-type="submit"
:disabled="['<p></p>', ''].includes(newComment)"
type="is-primary"
>{{ $t("Reply") }}</b-button
>
</form> </form>
</section> </section>
</div> </div>
@ -107,6 +114,7 @@ import DiscussionComment from "@/components/Discussion/DiscussionComment.vue";
import { GraphQLError } from "graphql"; import { GraphQLError } from "graphql";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { IComment } from "../../types/comment.model"; import { IComment } from "../../types/comment.model";
import { DELETE_COMMENT, UPDATE_COMMENT } from "@/graphql/comment";
@Component({ @Component({
apollo: { apollo: {
@ -191,6 +199,8 @@ export default class discussion extends Vue {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
async reply() { async reply() {
if (this.newComment === "") return;
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: REPLY_TO_DISCUSSION, mutation: REPLY_TO_DISCUSSION,
variables: { variables: {
@ -223,6 +233,80 @@ export default class discussion extends Vue {
this.newComment = ""; this.newComment = "";
} }
async updateComment(comment: IComment) {
const { data } = await this.$apollo.mutate<{ deleteComment: IComment }>({
mutation: UPDATE_COMMENT,
variables: {
commentId: comment.id,
text: comment.text,
},
update: (store, { data }) => {
if (!data || !data.deleteComment) return;
const discussionData = store.readQuery<{
discussion: IDiscussion;
}>({
query: GET_DISCUSSION,
variables: {
slug: this.slug,
page: this.page,
},
});
if (!discussionData) return;
const { discussion } = discussionData;
const index = discussion.comments.elements.findIndex(
({ id }) => id === data.deleteComment.id
);
if (index > -1) {
discussion.comments.elements.splice(index, 1);
discussion.comments.total -= 1;
}
store.writeQuery({
query: GET_DISCUSSION,
variables: { slug: this.slug, page: this.page },
data: { discussion },
});
},
});
}
async deleteComment(comment: IComment) {
const { data } = await this.$apollo.mutate<{ deleteComment: IComment }>({
mutation: DELETE_COMMENT,
variables: {
commentId: comment.id,
},
update: (store, { data }) => {
if (!data || !data.deleteComment) return;
const discussionData = store.readQuery<{
discussion: IDiscussion;
}>({
query: GET_DISCUSSION,
variables: {
slug: this.slug,
page: this.page,
},
});
if (!discussionData) return;
const { discussion } = discussionData;
const index = discussion.comments.elements.findIndex(
({ id }) => id === data.deleteComment.id
);
if (index > -1) {
const updatedComment = discussion.comments.elements[index];
updatedComment.deletedAt = new Date();
updatedComment.actor = null;
updatedComment.text = "";
discussion.comments.elements.splice(index, 1, updatedComment);
}
store.writeQuery({
query: GET_DISCUSSION,
variables: { slug: this.slug, page: this.page },
data: { discussion },
});
},
});
}
async loadMoreComments() { async loadMoreComments() {
if (!this.hasMoreComments) return; if (!this.hasMoreComments) return;
this.page += 1; this.page += 1;

View file

@ -46,7 +46,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { FETCH_GROUP } from "@/graphql/actor"; import { FETCH_GROUP } from "@/graphql/group";
import { IGroup, usernameWithDomain } from "@/types/actor"; import { IGroup, usernameWithDomain } from "@/types/actor";
import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue"; import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@ -56,6 +56,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
group: { group: {
query: FETCH_GROUP, query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
name: this.preferredUsername, name: this.preferredUsername,

View file

@ -33,7 +33,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { Group, IPerson } from "@/types/actor"; import { Group, IPerson } from "@/types/actor";
import { CREATE_GROUP, CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { CREATE_GROUP } from "@/graphql/group";
import PictureUpload from "@/components/PictureUpload.vue"; import PictureUpload from "@/components/PictureUpload.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";

View file

@ -19,6 +19,17 @@
</li> </li>
</ul> </ul>
</nav> </nav>
<invitations
v-if="isCurrentActorAnInvitedGroupMember"
:invitations="[groupMember]"
@acceptInvitation="acceptInvitation"
/>
<b-message v-if="isCurrentActorARejectedGroupMember" type="is-danger">
{{ $t("You have been removed from this group's members.") }}
</b-message>
<b-message v-if="isCurrentActorAGroupMember && isCurrentActorARecentMember" type="is-info">
{{ $t("Since you are a new member, private content can take a few minutes to appear.") }}
</b-message>
<header class="block-container presentation"> <header class="block-container presentation">
<div class="block-column media"> <div class="block-column media">
<div class="media-left"> <div class="media-left">
@ -35,6 +46,7 @@
> >
<b-skeleton v-else :animated="true" /> <b-skeleton v-else :animated="true" />
<br /> <br />
<div class="buttons">
<router-link <router-link
v-if="isCurrentActorAGroupAdmin" v-if="isCurrentActorAGroupAdmin"
:to="{ :to="{
@ -44,6 +56,14 @@
class="button is-outlined" class="button is-outlined"
>{{ $t("Group settings") }}</router-link >{{ $t("Group settings") }}</router-link
> >
<b-button
type="is-danger"
v-if="isCurrentActorAGroupMember"
outlined
@click="leaveGroup"
>{{ $t("Leave group") }}</b-button
>
</div>
</div> </div>
</div> </div>
<div class="block-column members" v-if="isCurrentActorAGroupMember"> <div class="block-column members" v-if="isCurrentActorAGroupMember">
@ -56,7 +76,7 @@
role: member.role, role: member.role,
}) })
" "
v-for="member in group.members.elements" v-for="member in members"
:key="member.actor.id" :key="member.actor.id"
> >
<img <img
@ -71,6 +91,7 @@
<p> <p>
{{ $t("{count} team members", { count: group.members.total }) }} {{ $t("{count} team members", { count: group.members.total }) }}
<router-link <router-link
v-if="isCurrentActorAGroupAdmin"
:to="{ :to="{
name: RouteName.GROUP_MEMBERS_SETTINGS, name: RouteName.GROUP_MEMBERS_SETTINGS,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
@ -255,7 +276,8 @@
<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 EventCard from "@/components/Event/EventCard.vue"; import EventCard from "@/components/Event/EventCard.vue";
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { FETCH_GROUP, LEAVE_GROUP } from "@/graphql/group";
import { import {
IActor, IActor,
IGroup, IGroup,
@ -263,6 +285,7 @@ import {
usernameWithDomain, usernameWithDomain,
Group as GroupModel, Group as GroupModel,
MemberRole, MemberRole,
IMember,
} from "@/types/actor"; } from "@/types/actor";
import Subtitle from "@/components/Utils/Subtitle.vue"; import Subtitle from "@/components/Utils/Subtitle.vue";
import CompactTodo from "@/components/Todo/CompactTodo.vue"; import CompactTodo from "@/components/Todo/CompactTodo.vue";
@ -274,6 +297,8 @@ import FolderItem from "@/components/Resource/FolderItem.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { Address } from "@/types/address.model"; import { Address } from "@/types/address.model";
import GroupSection from "../../components/Group/GroupSection.vue"; import GroupSection from "../../components/Group/GroupSection.vue";
import Invitations from "@/components/Group/Invitations.vue";
import addMinutes from "date-fns/addMinutes";
@Component({ @Component({
apollo: { apollo: {
@ -308,6 +333,7 @@ import GroupSection from "../../components/Group/GroupSection.vue";
FolderItem, FolderItem,
ResourceItem, ResourceItem,
GroupSection, GroupSection,
Invitations,
"map-leaflet": () => import(/* webpackChunkName: "map" */ "../../components/Map.vue"), "map-leaflet": () => import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
}, },
metaInfo() { metaInfo() {
@ -348,6 +374,29 @@ export default class Group extends Vue {
} }
} }
async leaveGroup() {
const { data } = await this.$apollo.mutate({
mutation: LEAVE_GROUP,
variables: {
groupId: this.group.id,
},
});
return this.$router.push({ name: RouteName.MY_GROUPS });
}
acceptInvitation() {
if (this.groupMember) {
const index = this.person.memberships.elements.findIndex(
// @ts-ignore
({ id }: IMember) => id === this.groupMember.id
);
const member = this.groupMember;
member.role = MemberRole.MEMBER;
this.person.memberships.elements.splice(index, 1, member);
this.$apollo.queries.group.refetch();
}
}
get groupTitle() { get groupTitle() {
if (!this.group) return undefined; if (!this.group) return undefined;
return this.group.preferredUsername; return this.group.preferredUsername;
@ -358,15 +407,47 @@ export default class Group extends Vue {
return this.group.summary; return this.group.summary;
} }
get groupMember(): IMember | undefined {
if (!this.person || !this.person.id) return undefined;
return this.person.memberships.elements.find(({ parent: { id } }) => id === this.group.id);
}
get groupMemberships() { get groupMemberships() {
if (!this.person || !this.person.id) return undefined; if (!this.person || !this.person.id) return undefined;
return this.person.memberships.elements.map(({ parent: { id } }) => id); return this.person.memberships.elements
.filter(
(membership: IMember) =>
![MemberRole.REJECTED, MemberRole.NOT_APPROVED, MemberRole.INVITED].includes(
membership.role
)
)
.map(({ parent: { id } }) => id);
} }
get isCurrentActorAGroupMember(): boolean { get isCurrentActorAGroupMember(): boolean {
return this.groupMemberships != undefined && this.groupMemberships.includes(this.group.id); return this.groupMemberships != undefined && this.groupMemberships.includes(this.group.id);
} }
get isCurrentActorARejectedGroupMember(): boolean {
return (
this.person &&
this.person.memberships.elements
.filter((membership) => membership.role === MemberRole.REJECTED)
.map(({ parent: { id } }) => id)
.includes(this.group.id)
);
}
get isCurrentActorAnInvitedGroupMember(): boolean {
return (
this.person &&
this.person.memberships.elements
.filter((membership) => membership.role === MemberRole.INVITED)
.map(({ parent: { id } }) => id)
.includes(this.group.id)
);
}
get isCurrentActorAGroupAdmin(): boolean { get isCurrentActorAGroupAdmin(): boolean {
return ( return (
this.person && this.person &&
@ -376,6 +457,24 @@ export default class Group extends Vue {
); );
} }
/**
* New members, if on a different server, can take a while to refresh the group and fetch all private data
*/
get isCurrentActorARecentMember(): boolean {
return (
this.groupMember !== undefined &&
this.groupMember.role === MemberRole.MEMBER &&
addMinutes(new Date(`${this.groupMember.updatedAt}Z`), 10) > new Date()
);
}
get members(): IMember[] {
return this.group.members.elements.filter(
(member) =>
![MemberRole.INVITED, MemberRole.REJECTED, MemberRole.NOT_APPROVED].includes(member.role)
);
}
get physicalAddress(): Address | null { get physicalAddress(): Address | null {
if (!this.group.physicalAddress) return null; if (!this.group.physicalAddress) return null;
return new Address(this.group.physicalAddress); return new Address(this.group.physicalAddress);

View file

@ -18,7 +18,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { LIST_GROUPS } from "@/graphql/actor"; import { LIST_GROUPS } from "@/graphql/group";
import { Group, IGroup } from "@/types/actor"; import { Group, IGroup } from "@/types/actor";
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue"; import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<nav class="breadcrumb" aria-label="breadcrumbs"> <nav class="breadcrumb" aria-label="breadcrumbs">
<ul> <ul v-if="group">
<li> <li>
<router-link <router-link
:to="{ :to="{
@ -134,6 +134,14 @@
}} }}
</span> </span>
</b-table-column> </b-table-column>
<b-table-column field="actions" :label="$t('Actions')">
<b-button
v-if="props.row.role === MemberRole.MEMBER"
@click="removeMember(props.row.id)"
type="is-danger"
>{{ $t("Remove") }}</b-button
>
</b-table-column>
</template> </template>
<template slot="empty"> <template slot="empty">
<section class="section"> <section class="section">
@ -150,7 +158,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator"; import { Component, Vue, Watch } from "vue-property-decorator";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { INVITE_MEMBER, GROUP_MEMBERS } from "../../graphql/member"; import { INVITE_MEMBER, GROUP_MEMBERS, REMOVE_MEMBER } from "../../graphql/member";
import { IGroup, usernameWithDomain } from "../../types/actor"; import { IGroup, usernameWithDomain } from "../../types/actor";
import { IMember, MemberRole } from "../../types/actor/group.model"; import { IMember, MemberRole } from "../../types/actor/group.model";
@ -206,7 +214,32 @@ export default class GroupMembers extends Vue {
groupId: this.group.id, groupId: this.group.id,
targetActorUsername: this.newMemberUsername, targetActorUsername: this.newMemberUsername,
}, },
update: (store, { data }) => {
if (data == null) return;
const query = {
query: GROUP_MEMBERS,
variables: {
name: this.$route.params.preferredUsername,
page: 1,
limit: this.MEMBERS_PER_PAGE,
roles: this.roles,
},
};
const memberData: IMember = data.inviteMember;
const groupData = store.readQuery<{ group: IGroup }>(query);
if (!groupData) return;
const { group } = groupData;
const index = group.members.elements.findIndex((m) => m.actor.id === memberData.actor.id);
if (index === -1) {
group.members.elements.push(memberData);
group.members.total += 1;
} else {
group.members.elements.splice(index, 1, memberData);
}
store.writeQuery({ ...query, data: { group } });
},
}); });
this.newMemberUsername = "";
} }
@Watch("page") @Watch("page")
@ -235,5 +268,36 @@ export default class GroupMembers extends Vue {
}, },
}); });
} }
async removeMember(memberId: string) {
await this.$apollo.mutate<{ removeMember: IMember }>({
mutation: REMOVE_MEMBER,
variables: {
groupId: this.group.id,
memberId,
},
update: (store, { data }) => {
if (data == null) return;
const query = {
query: GROUP_MEMBERS,
variables: {
name: this.$route.params.preferredUsername,
page: 1,
limit: this.MEMBERS_PER_PAGE,
roles: this.roles,
},
};
const groupData = store.readQuery<{ group: IGroup }>(query);
if (!groupData) return;
const { group } = groupData;
const index = group.members.elements.findIndex((m) => m.id === memberId);
if (index !== -1) {
group.members.elements.splice(index, 1);
group.members.total -= 1;
store.writeQuery({ ...query, data: { group } });
}
},
});
}
} }
</script> </script>

View file

@ -100,7 +100,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { FETCH_GROUP, UPDATE_GROUP } from "../../graphql/actor"; import { FETCH_GROUP, UPDATE_GROUP } from "../../graphql/group";
import { IGroup, usernameWithDomain } from "../../types/actor"; import { IGroup, usernameWithDomain } from "../../types/actor";
import { Address, IAddress } from "../../types/address.model"; import { Address, IAddress } from "../../types/address.model";
import { IMember, Group } from "../../types/actor/group.model"; import { IMember, Group } from "../../types/actor/group.model";

View file

@ -3,14 +3,11 @@
<h1 class="title">{{ $t("My groups") }}</h1> <h1 class="title">{{ $t("My groups") }}</h1>
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t("Create group") }}</router-link> <router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t("Create group") }}</router-link>
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
<section v-if="invitations && invitations.length > 0"> <invitations
<InvitationCard :invitations="invitations"
v-for="member in invitations" @acceptInvitation="acceptInvitation"
:key="member.id" @rejectInvitation="rejectInvitation"
:member="member"
@accept="acceptInvitation"
/> />
</section>
<section v-if="memberships && memberships.length > 0"> <section v-if="memberships && memberships.length > 0">
<GroupMemberCard v-for="member in memberships" :key="member.id" :member="member" /> <GroupMemberCard v-for="member in memberships" :key="member.id" :member="member" />
</section> </section>
@ -24,16 +21,16 @@
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor"; import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue"; import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
import InvitationCard from "@/components/Group/InvitationCard.vue"; import Invitations from "@/components/Group/Invitations.vue";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { IGroup, IMember, MemberRole } from "@/types/actor"; import { IGroup, IMember, MemberRole, usernameWithDomain } from "@/types/actor";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { ACCEPT_INVITATION } from "../../graphql/member"; import { ACCEPT_INVITATION } from "../../graphql/member";
@Component({ @Component({
components: { components: {
GroupMemberCard, GroupMemberCard,
InvitationCard, Invitations,
}, },
apollo: { apollo: {
membershipsPages: { membershipsPages: {
@ -61,6 +58,23 @@ export default class MyEvents extends Vue {
RouteName = RouteName; RouteName = RouteName;
acceptInvitation(member: IMember) {
return this.$router.push({
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
});
}
rejectInvitation({ id: memberId }: { id: string }) {
const index = this.membershipsPages.elements.findIndex(
(membership) => membership.role === MemberRole.INVITED && membership.id === memberId
);
if (index > -1) {
this.membershipsPages.elements.splice(index, 1);
this.membershipsPages.total -= 1;
}
}
get invitations() { get invitations() {
if (!this.membershipsPages) return []; if (!this.membershipsPages) return [];
return this.membershipsPages.elements.filter( return this.membershipsPages.elements.filter(
@ -74,15 +88,6 @@ export default class MyEvents extends Vue {
(member: IMember) => member.role !== MemberRole.INVITED (member: IMember) => member.role !== MemberRole.INVITED
); );
} }
async acceptInvitation(id: string) {
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
mutation: ACCEPT_INVITATION,
variables: {
id,
},
});
}
} }
</script> </script>

View file

@ -26,7 +26,7 @@
import { Component, Vue, Watch } from "vue-property-decorator"; import { Component, Vue, Watch } from "vue-property-decorator";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { IGroup, IPerson } from "@/types/actor"; import { IGroup, IPerson } from "@/types/actor";
import { FETCH_GROUP } from "@/graphql/actor"; import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import SettingMenuSection from "../../components/Settings/SettingMenuSection.vue"; import SettingMenuSection from "../../components/Settings/SettingMenuSection.vue";
import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue"; import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue";

View file

@ -79,7 +79,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator"; import { Component, Vue, Prop } from "vue-property-decorator";
import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { FETCH_GROUP } from "@/graphql/group";
import { TAGS } from "../../graphql/tags"; import { TAGS } from "../../graphql/tags";
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
import { FETCH_POST, CREATE_POST, UPDATE_POST, DELETE_POST } from "../../graphql/post"; import { FETCH_POST, CREATE_POST, UPDATE_POST, DELETE_POST } from "../../graphql/post";

View file

@ -42,7 +42,8 @@
import { Component, Vue, Prop } from "vue-property-decorator"; import { Component, Vue, Prop } from "vue-property-decorator";
import Editor from "@/components/Editor.vue"; import Editor from "@/components/Editor.vue";
import { GraphQLError } from "graphql"; import { GraphQLError } from "graphql";
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS, FETCH_GROUP } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { FETCH_GROUP } from "@/graphql/group";
import { TAGS } from "../../graphql/tags"; import { TAGS } from "../../graphql/tags";
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
import { FETCH_POST, CREATE_POST } from "../../graphql/post"; import { FETCH_POST, CREATE_POST } from "../../graphql/post";

View file

@ -31,7 +31,7 @@
name: RouteName.RESOURCE_FOLDER, name: RouteName.RESOURCE_FOLDER,
params: { params: {
path: ResourceMixin.resourcePathArray(resource).slice(0, index + 1), path: ResourceMixin.resourcePathArray(resource).slice(0, index + 1),
preferredUsername: resource.actor.preferredUsername, preferredUsername: usernameWithDomain(resource.actor),
}, },
}" }"
>{{ pathFragment }}</router-link >{{ pathFragment }}</router-link

View file

@ -44,7 +44,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { FETCH_GROUP } from "@/graphql/actor"; import { FETCH_GROUP } from "@/graphql/group";
import { IGroup } from "@/types/actor"; import { IGroup } from "@/types/actor";
import { ITodoList } from "@/types/todos"; import { ITodoList } from "@/types/todos";
import { CREATE_TODO_LIST } from "@/graphql/todos"; import { CREATE_TODO_LIST } from "@/graphql/todos";

View file

@ -3,5 +3,14 @@ const path = require("path");
module.exports = { module.exports = {
runtimeCompiler: true, runtimeCompiler: true,
lintOnSave: true, lintOnSave: true,
filenameHashing: true,
outputDir: path.resolve(__dirname, "../priv/static"), outputDir: path.resolve(__dirname, "../priv/static"),
configureWebpack: {
optimization: {
splitChunks: {
minSize: 10000,
maxSize: 250000,
},
},
},
}; };

View file

@ -1060,9 +1060,9 @@
yargs "^8.0.2" yargs "^8.0.2"
"@mdi/font@^5.0.45": "@mdi/font@^5.0.45":
version "5.4.55" version "5.5.55"
resolved "https://registry.yarnpkg.com/@mdi/font/-/font-5.4.55.tgz#f34263882251ac23f37c1312988e1f10256dc74c" resolved "https://registry.yarnpkg.com/@mdi/font/-/font-5.5.55.tgz#7f83d640f0692651f5e59558da99975f42114123"
integrity sha512-M+Wdcs4nZ4/Kid949fcI0DsnvHtpE6pwk6Hv8YJZDp+Zne7ZtYdIN0z73cvcANkbyNnY3ncScULGMIceNd0xxQ== integrity sha512-xrVCXiRMz7ubB8mu6ehDhMADmGpLBsk3GWZccs39jWmhoTxatFnOvW8STJjqMGtePPNgGYYu/6m/AJVyMjBxnw==
"@mrmlnc/readdir-enhanced@^2.2.1": "@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1" version "2.2.1"
@ -1465,14 +1465,14 @@
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@3.8.0": "@typescript-eslint/experimental-utils@3.9.0":
version "3.8.0" version "3.9.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz#ac1f7c88322dcfb7635ece6f0441516dd951099a" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.0.tgz#3171d8ddba0bf02a8c2034188593630914fcf5ee"
integrity sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w== integrity sha512-/vSHUDYizSOhrOJdjYxPNGfb4a3ibO8zd4nUKo/QBFOmxosT3cVUV7KIg8Dwi6TXlr667G7YPqFK9+VSZOorNA==
dependencies: dependencies:
"@types/json-schema" "^7.0.3" "@types/json-schema" "^7.0.3"
"@typescript-eslint/types" "3.8.0" "@typescript-eslint/types" "3.9.0"
"@typescript-eslint/typescript-estree" "3.8.0" "@typescript-eslint/typescript-estree" "3.9.0"
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
@ -1487,20 +1487,20 @@
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
"@typescript-eslint/parser@^3.0.0": "@typescript-eslint/parser@^3.0.0":
version "3.8.0" version "3.9.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.8.0.tgz#8e1dcd404299bf79492409c81c415fa95a7c622b" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.9.0.tgz#344978a265d9a5c7c8f13e62c78172a4374dabea"
integrity sha512-u5vjOBaCsnMVQOvkKCXAmmOhyyMmFFf5dbkM3TIbg3MZ2pyv5peE4gj81UAbTHwTOXEwf7eCQTUMKrDl/+qGnA== integrity sha512-rDHOKb6uW2jZkHQniUQVZkixQrfsZGUCNWWbKWep4A5hGhN5dLHMUCNAWnC4tXRlHedXkTDptIpxs6e4Pz8UfA==
dependencies: dependencies:
"@types/eslint-visitor-keys" "^1.0.0" "@types/eslint-visitor-keys" "^1.0.0"
"@typescript-eslint/experimental-utils" "3.8.0" "@typescript-eslint/experimental-utils" "3.9.0"
"@typescript-eslint/types" "3.8.0" "@typescript-eslint/types" "3.9.0"
"@typescript-eslint/typescript-estree" "3.8.0" "@typescript-eslint/typescript-estree" "3.9.0"
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
"@typescript-eslint/types@3.8.0": "@typescript-eslint/types@3.9.0":
version "3.8.0" version "3.9.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.8.0.tgz#58581dd863f86e0cd23353d94362bb90b4bea796" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.9.0.tgz#be9d0aa451e1bf3ce99f2e6920659e5b2e6bfe18"
integrity sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q== integrity sha512-rb6LDr+dk9RVVXO/NJE8dT1pGlso3voNdEIN8ugm4CWM5w5GimbThCMiMl4da1t5u3YwPWEwOnKAULCZgBtBHg==
"@typescript-eslint/typescript-estree@2.34.0": "@typescript-eslint/typescript-estree@2.34.0":
version "2.34.0" version "2.34.0"
@ -1515,13 +1515,13 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@3.8.0": "@typescript-eslint/typescript-estree@3.9.0":
version "3.8.0" version "3.9.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz#0606d19f629f813dbdd5a34c7a1e895d6191cac6" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.0.tgz#c6abbb50fa0d715cab46fef67ca6378bf2eaca13"
integrity sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw== integrity sha512-N+158NKgN4rOmWVfvKOMoMFV5n8XxAliaKkArm/sOypzQ0bUL8MSnOEBW3VFIeffb/K5ce/cAV0yYhR7U4ALAA==
dependencies: dependencies:
"@typescript-eslint/types" "3.8.0" "@typescript-eslint/types" "3.9.0"
"@typescript-eslint/visitor-keys" "3.8.0" "@typescript-eslint/visitor-keys" "3.9.0"
debug "^4.1.1" debug "^4.1.1"
glob "^7.1.6" glob "^7.1.6"
is-glob "^4.0.1" is-glob "^4.0.1"
@ -1529,10 +1529,10 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@3.8.0": "@typescript-eslint/visitor-keys@3.9.0":
version "3.8.0" version "3.9.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz#ad35110249fb3fc30a36bfcbfeea93e710cfaab1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.0.tgz#44de8e1b1df67adaf3b94d6b60b80f8faebc8dd3"
integrity sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA== integrity sha512-O1qeoGqDbu0EZUC/MZ6F1WHTIzcBVhGqDj3LhTnj65WUA548RXVxUHbYhAW9bZWfb2rnX9QsbbP5nmeJ5Z4+ng==
dependencies: dependencies:
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
@ -1553,10 +1553,10 @@
lodash.kebabcase "^4.1.1" lodash.kebabcase "^4.1.1"
svg-tags "^1.0.0" svg-tags "^1.0.0"
"@vue/babel-preset-app@^4.5.2": "@vue/babel-preset-app@^4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.5.2.tgz#60642083ee7941cfdf2eea8a94a959d3379ab130" resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.5.3.tgz#2d8fdef342621f663311df2db6944b4fb8c1d57a"
integrity sha512-XOB4c9Ieo/GUK39bbVkZhbZ4YELrQJvUw+uuaLYs3CPaR3uvXdzfi082ZZRIVDyLc8zLdzpOql7dN1S6xQpDuw== integrity sha512-hncM46Afbel470p4BvCNtTiyKbcZJpfBu6NHPLeWHu9AWd8d7ObrhldaGhjgqIFSXUlKKE/W0QefYEBBEMZ1DQ==
dependencies: dependencies:
"@ant-design-vue/babel-plugin-jsx" "^1.0.0-0" "@ant-design-vue/babel-plugin-jsx" "^1.0.0-0"
"@babel/core" "^7.11.0" "@babel/core" "^7.11.0"
@ -1622,68 +1622,68 @@
"@vue/babel-plugin-transform-vue-jsx" "^1.1.2" "@vue/babel-plugin-transform-vue-jsx" "^1.1.2"
camelcase "^5.0.0" camelcase "^5.0.0"
"@vue/cli-overlay@^4.5.2": "@vue/cli-overlay@^4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-4.5.2.tgz#a8ef6a8aa93169ac511e161a387f26f42d45d4d5" resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-4.5.3.tgz#5937a232c613e5019868ce090b7c3e5d9e5ae473"
integrity sha512-YsmBkLG6oHeLPoEcOmtZmI97NJt3+MaHQSZzfET4lGYYoFWogqikxesbDmuoxYSylzEBOIfkp00ADjG8z4xTiw== integrity sha512-CHIiZEZlcb2HlZNIU6ZgNfTysNZWokQGzStfrCrQMXUXG0ffBRoi8K/kXNox2HxSfrT1Swi4NqREdPXefZJgNQ==
"@vue/cli-plugin-babel@~4.5.2": "@vue/cli-plugin-babel@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.5.2.tgz#ac6cc7a6059dc6b1ae3963f416841b49999b401d" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.5.3.tgz#80b2d49b754f57707a25064935cb4efac60fa70c"
integrity sha512-YeyKck3R69k/N8MV3E3MNcIS2zGX9U8fY12U9k3iuILQtHgfDl5BLRKE4vnB5ZXj1dBj3KUNtn9rqJxNbHUbUQ== integrity sha512-couvyfj37ZLb5D0huKHQ7vk9f0vOWPmtJTTwEUidmYvdQOZdGMGuVgf/u7XoEqJqPKG1LPDaAJy45pBm1tAe+Q==
dependencies: dependencies:
"@babel/core" "^7.11.0" "@babel/core" "^7.11.0"
"@vue/babel-preset-app" "^4.5.2" "@vue/babel-preset-app" "^4.5.3"
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
babel-loader "^8.1.0" babel-loader "^8.1.0"
cache-loader "^4.1.0" cache-loader "^4.1.0"
thread-loader "^2.1.3" thread-loader "^2.1.3"
webpack "^4.0.0" webpack "^4.0.0"
"@vue/cli-plugin-e2e-cypress@~4.5.2": "@vue/cli-plugin-e2e-cypress@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-e2e-cypress/-/cli-plugin-e2e-cypress-4.5.2.tgz#0ce811425a77d95cc97bf68638517fe11927ce2e" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-e2e-cypress/-/cli-plugin-e2e-cypress-4.5.3.tgz#f96b5e93cfe66891155295145dc39a5cc8c0cab8"
integrity sha512-Y/2tptTaxOU/bu0lHmilxZQXK+OCRUTuU+LZFlvwrzifaj68DR2Gn7E7f0XZg7jCwmleLtEEoUdA49964iBcPA== integrity sha512-hYTMA4e44L4EIbIRgpdXtDZJqKKOKdybiyyKumzD08M1jWIKSA/0rjHTyPItHg6cIuceifJcVy3JC1+9x5iNwg==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
cypress "^3.8.3" cypress "^3.8.3"
eslint-plugin-cypress "^2.10.3" eslint-plugin-cypress "^2.10.3"
"@vue/cli-plugin-eslint@~4.5.2": "@vue/cli-plugin-eslint@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.2.tgz#2aaf9dee417bef936e9910d41e6e6cd1e1b499cb" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.3.tgz#edac801bf05001e1a7fccd58b20c1a6cfe52701a"
integrity sha512-CTb3CaFYXLmAae0NTIV6qChmFyiMvur5YQEw4m6pcLFfOor8spaeyWFqA9t4K5qFyAmEtFqle3hgxJfKllu1fQ== integrity sha512-zSOLvLGI2gXdYTlkTOOgll1PbeXj7ka1mTKKHaFl/nNQhc76CiJ0Y/OzZlWqJOgk2lRlbL86KdioDh2FzZxFiw==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
eslint-loader "^2.2.1" eslint-loader "^2.2.1"
globby "^9.2.0" globby "^9.2.0"
inquirer "^7.1.0" inquirer "^7.1.0"
webpack "^4.0.0" webpack "^4.0.0"
yorkie "^2.0.0" yorkie "^2.0.0"
"@vue/cli-plugin-pwa@~4.5.2": "@vue/cli-plugin-pwa@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.2.tgz#1b96a6325ba95dbfcc0ca27a6da79266bcba3145" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.3.tgz#2ad9bd9f5f357e0f26b6316a815ff229636c2c52"
integrity sha512-ZsZ7iXDqIVP/49+HnSzW9boCIDvf5nulVVKzod+fG7aLLQqqztiPQhryn/jTU9Epgec4wz/uTtYfed8S0SNZGQ== integrity sha512-brc8SF7OP9jW+mAYOC0iuOEGNNTNQX5TXpnRWw3gm+XNknLpet6Aew7VX78jsm+j1k343QJPA16FroScPLoHPA==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
webpack "^4.0.0" webpack "^4.0.0"
workbox-webpack-plugin "^4.3.1" workbox-webpack-plugin "^4.3.1"
"@vue/cli-plugin-router@^4.5.2", "@vue/cli-plugin-router@~4.5.2": "@vue/cli-plugin-router@^4.5.3", "@vue/cli-plugin-router@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.5.2.tgz#6305edabf006aef7591925cbe215dfd18c277632" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.5.3.tgz#32c73d6b68b1d2b0d945dcc9be3ceb15e1d7fd52"
integrity sha512-5TWVE3sLOAPikfByi5sb+Foea8vm0PQJF2w8rpxN2XoUTi9MS6J2/dMCP7Up403Hvm2N7cSdY01cQz3geYwnFQ== integrity sha512-e0EqfwY4AGar1SX3rqD58QMoMYIxRD0AUauNiwSmuGjyA0Fr4Lfl1gBEPDCMZ5jIsO/4QNBateQGUy1S/GlxAw==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
"@vue/cli-plugin-typescript@~4.5.2": "@vue/cli-plugin-typescript@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-typescript/-/cli-plugin-typescript-4.5.2.tgz#eeb4cf0a03029d69ab878108acb86debdcf11e7e" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-typescript/-/cli-plugin-typescript-4.5.3.tgz#e42f68e6c4270e929d7a6cb55ba9490108832756"
integrity sha512-lZ60GEFlkOjK3JxDz0/iNAfcCouTOtusex+mXzaCQZS3ED9qE5b9zZk/AF/VjrzwgLXo53BrHR0CN36P/9M6DA== integrity sha512-fYfHfXf7vM86IEHyIVgSmqsZpphBeQXJwXLzm2O/R2Af+QkqKEHp0S6VP4TIynM4MPq+55Y++D0aKfzFS1B4pA==
dependencies: dependencies:
"@types/webpack-env" "^1.15.2" "@types/webpack-env" "^1.15.2"
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
cache-loader "^4.1.0" cache-loader "^4.1.0"
fork-ts-checker-webpack-plugin "^3.1.1" fork-ts-checker-webpack-plugin "^3.1.1"
globby "^9.2.0" globby "^9.2.0"
@ -1695,26 +1695,26 @@
optionalDependencies: optionalDependencies:
fork-ts-checker-webpack-plugin-v5 "npm:fork-ts-checker-webpack-plugin@^5.0.11" fork-ts-checker-webpack-plugin-v5 "npm:fork-ts-checker-webpack-plugin@^5.0.11"
"@vue/cli-plugin-unit-mocha@~4.5.2": "@vue/cli-plugin-unit-mocha@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-unit-mocha/-/cli-plugin-unit-mocha-4.5.2.tgz#d1cfbe6a54ffb93ede1cdfd404c85983d4d5e459" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-unit-mocha/-/cli-plugin-unit-mocha-4.5.3.tgz#1f86545dfdf53d9b1aa66eacb741bb8ee5cc75c1"
integrity sha512-zWWg18PDxJVf7OHBwqM+APbVMZHBfW8zIa81jQK+bHtFK3PUnrE7D3bXHFIkUHjqXhKwFTekdx7jpizYF4xgXA== integrity sha512-1AWl7jLOBkcTIjo7bqp9wLIgDpjAifIFw3fKINiDnm5Na2CKIBiGUCLo1CHwNz/Adz6zgBtuAxuZyCnTpQArog==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
jsdom "^15.2.1" jsdom "^15.2.1"
jsdom-global "^3.0.2" jsdom-global "^3.0.2"
mocha "^6.2.2" mocha "^6.2.2"
mochapack "^1.1.15" mochapack "^1.1.15"
"@vue/cli-plugin-vuex@^4.5.2": "@vue/cli-plugin-vuex@^4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.2.tgz#802db021bcc4afe0909492098aae8ea4b5e4b3cb" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.3.tgz#c0a566b0156e5bbbcc41e8cec195bc683aca7f5c"
integrity sha512-WAGx3WrhL78hpuwd1innSfflLuNkMiS3zz+sO8p1olhE9bLWdrIJ2jxHgrCCW6PFMwwY/h85GZNHUJvASPmvAQ== integrity sha512-23AAuaVbng6OUc5l7VHEGqCNiL1g1BsZL99X1rvKRttjDpdIYHtQAFsXjcTFitGpHRWoA9dgeujj/MkBPa1TcA==
"@vue/cli-service@~4.5.2": "@vue/cli-service@~4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-4.5.2.tgz#023128f5ae59f7b0032e30c0807ca1c361e68f1f" resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-4.5.3.tgz#4cf269a86d3d78c0568ed77908c18e9b970ad2ff"
integrity sha512-Oj4bYJriR0gWTEJEbC/C1sWBh/WBac9panI2M8d3KUA6ZI0KYNZZcD6OKaAq1/zd4DPntmwf2Zl4IuerBhCf4Q== integrity sha512-AufXUW+n8Wh9pqJu1v9Uh+6Sx6HdDrRopHMhUB/FrXhLFFPXRDo+s9zFC5QuJSt+roR0oBwmAp/x6KvBFQosIQ==
dependencies: dependencies:
"@intervolga/optimize-cssnano-plugin" "^1.0.5" "@intervolga/optimize-cssnano-plugin" "^1.0.5"
"@soda/friendly-errors-webpack-plugin" "^1.7.1" "@soda/friendly-errors-webpack-plugin" "^1.7.1"
@ -1722,10 +1722,10 @@
"@types/minimist" "^1.2.0" "@types/minimist" "^1.2.0"
"@types/webpack" "^4.0.0" "@types/webpack" "^4.0.0"
"@types/webpack-dev-server" "^3.11.0" "@types/webpack-dev-server" "^3.11.0"
"@vue/cli-overlay" "^4.5.2" "@vue/cli-overlay" "^4.5.3"
"@vue/cli-plugin-router" "^4.5.2" "@vue/cli-plugin-router" "^4.5.3"
"@vue/cli-plugin-vuex" "^4.5.2" "@vue/cli-plugin-vuex" "^4.5.3"
"@vue/cli-shared-utils" "^4.5.2" "@vue/cli-shared-utils" "^4.5.3"
"@vue/component-compiler-utils" "^3.1.2" "@vue/component-compiler-utils" "^3.1.2"
"@vue/preload-webpack-plugin" "^1.1.0" "@vue/preload-webpack-plugin" "^1.1.0"
"@vue/web-component-wrapper" "^1.2.0" "@vue/web-component-wrapper" "^1.2.0"
@ -1774,10 +1774,10 @@
optionalDependencies: optionalDependencies:
vue-loader-v16 "npm:vue-loader@^16.0.0-beta.3" vue-loader-v16 "npm:vue-loader@^16.0.0-beta.3"
"@vue/cli-shared-utils@^4.5.2": "@vue/cli-shared-utils@^4.5.3":
version "4.5.2" version "4.5.3"
resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.5.2.tgz#f46f04ad0476f9758d1ce8c1a306124dde704ed6" resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.5.3.tgz#23bcca7ffc3a09b2c50d8b8d9d031a9ff775b512"
integrity sha512-V/nNcYX+IRVG9o/9o3fxsj3aBPfpYExzjIHSusjWuSxUHpv0Vn4f9sIXG3N+FHgmcnQLqhmDNwlvRKIc+WFnLQ== integrity sha512-AjXSll67gpYWyjGOyHrwofLuxa7vL8KM6aUQCII+cHlFQey6oLS5bAWq9qcIM0P2ZyD+6i0fooNCihIuNrX4yg==
dependencies: dependencies:
"@hapi/joi" "^15.0.1" "@hapi/joi" "^15.0.1"
chalk "^2.4.2" chalk "^2.4.2"
@ -2632,14 +2632,15 @@ asap@~2.0.3:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
asn1.js@^4.0.0: asn1.js@^5.2.0:
version "4.10.1" version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
dependencies: dependencies:
bn.js "^4.0.0" bn.js "^4.0.0"
inherits "^2.0.1" inherits "^2.0.1"
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
asn1@~0.2.3: asn1@~0.2.3:
version "0.2.4" version "0.2.4"
@ -2754,9 +2755,9 @@ aws-sign2@~0.7.0:
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
aws4@^1.8.0: aws4@^1.8.0:
version "1.10.0" version "1.10.1"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428"
integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==
babel-code-frame@^6.22.0: babel-code-frame@^6.22.0:
version "6.26.0" version "6.26.0"
@ -3396,9 +3397,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
version "1.0.30001112" version "1.0.30001114"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001112.tgz#0fffc3b934ff56ff0548c37bc9dad7d882bcf672" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001114.tgz#2e88119afb332ead5eaa330e332e951b1c4bfea9"
integrity sha512-J05RTQlqsatidif/38aN3PGULCLrg8OYQOlJUKbeYVzC2mGZkZLIztwRlB3MtrfLmawUmjFlNJvy/uhwniIe1Q== integrity sha512-ml/zTsfNBM+T1+mjglWRPgVsu2L76GAaADKX5f4t0pbhttEp0WMawJsHDYlFkVZkoA+89uvBRrVrEE4oqenzXQ==
capture-stack-trace@^1.0.0: capture-stack-trace@^1.0.0:
version "1.0.1" version "1.0.1"
@ -5103,9 +5104,9 @@ duplexer3@^0.1.4:
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
duplexer@^0.1.1: duplexer@^0.1.1:
version "0.1.1" version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
duplexify@^3.4.2, duplexify@^3.6.0: duplexify@^3.4.2, duplexify@^3.6.0:
version "3.7.1" version "3.7.1"
@ -5158,9 +5159,9 @@ ejs@^2.6.1:
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.523: electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.523:
version "1.3.526" version "1.3.533"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.526.tgz#0e004899edf75afc172cce1b8189aac5dca646aa" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.533.tgz#d7e5ca4d57e9bc99af87efbe13e7be5dde729b0f"
integrity sha512-HiroW5ZbGwgT8kCnoEO8qnGjoTPzJxduvV/Vv/wH63eo2N6Zj3xT5fmmaSPAPUM05iN9/5fIEkIg3owTtV6QZg== integrity sha512-YqAL+NXOzjBnpY+dcOKDlZybJDCOzgsq4koW3fvyty/ldTmsb4QazZpOWmVvZ2m0t5jbBf7L0lIGU3BUipwG+A==
elegant-spinner@^1.0.1: elegant-spinner@^1.0.1:
version "1.0.1" version "1.0.1"
@ -8137,14 +8138,14 @@ js-base64@^2.1.8, js-base64@^2.1.9, js-base64@^2.3.2:
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
js-beautify@^1.6.12: js-beautify@^1.6.12:
version "1.11.0" version "1.12.0"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.12.0.tgz#6c7e6a47a6075a7c8e60c861e850440a5479d36e"
integrity sha512-a26B+Cx7USQGSWnz9YxgJNMmML/QG2nqIaL7VVYPCXbqiKz8PN0waSNvroMtvAK6tY7g/wPdNWGEP+JTNIBr6A== integrity sha512-hZCm93+sWHqrsB2ac38cPX4A9t6mfReq13ZUr/0dk6rCXNLIq0R4lu0EiuJc0Ip6RiWNtE0vECjXOhcy/jMt9Q==
dependencies: dependencies:
config-chain "^1.1.12" config-chain "^1.1.12"
editorconfig "^0.15.3" editorconfig "^0.15.3"
glob "^7.1.3" glob "^7.1.3"
mkdirp "~1.0.3" mkdirp "^1.0.4"
nopt "^4.0.3" nopt "^4.0.3"
js-message@1.0.5: js-message@1.0.5:
@ -8866,9 +8867,9 @@ lodash@4.17.5:
integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw== integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==
lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10: lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10:
version "4.17.19" version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
log-symbols@2.2.0, log-symbols@^2.2.0: log-symbols@2.2.0, log-symbols@^2.2.0:
version "2.2.0" version "2.2.0"
@ -9409,7 +9410,7 @@ mkdirp@0.5.4:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
mkdirp@^1.0.3, mkdirp@~1.0.3: mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@ -10300,13 +10301,12 @@ parent-module@^1.0.0:
callsites "^3.0.0" callsites "^3.0.0"
parse-asn1@^5.0.0, parse-asn1@^5.1.5: parse-asn1@^5.0.0, parse-asn1@^5.1.5:
version "5.1.5" version "5.1.6"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
dependencies: dependencies:
asn1.js "^4.0.0" asn1.js "^5.2.0"
browserify-aes "^1.0.0" browserify-aes "^1.0.0"
create-hash "^1.1.0"
evp_bytestokey "^1.0.0" evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3" pbkdf2 "^3.0.3"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
@ -11268,9 +11268,9 @@ prosemirror-model@1.11.0, prosemirror-model@1.9.1, prosemirror-model@^1.0.0, pro
orderedmap "^1.1.0" orderedmap "^1.1.0"
prosemirror-schema-list@^1.1.3: prosemirror-schema-list@^1.1.3:
version "1.1.3" version "1.1.4"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.3.tgz#c69fe19eefd0cc6461d820b459011d9f61a8e6b5" resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.4.tgz#471f9caf2d2bed93641d2e490434c0d2d4330df1"
integrity sha512-Km7YAZI21XYxBtMpYswuwBwTkDKoRz1mTsFyyA3/FFdbLxJrrBXIcd1+18dHqVJTn8HK4qYOocjQDfi+xVP9sQ== integrity sha512-pNTuZflacFOBlxrTcWSdWhjoB8BaucwfJVp/gJNxztOwaN3wQiC65axclXyplf6TKgXD/EkWfS/QAov3/Znadw==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
@ -11295,9 +11295,9 @@ prosemirror-tables@^1.1.1:
prosemirror-view "^1.13.3" prosemirror-view "^1.13.3"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.2.7: prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.2.7:
version "1.2.7" version "1.2.8"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.7.tgz#ba0e291a3cb43e6b633b779d93f53d01f5dad570" resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.8.tgz#4b86544fa43637fe381549fb7b019f4fb71fe65c"
integrity sha512-/107Lo2zeDgXuJBxb8s/clNu0Z2W8Gv3MKmkuSS/68Mcr7LBaUnN/Hj2g+GUxEJ7MpExCzFs65GrsNo2K9rxUQ== integrity sha512-hKqceqv9ZmMQXNQkhFjr0KFGPvkhygaWND+uIM0GxRpALrKfxP97SsgHTBs3OpJhDmh5N+mB4D/CksB291Eavg==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
@ -11306,7 +11306,7 @@ prosemirror-utils@^0.9.6:
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973" resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973"
integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA== integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==
prosemirror-view@1.15.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.15.2: prosemirror-view@1.15.2:
version "1.15.2" version "1.15.2"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.2.tgz#3f07881d11f18c033467591bbaec26b569bbc22c" resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.2.tgz#3f07881d11f18c033467591bbaec26b569bbc22c"
integrity sha512-0wftmMDVD8VXj2HZgv6Rg//+tgJC0lpV9LkYlCiAkDLKsf4yW3Ozs5td1ZXqsyoqvX0ga/k5g2EyLbqOMmC1+w== integrity sha512-0wftmMDVD8VXj2HZgv6Rg//+tgJC0lpV9LkYlCiAkDLKsf4yW3Ozs5td1ZXqsyoqvX0ga/k5g2EyLbqOMmC1+w==
@ -11315,6 +11315,15 @@ prosemirror-view@1.15.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prose
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0" prosemirror-transform "^1.1.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.15.2:
version "1.15.4"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.4.tgz#69a6217e3557dd1eb34a6d45caed1c3ee8e05b12"
integrity sha512-SzcszIrDJnQIS+f7WiS5KmQBfdYEhPqp/Hx9bKmXH7ZxrxRiBKPy1/9MoZzxjXUkm+5WHjX+N1fjAMXKoz/OQw==
dependencies:
prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proto-list@~1.2.1: proto-list@~1.2.1:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@ -12627,13 +12636,6 @@ serialize-javascript@^2.1.2:
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
serialize-javascript@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea"
integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==
dependencies:
randombytes "^2.1.0"
serialize-javascript@^4.0.0: serialize-javascript@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@ -13521,31 +13523,31 @@ term-size@^1.2.0:
execa "^0.7.0" execa "^0.7.0"
terser-webpack-plugin@^1.4.3: terser-webpack-plugin@^1.4.3:
version "1.4.4" version "1.4.5"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
integrity sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA== integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==
dependencies: dependencies:
cacache "^12.0.2" cacache "^12.0.2"
find-cache-dir "^2.1.0" find-cache-dir "^2.1.0"
is-wsl "^1.1.0" is-wsl "^1.1.0"
schema-utils "^1.0.0" schema-utils "^1.0.0"
serialize-javascript "^3.1.0" serialize-javascript "^4.0.0"
source-map "^0.6.1" source-map "^0.6.1"
terser "^4.1.2" terser "^4.1.2"
webpack-sources "^1.4.0" webpack-sources "^1.4.0"
worker-farm "^1.7.0" worker-farm "^1.7.0"
terser-webpack-plugin@^2.2.1, terser-webpack-plugin@^2.2.2, terser-webpack-plugin@^2.3.6: terser-webpack-plugin@^2.2.1, terser-webpack-plugin@^2.2.2, terser-webpack-plugin@^2.3.6:
version "2.3.7" version "2.3.8"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.7.tgz#4910ff5d1a872168cc7fa6cd3749e2b0d60a8a0b" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz#894764a19b0743f2f704e7c2a848c5283a696724"
integrity sha512-xzYyaHUNhzgaAdBsXxk2Yvo/x1NJdslUaussK3fdpBbvttm1iIwU+c26dj9UxJcwk2c5UWt5F55MUTIA8BE7Dg== integrity sha512-/fKw3R+hWyHfYx7Bv6oPqmk4HGQcrWLtV3X6ggvPuwPNHSnzvVV51z6OaaCOus4YLjutYGOz3pEpbhe6Up2s1w==
dependencies: dependencies:
cacache "^13.0.1" cacache "^13.0.1"
find-cache-dir "^3.3.1" find-cache-dir "^3.3.1"
jest-worker "^25.4.0" jest-worker "^25.4.0"
p-limit "^2.3.0" p-limit "^2.3.0"
schema-utils "^2.6.6" schema-utils "^2.6.6"
serialize-javascript "^3.1.0" serialize-javascript "^4.0.0"
source-map "^0.6.1" source-map "^0.6.1"
terser "^4.6.12" terser "^4.6.12"
webpack-sources "^1.4.3" webpack-sources "^1.4.3"
@ -13588,9 +13590,9 @@ thread-loader@^2.1.3:
neo-async "^2.6.0" neo-async "^2.6.0"
throttle-debounce@^2.1.0: throttle-debounce@^2.1.0:
version "2.2.1" version "2.3.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA== integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
throttleit@^1.0.0: throttleit@^1.0.0:
version "1.0.0" version "1.0.0"
@ -13649,10 +13651,10 @@ tippy.js@^6.2.3:
dependencies: dependencies:
"@popperjs/core" "^2.4.4" "@popperjs/core" "^2.4.4"
tiptap-commands@^1.14.4: tiptap-commands@^1.14.5:
version "1.14.4" version "1.14.5"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.14.4.tgz#b1ca5e29ae7b578597e72889227ebffcea853676" resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.14.5.tgz#af173513dd05f73c73780744f8d56ebb101a5f60"
integrity sha512-rshXFhrYJaKxLLKVDD0Zm4aQkvIq5v5NzLwRv9rrnd0Qh0YnJdfQQdw6w0RatdWuXusCmWyM5YdoF9D3hZecgw== integrity sha512-a1Sc3A7X7/aV5oHOcTCOsP07ln5vGCKoCcyeF3Hfr3GqA4uJvmpoHaWMXirYJFPSs1Sh+txNnZfck5Gi72IfFw==
dependencies: dependencies:
prosemirror-commands "^1.1.4" prosemirror-commands "^1.1.4"
prosemirror-inputrules "^1.1.2" prosemirror-inputrules "^1.1.2"
@ -13664,9 +13666,9 @@ tiptap-commands@^1.14.4:
tiptap-utils "^1.10.4" tiptap-utils "^1.10.4"
tiptap-extensions@^1.29.1: tiptap-extensions@^1.29.1:
version "1.32.1" version "1.32.4"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.32.1.tgz#6b9ce765fd2cc96930b8fb2f3e8dfeb2aaa3ea09" resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.32.4.tgz#335ac48bb35401dfb7f028df9f9e8456108312c8"
integrity sha512-gegR6Wp+NOViI/pMV6BCqjJ7VmnrUhj31Ws6a32sv2RnPI8ZJbqJcblMpiK5k1AXi2cgJ6dQyH8Tu8zwVZlu5w== integrity sha512-o3PtOQCD2lb/OooPpqQ69pst4mFAJqXid2kDdBiioRcv5dGTfPsaXfCNK7Kj6rR59vt+fzadFC/PLcSW0LkYaw==
dependencies: dependencies:
lowlight "^1.14.0" lowlight "^1.14.0"
prosemirror-collab "^1.2.2" prosemirror-collab "^1.2.2"
@ -13677,8 +13679,8 @@ tiptap-extensions@^1.29.1:
prosemirror-transform "^1.2.7" prosemirror-transform "^1.2.7"
prosemirror-utils "^0.9.6" prosemirror-utils "^0.9.6"
prosemirror-view "^1.15.2" prosemirror-view "^1.15.2"
tiptap "^1.29.4" tiptap "^1.29.5"
tiptap-commands "^1.14.4" tiptap-commands "^1.14.5"
tiptap-utils@^1.10.4: tiptap-utils@^1.10.4:
version "1.10.4" version "1.10.4"
@ -13690,10 +13692,10 @@ tiptap-utils@^1.10.4:
prosemirror-tables "^1.1.1" prosemirror-tables "^1.1.1"
prosemirror-utils "^0.9.6" prosemirror-utils "^0.9.6"
tiptap@^1.26.0, tiptap@^1.29.4: tiptap@^1.26.0, tiptap@^1.29.5:
version "1.29.4" version "1.29.5"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.29.4.tgz#6a2bb37f8d4a213d11e107fdf9312e56a1ed532c" resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.29.5.tgz#3f00bd98eea7d7518d2dc168f3e45eae1283db7c"
integrity sha512-brzl1hJQU0s7He4PJSI85e1TndJ4g/omt3mS4rNa/t1YnEU/NPpy2MMh8B8ZSFE23lP6FjaYQm42EfXC8n7B8w== integrity sha512-0/xj7mKpBqYQMxJTfdO/turZdMoW0MaSbiTrhv4UwGx91A3h8DJfpoLV6XStUGR9VVtBkyax3+yTjNRs2fXtew==
dependencies: dependencies:
prosemirror-commands "1.1.4" prosemirror-commands "1.1.4"
prosemirror-dropcursor "1.3.2" prosemirror-dropcursor "1.3.2"
@ -13703,7 +13705,7 @@ tiptap@^1.26.0, tiptap@^1.29.4:
prosemirror-model "1.11.0" prosemirror-model "1.11.0"
prosemirror-state "1.3.3" prosemirror-state "1.3.3"
prosemirror-view "1.15.2" prosemirror-view "1.15.2"
tiptap-commands "^1.14.4" tiptap-commands "^1.14.5"
tiptap-utils "^1.10.4" tiptap-utils "^1.10.4"
title-case@^2.1.0: title-case@^2.1.0:
@ -14566,9 +14568,9 @@ vue-i18n-extract@^1.0.2:
js-yaml "^3.13.1" js-yaml "^3.13.1"
vue-i18n@^8.14.0: vue-i18n@^8.14.0:
version "8.20.0" version "8.21.0"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.20.0.tgz#c81b01d6541182b28565316cafe881b65a3c0f1b" resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.21.0.tgz#526450525fdbb9c877685b5ba6cb9573b73d3940"
integrity sha512-ZiAOoeR4d/JtKpbjipx3I80ey7cYG1ki5gQ7HwzWm4YFio9brA15BEYHjalEoBaEfzF5OBEZP+Y2MvAaWnyXXg== integrity sha512-pKBq6Kg5hNacFHMFgPbpYsFlDIMRu4Ew/tpvTWns14CZoCxt7B3tmSNdrLruGMMivnJu1rhhRqsQqT6YwHkuQQ==
vue-inbrowser-compiler-utils@^4.27.0: vue-inbrowser-compiler-utils@^4.27.0:
version "4.27.0" version "4.27.0"
@ -14590,9 +14592,9 @@ vue-inbrowser-compiler@^4.27.0:
walkes "^0.2.1" walkes "^0.2.1"
"vue-loader-v16@npm:vue-loader@^16.0.0-beta.3": "vue-loader-v16@npm:vue-loader@^16.0.0-beta.3":
version "16.0.0-beta.4" version "16.0.0-beta.5"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.0.0-beta.4.tgz#1d9d7894f430992096727c4414bcf3b1ae8c1be9" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.0.0-beta.5.tgz#04edc889492b03a445e7ac66e9226a70175ca8a0"
integrity sha512-uh/+SIwoN+hny0+GqxdkTuEmt1NV4wb8etF5cKkB1YVMv29ck0byrmkt8IIYadQ3r/fiYsr2brGJqP+hytQwuw== integrity sha512-ciWfzNefqWlmzKznCWY9hl+fPP4KlQ0A9MtHbJ/8DpyY+dAM8gDrjufIdxwTgC4szE4EZC3A6ip/BbrqM84GqA==
dependencies: dependencies:
"@types/mini-css-extract-plugin" "^0.9.1" "@types/mini-css-extract-plugin" "^0.9.1"
chalk "^3.0.0" chalk "^3.0.0"
@ -14630,9 +14632,9 @@ vue-resize@^0.4.5:
integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg== integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
vue-router@^3.1.6: vue-router@^3.1.6:
version "3.4.2" version "3.4.3"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.2.tgz#541221d7ac467786c1c9381bcf36019d883b9cf8" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.3.tgz#fa93768616ee338aa174f160ac965167fa572ffa"
integrity sha512-n3Ok70hW0EpcJF4lcWIwSHAQbFTnIOLl/fhO8+oTs4jHNtBNsovcVvPZeTOyKEd8C3xF1Crft2ASuOiVT5K1mw== integrity sha512-BADg1mjGWX18Dpmy6bOGzGNnk7B/ZA0RxuA6qedY/YJwirMfKXIDzcccmHbQI0A6k5PzMdMloc0ElHfyOoX35A==
vue-scrollto@^2.17.1: vue-scrollto@^2.17.1:
version "2.18.2" version "2.18.2"

View file

@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub do
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Admin, Mailer} alias Mobilizon.Web.Email.{Admin, Group, Mailer}
require Logger require Logger
@ -225,7 +225,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
with {:ok, activity} <- create_activity(update_data, local), with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity} {:ok, activity, entity}
else else
err -> err ->
@ -240,10 +241,12 @@ defmodule Mobilizon.Federation.ActivityPub do
case type do case type do
:join -> reject_join(entity, additional) :join -> reject_join(entity, additional)
:follow -> reject_follow(entity, additional) :follow -> reject_follow(entity, additional)
:invite -> reject_invite(entity, additional)
end end
with {:ok, activity} <- create_activity(update_data, local), with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity} {:ok, activity, entity}
else else
err -> err ->
@ -376,8 +379,9 @@ defmodule Mobilizon.Federation.ActivityPub do
def leave(object, actor, local \\ true, additional \\ %{}) def leave(object, actor, local \\ true, additional \\ %{})
# TODO: If we want to use this for exclusion we need to have an extra field @doc """
# for the actor that excluded the participant Leave an event
"""
def leave( def leave(
%Event{id: event_id, url: event_url} = _event, %Event{id: event_id, url: event_url} = _event,
%Actor{id: actor_id, url: actor_url} = _actor, %Actor{id: actor_id, url: actor_url} = _actor,
@ -409,10 +413,63 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@doc """
Leave a group
"""
def leave(
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
%Actor{id: actor_id, url: actor_url},
local,
_additional
) do
with {:member, {:ok, %Member{id: member_id} = member}} <-
{:member, Actors.get_member(actor_id, group_id)},
{:is_only_admin, false} <-
{:is_only_admin, Actors.is_only_administrator?(member_id, group_id)},
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)},
leave_data <- %{
"to" => [group_members_url],
"cc" => [group_url],
"attributedTo" => group_url,
"type" => "Leave",
"actor" => actor_url,
"object" => group_url
},
{:ok, activity} <- create_activity(leave_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, member}
end
end
def remove(
%Member{} = member,
%Actor{type: :Group, url: group_url, members_url: group_members_url},
%Actor{url: moderator_url},
local,
_additional \\ %{}
) do
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
%Member{} = member <- Actors.get_member(member_id),
:ok <- Group.send_notification_to_removed_member(member),
remove_data <- %{
"to" => [group_members_url],
"type" => "Remove",
"actor" => moderator_url,
"object" => member.actor.url,
"origin" => group_url
},
{:ok, activity} <- create_activity(remove_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, member}
end
end
@spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) :: @spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) ::
{:ok, map(), Member.t()} | {:error, :member_not_found} {:ok, map(), Member.t()} | {:error, :member_not_found}
def invite( def invite(
%Actor{url: group_url, id: group_id} = group, %Actor{url: group_url, id: group_id, members_url: members_url} = group,
%Actor{url: actor_url, id: actor_id} = actor, %Actor{url: actor_url, id: actor_id} = actor,
%Actor{url: target_actor_url, id: target_actor_id} = _target_actor, %Actor{url: target_actor_url, id: target_actor_id} = _target_actor,
local \\ true, local \\ true,
@ -431,6 +488,7 @@ defmodule Mobilizon.Federation.ActivityPub do
}), }),
invite_data <- %{ invite_data <- %{
"type" => "Invite", "type" => "Invite",
"attributedTo" => group_url,
"actor" => actor_url, "actor" => actor_url,
"object" => group_url, "object" => group_url,
"target" => target_actor_url, "target" => target_actor_url,
@ -439,11 +497,12 @@ defmodule Mobilizon.Federation.ActivityPub do
{:ok, activity} <- {:ok, activity} <-
create_activity( create_activity(
invite_data invite_data
|> Map.merge(%{"to" => [target_actor_url], "cc" => [group_url]}) |> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]})
|> Map.merge(additional), |> Map.merge(additional),
local local
), ),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, member} {:ok, activity, member}
end end
end end
@ -806,9 +865,10 @@ defmodule Mobilizon.Federation.ActivityPub do
Actors.update_member(member, %{role: :member}), Actors.update_member(member, %{role: :member}),
accept_data <- %{ accept_data <- %{
"type" => "Accept", "type" => "Accept",
"actor" => actor_url, "attributedTo" => member.parent.url,
"to" => [inviter.url], "to" => [inviter.url, member.parent.members_url],
"cc" => [member.parent.url], "cc" => [member.parent.url],
"actor" => actor_url,
"object" => member_url, "object" => member_url,
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}" "id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
} do } do
@ -873,4 +933,26 @@ defmodule Mobilizon.Federation.ActivityPub do
err err
end end
end end
@spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
defp reject_invite(
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
_additional
) do
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
{:ok, %Member{url: member_url, id: member_id} = member} <-
Actors.delete_member(member),
accept_data <- %{
"type" => "Reject",
"actor" => actor_url,
"attributedTo" => member.parent.url,
"to" => [inviter.url, member.parent.members_url],
"cc" => [member.parent.url],
"object" => member_url,
"id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}"
} do
{:ok, member, accept_data}
end
end
end end

View file

@ -40,7 +40,8 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
"type" => "Create", "type" => "Create",
"to" => data["to"], "to" => data["to"],
"cc" => data["cc"], "cc" => data["cc"],
"actor" => data["attributedTo"] || data["actor"], "actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"],
"object" => data "object" => data
} do } do
Transmogrifier.handle_incoming(params) Transmogrifier.handle_incoming(params)
@ -61,7 +62,8 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
"type" => "Update", "type" => "Update",
"to" => data["to"], "to" => data["to"],
"cc" => data["cc"], "cc" => data["cc"],
"actor" => data["attributedTo"] || data["actor"], "actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"],
"object" => data "object" => data
} do } do
Transmogrifier.handle_incoming(params) Transmogrifier.handle_incoming(params)

View file

@ -17,7 +17,8 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
def maybe_preload(%Comment{url: url}), def maybe_preload(%Comment{url: url}),
do: {:ok, Discussions.get_comment_from_url_with_preload!(url)} do: {:ok, Discussions.get_comment_from_url_with_preload!(url)}
def maybe_preload(%Discussion{} = discussion), do: {:ok, discussion} def maybe_preload(%Discussion{id: discussion_id}),
do: {:ok, Discussions.get_discussion(discussion_id)}
def maybe_preload(%Resource{url: url}), def maybe_preload(%Resource{url: url}),
do: {:ok, Resources.get_resource_by_url_with_preloads(url)} do: {:ok, Resources.get_resource_by_url_with_preloads(url)}

View file

@ -21,6 +21,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Tombstone alias Mobilizon.Tombstone
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Group, Participation} alias Mobilizon.Web.Email.{Group, Participation}
require Logger require Logger
@ -313,7 +314,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:object_not_found, {:ok, activity, object}} <- {:object_not_found, {:ok, activity, object}} <-
{:object_not_found, {:object_not_found,
do_handle_incoming_reject_following(rejected_object, actor) || do_handle_incoming_reject_following(rejected_object, actor) ||
do_handle_incoming_reject_join(rejected_object, actor)} do do_handle_incoming_reject_join(rejected_object, actor) ||
do_handle_incoming_reject_invite(rejected_object, actor)} do
{:ok, activity, object} {:ok, activity, object}
else else
{:object_not_found, nil} -> {:object_not_found, nil} ->
@ -341,8 +343,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Actor{id: actor_id, suspended: false} = actor} <- {:ok, %Actor{id: actor_id, suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor_url), ActivityPub.get_or_fetch_actor_by_url(actor_url),
:ok <- Logger.debug("Fetching contained object"), :ok <- Logger.debug("Fetching contained object"),
{:ok, entity} <- {:ok, entity} <- process_announce_data(object, actor),
object |> Utils.get_url() |> fetch_object_optionnally_authenticated(actor),
:ok <- eventually_create_share(object, entity, actor_id) do :ok <- eventually_create_share(object, entity, actor_id) do
{:ok, nil, entity} {:ok, nil, entity}
else else
@ -396,6 +397,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
%{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} = %{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} =
update_data update_data
) do ) do
Logger.info("Handle incoming to update a note")
with actor <- Utils.get_actor(update_data), with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url, suspended: false}} <- {:ok, %Actor{url: actor_url, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(actor), ActivityPub.get_or_fetch_actor_by_url(actor),
@ -520,9 +523,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( def handle_incoming(%{"type" => "Leave", "object" => object, "actor" => actor} = data) do
%{"type" => "Leave", "object" => object, "actor" => actor, "id" => _id} = data
) do
with actor <- Utils.get_actor(data), with actor <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor), {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
object <- Utils.get_url(object), object <- Utils.get_url(object),
@ -565,6 +566,39 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming(
%{"type" => "Remove", "actor" => actor, "object" => object, "origin" => origin} = data
) do
Logger.info("Handle incoming to remove a member from a group")
with {:ok, %Actor{id: moderator_id} = moderator} <-
data |> Utils.get_actor() |> ActivityPub.get_or_fetch_actor_by_url(),
{:ok, %Actor{id: person_id}} <-
object |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url(),
{:ok, %Actor{type: :Group, id: group_id} = group} <-
origin |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url(),
{:is_admin, {:ok, %Member{role: role}}}
when role in [:moderator, :administrator, :creator] <-
{:is_admin, Actors.get_member(moderator_id, group_id)},
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
{:is_member, Actors.get_member(person_id, group_id)} do
ActivityPub.remove(member, group, moderator, false)
else
{:is_admin, {:ok, %Member{}}} ->
Logger.warn(
"Person #{inspect(actor)} is not an admin from #{inspect(origin)} and can't remove member #{
inspect(object)
}"
)
{:error, "Member already removed"}
{:is_member, {:ok, %Member{role: :rejected}}} ->
Logger.warn("Member #{inspect(object)} already removed from #{inspect(origin)}")
{:error, "Member already removed"}
end
end
# #
# # TODO # # TODO
# # Accept # # Accept
@ -761,6 +795,16 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do
with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <-
{:invite, get_member(invite_object)},
{:same_actor, true} <- {:same_actor, actor_rejecting.id === actor_id},
{:ok, activity, member} <-
ActivityPub.reject(:invite, member, false) do
{:ok, activity, member}
end
end
# If the object has been announced by a group let's use one of our members to fetch it # If the object has been announced by a group let's use one of our members to fetch it
@spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) :: @spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) ::
{:ok, struct()} | {:error, any()} {:ok, struct()} | {:error, any()}
@ -787,17 +831,24 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
:ok :ok
end end
# Comment initiates a whole discussion only if it has full title
@spec is_data_for_comment_or_discussion?(map()) :: boolean() @spec is_data_for_comment_or_discussion?(map()) :: boolean()
defp is_data_for_comment_or_discussion?(object_data) do defp is_data_for_comment_or_discussion?(object_data) do
(not Map.has_key?(object_data, :title) or is_data_a_discussion_initialization?(object_data) and
is_nil(object_data.title) or object_data.title == "") and
is_nil(object_data.discussion_id) is_nil(object_data.discussion_id)
end end
# Comment initiates a whole discussion only if it has full title
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
defp is_data_a_discussion_initialization?(object_data) do
not Map.has_key?(object_data, :title) or
is_nil(object_data.title) or object_data.title == ""
end
# Comment and conversations have different attributes for actor and groups # Comment and conversations have different attributes for actor and groups
defp transform_object_data_for_discussion(object_data) do defp transform_object_data_for_discussion(object_data) do
# Basic comment # Basic comment
if is_data_for_comment_or_discussion?(object_data) do if is_data_a_discussion_initialization?(object_data) do
object_data object_data
else else
# Conversation # Conversation
@ -880,4 +931,16 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, Convertible.model_to_as(object)} {:ok, Convertible.model_to_as(object)}
end end
end end
# Otherwise we need to fetch what's at the URL (this is possible only for objects, not activities)
defp process_announce_data(%{"id" => url}, %Actor{} = actor),
do: process_announce_data(url, actor)
defp process_announce_data(url, %Actor{} = actor) do
if Utils.are_same_origin?(url, Endpoint.url()) do
ActivityPub.fetch_object_from_url(url, force: false)
else
fetch_object_optionnally_authenticated(url, actor)
end
end
end end

View file

@ -36,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
@impl Entity @impl Entity
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any() @spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
def update(%Comment{} = old_comment, args, additional) do def update(%Comment{} = old_comment, args, additional) do
with args <- prepare_args_for_comment(args), with args <- prepare_args_for_comment_update(args),
{:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args), {:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"), {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
comment_as_data <- Convertible.model_to_as(new_comment), comment_as_data <- Convertible.model_to_as(new_comment),
@ -125,6 +125,20 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
end end
defp prepare_args_for_comment_update(args) do
with {text, mentions, tags} <-
APIUtils.make_content_html(
args |> Map.get(:text, "") |> String.trim(),
# Can't put additional tags on a comment
[],
"text/html"
),
tags <- ConverterUtils.fetch_tags(tags),
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions) do
Map.merge(args, %{text: text, mentions: mentions, tags: tags})
end
end
@spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil @spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
defp handle_event_for_comment(event_id) when not is_nil(event_id) do defp handle_event_for_comment(event_id) when not is_nil(event_id) do
case Events.get_event_with_preload(event_id) do case Events.get_event_with_preload(event_id) do

View file

@ -145,7 +145,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def maybe_relay_if_group_activity(_, _), do: :ok def maybe_relay_if_group_activity(_, _), do: :ok
defp do_maybe_relay_if_group_activity(object, attributed_to) when not is_nil(attributed_to) do defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to),
do: do_maybe_relay_if_group_activity(object, hd(attributed_to))
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_binary(attributed_to) do
id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}" id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}"
case Actors.get_local_group_by_url(attributed_to) do case Actors.get_local_group_by_url(attributed_to) do
@ -358,15 +361,14 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def make_announce_data( def make_announce_data(
%Actor{} = actor, %Actor{} = actor,
%{"id" => url, "type" => type, "actor" => object_actor_url} = _object, %{"actor" => object_actor_url} = object,
activity_id, activity_id,
public public
) ) do
when type in ["Note", "Event", "ResourceCollection", "Document", "Todo"] do
do_make_announce_data( do_make_announce_data(
actor, actor,
object_actor_url, object_actor_url,
url, object,
activity_id, activity_id,
public public
) )
@ -375,7 +377,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
defp do_make_announce_data( defp do_make_announce_data(
%Actor{type: actor_type} = actor, %Actor{type: actor_type} = actor,
object_actor_url, object_actor_url,
object_url, object,
activity_id, activity_id,
public public
) do ) do
@ -394,7 +396,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
data = %{ data = %{
"type" => "Announce", "type" => "Announce",
"actor" => actor.url, "actor" => actor.url,
"object" => object_url, "object" => object,
"to" => to, "to" => to,
"cc" => cc "cc" => cc
} }

View file

@ -68,10 +68,18 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
tags: tags, tags: tags,
mentions: mentions, mentions: mentions,
local: is_nil(actor_domain), local: is_nil(actor_domain),
visibility: if(Visibility.is_public?(object), do: :public, else: :private) visibility: if(Visibility.is_public?(object), do: :public, else: :private),
published_at: object["published"]
} }
maybe_fetch_parent_object(object, data) Logger.debug("Converted object before fetching parents")
Logger.debug(inspect(data))
data = maybe_fetch_parent_object(object, data)
Logger.debug("Converted object after fetching parents")
Logger.debug(inspect(data))
data
else else
{:ok, %Actor{suspended: true}} -> {:ok, %Actor{suspended: true}} ->
:error :error
@ -98,7 +106,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
comment.actor.url, comment.actor.url,
"uuid" => comment.uuid, "uuid" => comment.uuid,
"id" => comment.url, "id" => comment.url,
"tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags) "tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags),
"published" => comment.published_at |> DateTime.to_iso8601()
} }
object = object =

View file

@ -35,7 +35,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
"name" => post.title, "name" => post.title,
"content" => post.body, "content" => post.body,
"attributedTo" => creator_url, "attributedTo" => creator_url,
"published" => post.publish_at || post.inserted_at "published" => (post.publish_at || post.inserted_at) |> DateTime.to_iso8601()
} }
end end

View file

@ -36,7 +36,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Resource do
"name" => resource.title, "name" => resource.title,
"summary" => resource.summary, "summary" => resource.summary,
"context" => get_context(resource), "context" => get_context(resource),
"attributedTo" => actor_url "attributedTo" => actor_url,
"published" => resource.published_at |> DateTime.to_iso8601()
} }
case type do case type do
@ -65,7 +66,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Resource do
url: object["id"], url: object["id"],
actor_id: actor_id, actor_id: actor_id,
creator_id: creator_id, creator_id: creator_id,
parent_id: parent_id parent_id: parent_id,
published_at: object["published"]
} }
case type do case type do

View file

@ -37,7 +37,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
"id" => todo.url, "id" => todo.url,
"name" => todo.title, "name" => todo.title,
"status" => todo.status, "status" => todo.status,
"todoList" => todo_list_url "todoList" => todo_list_url,
"published" => todo.published_at |> DateTime.to_iso8601()
} }
end end
@ -58,7 +59,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
status: object["status"], status: object["status"],
url: object["id"], url: object["id"],
todo_list_id: todo_list_id, todo_list_id: todo_list_id,
creator_id: creator_id creator_id: creator_id,
published_at: object["published"]
} }
else else
{:todo_list, nil} -> {:todo_list, nil} ->

View file

@ -28,7 +28,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
"type" => "TodoList", "type" => "TodoList",
"actor" => group_url, "actor" => group_url,
"id" => todo_list.url, "id" => todo_list.url,
"name" => todo_list.title "name" => todo_list.title,
"published" => todo_list.published_at |> DateTime.to_iso8601()
} }
end end
@ -43,7 +44,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
%{ %{
title: object["name"], title: object["name"],
url: object["id"], url: object["id"],
actor_id: group_id actor_id: group_id,
published_at: object["published"]
} }
_ -> _ ->

View file

@ -51,7 +51,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
%CommentModel{actor_id: comment_actor_id} = comment <- %CommentModel{actor_id: comment_actor_id} = comment <-
Mobilizon.Discussions.get_comment(comment_id), Mobilizon.Discussions.get_comment_with_preload(comment_id),
true <- actor_id === comment_actor_id, true <- actor_id === comment_actor_id,
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do {:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
{:ok, comment} {:ok, comment}
@ -64,15 +64,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
def delete_comment( def delete_comment(
_parent, _parent,
%{actor_id: actor_id, comment_id: comment_id}, %{comment_id: comment_id},
%{ %{
context: %{ context: %{
current_user: %User{role: role} = user current_user: %User{role: role} = user
} }
} }
) do ) do
with {actor_id, ""} <- Integer.parse(actor_id), with {:actor, %Actor{id: actor_id} = actor} <- {:actor, Users.get_actor_for_user(user)},
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
%CommentModel{deleted_at: nil} = comment <- %CommentModel{deleted_at: nil} = comment <-
Discussions.get_comment_with_preload(comment_id) do Discussions.get_comment_with_preload(comment_id) do
cond do cond do

View file

@ -213,40 +213,25 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
""" """
def leave_group( def leave_group(
_parent, _parent,
%{group_id: group_id, actor_id: actor_id}, %{group_id: group_id},
%{ %{
context: %{ context: %{
current_user: user current_user: %User{} = user
} }
} }
) do ) do
with {actor_id, ""} <- Integer.parse(actor_id), with {:actor, %Actor{} = actor} <- {:actor, Users.get_actor_for_user(user)},
{group_id, ""} <- Integer.parse(group_id), {:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:ok, _activity, %Member{} = member} <- ActivityPub.leave(group, actor, true) do
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id), {:ok, member}
{:only_administrator, false} <-
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)},
{:ok, _} <-
Mobilizon.Actors.delete_member(member) do
{
:ok,
%{
parent: %{
id: group_id
},
actor: %{
id: actor_id
}
}
}
else else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :member_not_found} -> {:error, :member_not_found} ->
{:error, "Member not found"} {:error, "Member not found"}
{:only_administrator, true} -> {:group, nil} ->
{:error, "Group not found"}
{:is_only_admin, true} ->
{:error, "You can't leave this group because you are the only administrator"} {:error, "You can't leave this group because you are the only administrator"}
end end
end end
@ -278,32 +263,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
end end
# We check that the actor asking to leave the group is not it's only administrator
# We start by fetching the list of administrator or creators and if there's only one of them
# and that it's the actor requesting leaving the group we return true
@spec check_that_member_is_not_last_administrator(integer, integer) :: boolean
defp check_that_member_is_not_last_administrator(group_id, actor_id) do
case Actors.list_administrator_members_for_group(group_id) do
%Page{total: total} when total > 1 ->
true
%Page{
total: 1,
elements: [
%Member{
actor: %Actor{
id: member_actor_id
}
}
]
} ->
actor_id == member_actor_id
_ ->
false
end
end
defp restrict_fields_for_non_member_request(%Actor{} = group) do defp restrict_fields_for_non_member_request(%Actor{} = group) do
Map.merge( Map.merge(
group, group,

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
alias Mobilizon.{Actors, Users} alias Mobilizon.{Actors, Users}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Refresher
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -65,7 +66,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
{:target_actor_username, {:ok, %Actor{id: target_actor_id} = target_actor}} <- {:target_actor_username, {:ok, %Actor{id: target_actor_id} = target_actor}} <-
{:target_actor_username, {:target_actor_username,
ActivityPub.find_or_make_actor_from_nickname(target_actor_username)}, ActivityPub.find_or_make_actor_from_nickname(target_actor_username)},
{:error, :member_not_found} <- Actors.get_member(target_actor_id, group.id), true <- check_member_not_existant_or_rejected(target_actor_id, group.id),
{:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do {:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do
{:ok, member} {:ok, member}
else else
@ -91,7 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
def accept_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do def accept_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
%Member{actor: %Actor{id: member_actor_id}} = member <- Actors.get_member(member_id), %Member{actor: %Actor{id: member_actor_id} = actor} = member <-
Actors.get_member(member_id),
{:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id}, {:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id},
{:ok, _activity, %Member{} = member} <- {:ok, _activity, %Member{} = member} <-
ActivityPub.accept( ActivityPub.accept(
@ -100,7 +102,51 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
true true
) do ) do
# Launch an async task to refresh the group profile, fetch resources, discussions, members # Launch an async task to refresh the group profile, fetch resources, discussions, members
Refresher.fetch_group(member.parent.url, actor)
{:ok, member} {:ok, member}
end end
end end
def reject_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
%Member{actor: %Actor{id: member_actor_id}} = member <- Actors.get_member(member_id),
{:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id},
{:ok, _activity, %Member{} = member} <-
ActivityPub.reject(
:invite,
member,
true
) do
{:ok, member}
end
end
def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{
context: %{current_user: %User{} = user}
}) do
with %Actor{} = moderator <- Users.get_actor_for_user(user),
%Member{} = member <- Actors.get_member(member_id),
%Actor{type: :Group} = group <- Actors.get_actor(group_id),
{:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do
{:ok, member}
end
end
# Rejected members can be invited again
@spec check_member_not_existant_or_rejected(String.t() | integer, String.t() | integer()) ::
boolean()
defp check_member_not_existant_or_rejected(target_actor_id, group_id) do
case Actors.get_member(target_actor_id, group_id) do
{:ok, %Member{role: :rejected}} ->
true
{:error, :member_not_found} ->
true
err ->
require Logger
Logger.error(inspect(err))
false
end
end
end end

View file

@ -16,6 +16,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field(:role, :member_role_enum, description: "The role of this membership") field(:role, :member_role_enum, description: "The role of this membership")
field(:invited_by, :person, description: "Who invited this member") field(:invited_by, :person, description: "Who invited this member")
field(:inserted_at, :naive_datetime, description: "When was this member created") field(:inserted_at, :naive_datetime, description: "When was this member created")
field(:updated_at, :naive_datetime, description: "When was this member updated")
end end
enum :member_role_enum do enum :member_role_enum do
@ -28,12 +29,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
value(:rejected) value(:rejected)
end end
@desc "Represents a deleted member"
object :deleted_member do
field(:parent, :deleted_object)
field(:actor, :deleted_object)
end
object :paginated_member_list do object :paginated_member_list do
field(:elements, list_of(:member), description: "A list of members") field(:elements, list_of(:member), description: "A list of members")
field(:total, :integer, description: "The total number of elements in the list") field(:total, :integer, description: "The total number of elements in the list")
@ -49,9 +44,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
end end
@desc "Leave a group" @desc "Leave a group"
field :leave_group, :deleted_member do field :leave_group, :deleted_object do
arg(:group_id, non_null(:id)) arg(:group_id, non_null(:id))
arg(:actor_id, non_null(:id))
resolve(&Group.leave_group/3) resolve(&Group.leave_group/3)
end end
@ -70,5 +64,20 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
resolve(&Member.accept_invitation/3) resolve(&Member.accept_invitation/3)
end end
@desc "Reject an invitation to a group"
field :reject_invitation, :member do
arg(:id, non_null(:id))
resolve(&Member.reject_invitation/3)
end
@desc "Remove a member from a group"
field :remove_member, :member do
arg(:group_id, non_null(:id))
arg(:member_id, non_null(:id))
resolve(&Member.remove_member/3)
end
end end
end end

View file

@ -80,9 +80,9 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
resolve(&Comment.update_comment/3) resolve(&Comment.update_comment/3)
end end
@desc "Delete a single comment"
field :delete_comment, type: :comment do field :delete_comment, type: :comment do
arg(:comment_id, non_null(:id)) arg(:comment_id, non_null(:id))
arg(:actor_id, non_null(:id))
resolve(&Comment.delete_comment/3) resolve(&Comment.delete_comment/3)
end end

View file

@ -283,6 +283,7 @@ defmodule Mobilizon.Actors.Actor do
|> validate_required(@remote_actor_creation_required_attrs) |> validate_required(@remote_actor_creation_required_attrs)
|> common_changeset(attrs) |> common_changeset(attrs)
|> unique_username_validator() |> unique_username_validator()
|> validate_required(:domain)
|> validate_length(:summary, max: 5000) |> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100) |> validate_length(:preferred_username, max: 100)

View file

@ -558,7 +558,7 @@ defmodule Mobilizon.Actors do
@doc """ @doc """
Gets a single member. Gets a single member.
""" """
@spec get_member(integer | String.t()) :: Member.t() | nil @spec get_member(integer | String.t()) :: {:ok, Member.t()} | nil
def get_member(id) do def get_member(id) do
Member Member
|> Repo.get(id) |> Repo.get(id)
@ -642,7 +642,14 @@ defmodule Mobilizon.Actors do
with {:ok, %Member{} = member} <- with {:ok, %Member{} = member} <-
%Member{} %Member{}
|> Member.changeset(attrs) |> Member.changeset(attrs)
|> Repo.insert() do |> Repo.insert(
on_conflict: {:replace_all_except, [:id, :url, :actor_id, :parent_id]},
conflict_target: [:actor_id, :parent_id],
# See https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert/2-upserts,
# when doing an upsert with on_conflict, PG doesn't return whether it's an insert or upsert
# so we need to refresh the fields
returning: true
) do
{:ok, Repo.preload(member, [:actor, :parent, :invited_by])} {:ok, Repo.preload(member, [:actor, :parent, :invited_by])}
end end
end end
@ -739,6 +746,20 @@ defmodule Mobilizon.Actors do
|> Repo.all() |> Repo.all()
end end
@doc """
Returns whether the member is the last administrator for a group
"""
@spec is_only_administrator?(integer | String.t(), integer | String.t()) :: boolean()
def is_only_administrator?(member_id, group_id) do
Member
|> where(
[m],
m.parent_id == ^group_id and m.id != ^member_id and m.role in ^@administrator_roles
)
|> Repo.aggregate(:count)
|> (&(&1 == 0)).()
end
@doc """ @doc """
Gets a single bot. Gets a single bot.
Raises `Ecto.NoResultsError` if the bot does not exist. Raises `Ecto.NoResultsError` if the bot does not exist.
@ -1240,7 +1261,7 @@ defmodule Mobilizon.Actors do
from( from(
m in Member, m in Member,
where: m.actor_id == ^actor_id, where: m.actor_id == ^actor_id,
preload: [:parent] preload: [:parent, :invited_by]
) )
end end

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Discussions.Comment do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [maybe_add_published_at: 1]
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion} alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
@ -32,7 +33,7 @@ defmodule Mobilizon.Discussions.Comment do
# When deleting an event we only nihilify everything # When deleting an event we only nihilify everything
@required_attrs [:url] @required_attrs [:url]
@creation_required_attrs @required_attrs ++ [:text, :actor_id] @creation_required_attrs @required_attrs ++ [:text, :actor_id, :published_at]
@optional_attrs [ @optional_attrs [
:text, :text,
:actor_id, :actor_id,
@ -54,6 +55,7 @@ defmodule Mobilizon.Discussions.Comment do
field(:uuid, Ecto.UUID) field(:uuid, Ecto.UUID)
field(:total_replies, :integer, virtual: true, default: 0) field(:total_replies, :integer, virtual: true, default: 0)
field(:deleted_at, :utc_datetime) field(:deleted_at, :utc_datetime)
field(:published_at, :utc_datetime)
belongs_to(:actor, Actor, foreign_key: :actor_id) belongs_to(:actor, Actor, foreign_key: :actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
@ -98,7 +100,6 @@ defmodule Mobilizon.Discussions.Comment do
|> change() |> change()
|> put_change(:text, nil) |> put_change(:text, nil)
|> put_change(:actor_id, nil) |> put_change(:actor_id, nil)
|> put_change(:discussion_id, nil)
|> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second)) |> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second))
end end
@ -116,6 +117,7 @@ defmodule Mobilizon.Discussions.Comment do
defp common_changeset(%__MODULE__{} = comment, attrs) do defp common_changeset(%__MODULE__{} = comment, attrs) do
comment comment
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> maybe_add_published_at()
|> maybe_generate_uuid() |> maybe_generate_uuid()
|> maybe_generate_url() |> maybe_generate_url()
|> put_tags(attrs) |> put_tags(attrs)

View file

@ -294,6 +294,7 @@ defmodule Mobilizon.Discussions do
Discussion Discussion
|> where([c], c.actor_id == ^actor_id) |> where([c], c.actor_id == ^actor_id)
|> preload(^@discussion_preloads) |> preload(^@discussion_preloads)
|> order_by(desc: :updated_at)
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Resources.Resource do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Ecto.Changeset alias Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [ensure_url: 2] import Mobilizon.Storage.Ecto, only: [ensure_url: 2, maybe_add_published_at: 1]
import EctoEnum import EctoEnum
defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50) defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50)
@ -22,7 +22,8 @@ defmodule Mobilizon.Resources.Resource do
parent: __MODULE__, parent: __MODULE__,
actor: Actor.t(), actor: Actor.t(),
creator: Actor.t(), creator: Actor.t(),
local: boolean local: boolean,
published_at: DateTime.t()
} }
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@ -34,6 +35,7 @@ defmodule Mobilizon.Resources.Resource do
field(:type, TypeEnum) field(:type, TypeEnum)
field(:path, :string) field(:path, :string)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
embeds_one :metadata, Metadata, on_replace: :delete do embeds_one :metadata, Metadata, on_replace: :delete do
field(:type, :string) field(:type, :string)
@ -58,7 +60,7 @@ defmodule Mobilizon.Resources.Resource do
timestamps() timestamps()
end end
@required_attrs [:title, :url, :actor_id, :creator_id, :type, :path] @required_attrs [:title, :url, :actor_id, :creator_id, :type, :path, :published_at]
@optional_attrs [:summary, :parent_id, :resource_url, :local] @optional_attrs [:summary, :parent_id, :resource_url, :local]
@attrs @required_attrs ++ @optional_attrs @attrs @required_attrs ++ @optional_attrs
@metadata_attrs [ @metadata_attrs [
@ -82,6 +84,7 @@ defmodule Mobilizon.Resources.Resource do
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2) |> cast_embed(:metadata, with: &metadata_changeset/2)
|> ensure_url(:resource) |> ensure_url(:resource)
|> maybe_add_published_at()
|> validate_resource_or_folder() |> validate_resource_or_folder()
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> unique_constraint(:url, name: :resource_url_index) |> unique_constraint(:url, name: :resource_url_index)

View file

@ -22,7 +22,7 @@ defmodule Mobilizon.Resources do
def get_resources_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do def get_resources_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
Resource Resource
|> where(actor_id: ^group_id) |> where(actor_id: ^group_id)
|> order_by(desc: :updated_at) |> order_by(desc: :published_at)
|> preload([r], [:actor, :creator]) |> preload([r], [:actor, :creator])
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end

View file

@ -4,14 +4,15 @@ defmodule Mobilizon.Storage.Ecto do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
import Ecto.Changeset, only: [fetch_change: 2, put_change: 3] import Ecto.Changeset, only: [fetch_change: 2, put_change: 3, get_field: 2]
alias Ecto.{Changeset, Query}
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
@doc """ @doc """
Adds sort to the query. Adds sort to the query.
""" """
@spec sort(Ecto.Query.t(), atom, atom) :: Ecto.Query.t() @spec sort(Query.t(), atom, atom) :: Query.t()
def sort(query, sort, direction) do def sort(query, sort, direction) do
from(query, order_by: [{^direction, ^sort}]) from(query, order_by: [{^direction, ^sort}])
end end
@ -22,8 +23,8 @@ defmodule Mobilizon.Storage.Ecto do
If there's a blank URL that's because we're doing the first insert. If there's a blank URL that's because we're doing the first insert.
Most of the time just go with the given URL. Most of the time just go with the given URL.
""" """
@spec ensure_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t() @spec ensure_url(Changeset.t(), atom()) :: Changeset.t()
def ensure_url(%Ecto.Changeset{data: %{url: nil}} = changeset, route) do def ensure_url(%Changeset{data: %{url: nil}} = changeset, route) do
case fetch_change(changeset, :url) do case fetch_change(changeset, :url) do
{:ok, _url} -> {:ok, _url} ->
changeset changeset
@ -33,10 +34,10 @@ defmodule Mobilizon.Storage.Ecto do
end end
end end
def ensure_url(%Ecto.Changeset{} = changeset, _route), do: changeset def ensure_url(%Changeset{} = changeset, _route), do: changeset
@spec generate_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t() @spec generate_url(Changeset.t(), atom()) :: Changeset.t()
defp generate_url(%Ecto.Changeset{} = changeset, route) do defp generate_url(%Changeset{} = changeset, route) do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
changeset changeset
@ -46,4 +47,13 @@ defmodule Mobilizon.Storage.Ecto do
apply(Routes, String.to_existing_atom("page_url"), [Endpoint, route, uuid]) apply(Routes, String.to_existing_atom("page_url"), [Endpoint, route, uuid])
) )
end end
@spec maybe_add_published_at(Changeset.t()) :: Changeset.t()
def maybe_add_published_at(%Changeset{} = changeset) do
if is_nil(get_field(changeset, :published_at)) do
put_change(changeset, :published_at, DateTime.utc_now() |> DateTime.truncate(:second))
else
changeset
end
end
end end

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Todos.Todo do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [ensure_url: 2] import Mobilizon.Storage.Ecto, only: [ensure_url: 2, maybe_add_published_at: 1]
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Todos.TodoList alias Mobilizon.Todos.TodoList
@ -16,7 +16,8 @@ defmodule Mobilizon.Todos.Todo do
todo_list: TodoList.t(), todo_list: TodoList.t(),
creator: Actor.t(), creator: Actor.t(),
assigned_to: Actor.t(), assigned_to: Actor.t(),
local: boolean local: boolean,
published_at: DateTime.t()
} }
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@ -26,6 +27,7 @@ defmodule Mobilizon.Todos.Todo do
field(:url, :string) field(:url, :string)
field(:due_date, :utc_datetime) field(:due_date, :utc_datetime)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
belongs_to(:todo_list, TodoList, type: :binary_id) belongs_to(:todo_list, TodoList, type: :binary_id)
belongs_to(:creator, Actor) belongs_to(:creator, Actor)
belongs_to(:assigned_to, Actor) belongs_to(:assigned_to, Actor)
@ -33,7 +35,7 @@ defmodule Mobilizon.Todos.Todo do
timestamps() timestamps()
end end
@required_attrs [:title, :creator_id, :url, :todo_list_id] @required_attrs [:title, :creator_id, :url, :todo_list_id, :published_at]
@optional_attrs [:status, :due_date, :assigned_to_id, :local] @optional_attrs [:status, :due_date, :assigned_to_id, :local]
@attrs @required_attrs ++ @optional_attrs @attrs @required_attrs ++ @optional_attrs
@ -42,6 +44,7 @@ defmodule Mobilizon.Todos.Todo do
todo todo
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> ensure_url(:todo) |> ensure_url(:todo)
|> maybe_add_published_at()
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
end end
end end

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Todos.TodoList do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Mobilizon.Storage.Ecto, only: [ensure_url: 2] import Mobilizon.Storage.Ecto, only: [ensure_url: 2, maybe_add_published_at: 1]
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Todos.Todo alias Mobilizon.Todos.Todo
@ -13,7 +13,8 @@ defmodule Mobilizon.Todos.TodoList do
title: String.t(), title: String.t(),
todos: [Todo.t()], todos: [Todo.t()],
actor: Actor.t(), actor: Actor.t(),
local: boolean local: boolean,
published_at: DateTime.t()
} }
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@ -21,14 +22,14 @@ defmodule Mobilizon.Todos.TodoList do
field(:title, :string) field(:title, :string)
field(:url, :string) field(:url, :string)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
has_many(:todos, Todo) has_many(:todos, Todo)
timestamps() timestamps()
end end
@required_attrs [:title, :url, :actor_id] @required_attrs [:title, :url, :actor_id, :published_at]
@optional_attrs [:local] @optional_attrs [:local]
@attrs @required_attrs ++ @optional_attrs @attrs @required_attrs ++ @optional_attrs
@ -37,6 +38,7 @@ defmodule Mobilizon.Todos.TodoList do
todo_list todo_list
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> ensure_url(:todo_list) |> ensure_url(:todo_list)
|> maybe_add_published_at()
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
end end
end end

View file

@ -16,13 +16,30 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
@cache :activity_pub @cache :activity_pub
@doc """
Gets a actor by username and eventually domain.
"""
@spec get_actor_by_name(String.t()) ::
{:commit, Actor.t()} | {:ignore, nil}
def get_actor_by_name(name) do
Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name ->
case Actors.get_actor_by_name(name) do
%Actor{} = actor ->
{:commit, actor}
nil ->
{:ignore, nil}
end
end)
end
@doc """ @doc """
Gets a local actor by username. Gets a local actor by username.
""" """
@spec get_local_actor_by_name(String.t()) :: @spec get_local_actor_by_name(String.t()) ::
{:commit, Actor.t()} | {:ignore, nil} {:commit, Actor.t()} | {:ignore, nil}
def get_local_actor_by_name(name) do def get_local_actor_by_name(name) do
Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name -> Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name ->
case Actors.get_local_actor_by_name(name) do case Actors.get_local_actor_by_name(name) do
%Actor{} = actor -> %Actor{} = actor ->
{:commit, actor} {:commit, actor}

View file

@ -17,6 +17,7 @@ defmodule Mobilizon.Web.Cache do
Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username)) Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username))
end end
defdelegate get_actor_by_name(name), to: ActivityPub
defdelegate get_local_actor_by_name(name), to: ActivityPub defdelegate get_local_actor_by_name(name), to: ActivityPub
defdelegate get_public_event_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_public_event_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_comment_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_comment_by_uuid_with_preload(uuid), to: ActivityPub

View file

@ -27,7 +27,7 @@ defmodule Mobilizon.Web.PageController do
@spec actor(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() @spec actor(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def actor(conn, %{"name" => name}) do def actor(conn, %{"name" => name}) do
{status, actor} = Cache.get_local_actor_by_name(name) {status, actor} = Cache.get_actor_by_name(name)
render_or_error(conn, &ok_status?/3, status, :actor, actor) render_or_error(conn, &ok_status?/3, status, :actor, actor)
end end

View file

@ -15,10 +15,14 @@ defmodule Mobilizon.Web.Email.Group do
@doc """ @doc """
Send emails to local user Send emails to local user
""" """
@spec send_invite_to_user(Member.t(), String.t()) :: :ok
def send_invite_to_user(member, locale \\ "en")
def send_invite_to_user(%Member{actor: %Actor{user_id: nil}}, _locale), do: :ok
def send_invite_to_user( def send_invite_to_user(
%Member{actor: %Actor{user_id: user_id}, parent: %Actor{} = group, role: :invited} = %Member{actor: %Actor{user_id: user_id}, parent: %Actor{} = group, role: :invited} =
member, member,
locale \\ "en" locale
) do ) do
with %User{email: email} <- Users.get_user!(user_id) do with %User{email: email} <- Users.get_user!(user_id) do
Gettext.put_locale(locale) Gettext.put_locale(locale)
@ -43,5 +47,33 @@ defmodule Mobilizon.Web.Email.Group do
end end
end end
# Only send notification to local members
def send_notification_to_removed_member(%Member{actor: %Actor{user_id: nil}}), do: :ok
def send_notification_to_removed_member(%Member{
actor: %Actor{user_id: user_id},
parent: %Actor{} = group,
role: :rejected
}) do
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
Gettext.put_locale(locale)
subject =
gettext(
"You have been removed from group %{group}",
group: group.name
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:group, group)
|> assign(:subject, subject)
|> render(:group_removal)
|> Email.Mailer.deliver_later()
:ok
end
end
# TODO : def send_confirmation_to_inviter() # TODO : def send_confirmation_to_inviter()
end end

View file

@ -35,7 +35,7 @@
<tr> <tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" > <td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;"> <p style="margin: 0;">
<%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %> <%= gettext("<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}", group: @group.name, inviter: @inviter.name, link_start: "<a href=\"#{@group.url}\">", link_end: "</a>") |> raw %>
</p> </p>
</td> </td>
</tr> </tr>

View file

@ -1,5 +1,6 @@
<%= gettext "Come along!" %> <%= gettext "Come along!" %>
== ==
<%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %> <%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %>
<%= @group.url %>
<%= gettext "To accept this invitation, head over to your groups." %> <%= gettext "To accept this invitation, head over to your groups." %>
<%= page_url(Mobilizon.Web.Endpoint, :my_groups) %> <%= page_url(Mobilizon.Web.Endpoint, :my_groups) %>

View file

@ -0,0 +1,56 @@
<!-- HERO -->
<tr>
<td bgcolor="#474467" 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: #3A384C; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "So long, and thanks for the fish!" %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#E6E4F4" 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: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext("You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore.", group: @group.name, link_start: "<a href=\"#{@group.url}\">", link_end: "</a>") |> raw %>
</p>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "If you feel this is an error, you may contact the group's administrators so that they can add you back." %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View file

@ -0,0 +1,5 @@
<%= gettext "So long, and thanks for the fish!" %>
==
<%= gettext "You have been removed from group %{group}. You will not be able to access this group's private content anymore.", group: @group.name %>
<%= @group.url %>
<%= gettext "If you feel this is an error, you may contact the group's administrators so that they can add you back." %>

View file

@ -14,7 +14,7 @@ defmodule Mobilizon.Web.ActivityPub.ObjectView do
end, end,
"actor" => activity.actor, "actor" => activity.actor,
# Not sure if needed since this is used into outbox # Not sure if needed since this is used into outbox
"published" => Timex.now(), "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"to" => activity.recipients, "to" => activity.recipients,
"object" => "object" =>
case data["type"] do case data["type"] do

View file

@ -9,7 +9,7 @@
"bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"}, "bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"},
"bamboo_smtp": {:hex, :bamboo_smtp, "2.1.0", "4be58f3c51d9f7875dc169ae58a1d2f08e5b718bf3895f70d130548c0598f422", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "0aad00ef93d0e0c83a0e1ca6998fea070c8a720a990fbda13ce834136215ee49"}, "bamboo_smtp": {:hex, :bamboo_smtp, "2.1.0", "4be58f3c51d9f7875dc169ae58a1d2f08e5b718bf3895f70d130548c0598f422", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "0aad00ef93d0e0c83a0e1ca6998fea070c8a720a990fbda13ce834136215ee49"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "aef93694067a43697ae0531727e097754a9e992a1e7946296f5969d6dd9ac986"}, "cachex": {:hex, :cachex, "3.3.0", "6f2ebb8f27491fe39121bd207c78badc499214d76c695658b19d6079beeca5c2", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d90e5ee1dde14cef33f6b187af4335b88748b72b30c038969176cd4e6ccc31a1"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"cldr_utils": {:hex, :cldr_utils, "2.9.1", "be714403abe1a7abed5ee4f7dd3823a9067f96ab4b0613a454177b51ca204236", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6cba0a485f57feb773291ca1816469ddd887e22d73d9b12a1b207d82a67a4e71"}, "cldr_utils": {:hex, :cldr_utils, "2.9.1", "be714403abe1a7abed5ee4f7dd3823a9067f96ab4b0613a454177b51ca204236", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6cba0a485f57feb773291ca1816469ddd887e22d73d9b12a1b207d82a67a4e71"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},

View file

@ -369,8 +369,7 @@ msgstr[4] ""
msgstr[5] "" msgstr[5] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -398,7 +397,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1199,12 +1198,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -360,8 +360,7 @@ msgstr[1] ""
msgstr[2] "" msgstr[2] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -389,7 +388,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1175,12 +1174,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -368,8 +368,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -397,7 +396,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1192,15 +1191,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "No ho facis servir més que proves, sisplau" msgstr "No ho facis servir més que proves, sisplau"
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizon està en desenvolupament. Hi anirem afegint funcionalitats dins de " "Mobilizon està en desenvolupament. Hi anirem afegint funcionalitats dins de "
"les actualitzacions freqüents. Treurem la {b_start}versió 1.0 a la primera " "les actualitzacions freqüents. Treurem la {b_start}versió 1.0 a la primera "
"meitat del 2020%{b_end}." "meitat del 2020%{b_end}."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -360,8 +360,7 @@ msgstr[1] ""
msgstr[2] "" msgstr[2] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -389,7 +388,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1175,12 +1174,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -373,8 +373,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -402,7 +401,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1200,15 +1199,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizon befindet sich in der Entwicklung, wir werden neue Funktionen " "Mobilizon befindet sich in der Entwicklung, wir werden neue Funktionen "
"während regulären Updates hinzufügen, bis <b>Version 1 der Software " "während regulären Updates hinzufügen, bis <b>Version 1 der Software "
"in der ersten Hälfte von 2020 veröffentlicht wird</b>." "in der ersten Hälfte von 2020 veröffentlicht wird</b>."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -344,8 +344,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -373,7 +372,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1163,3 +1162,35 @@ msgstr ""
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -367,8 +367,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -396,7 +395,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1177,12 +1176,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Please do not use it in any real way" msgstr "Please do not use it in any real way"
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "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 "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."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -398,8 +398,7 @@ msgstr[0] "Sinulla on tänään yksi tapahtuma:"
msgstr[1] "Sinulla on tänään %{total} tapahtumaa:" msgstr[1] "Sinulla on tänään %{total} tapahtumaa:"
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "%{inviter} kutsui sinut ryhmään %{group}" msgstr "%{inviter} kutsui sinut ryhmään %{group}"
@ -427,7 +426,7 @@ msgstr "Näytä omat ryhmät"
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "Hyväksy kutsu siirtymällä omiin ryhmiisi." msgstr "Hyväksy kutsu siirtymällä omiin ryhmiisi."
@ -1324,15 +1323,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Älä käytä todellisiin tarkoituksiin." msgstr "Älä käytä todellisiin tarkoituksiin."
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizonin kehitystyö on vielä käynnissä, ja tälle sivulle lisätään " "Mobilizonin kehitystyö on vielä käynnissä, ja tälle sivulle lisätään "
"säännöllisesti uusia ominaisuuksia, kunnes ohjelman versio 1 julkaistaan " "säännöllisesti uusia ominaisuuksia, kunnes ohjelman versio 1 julkaistaan "
"vuoden 2020 alkupuoliskolla." "vuoden 2020 alkupuoliskolla."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "%{inviter} kutsui sinut ryhmään %{group}"
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

File diff suppressed because it is too large Load diff

View file

@ -360,8 +360,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -389,7 +388,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1171,12 +1170,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -356,8 +356,7 @@ msgid_plural "You have %{total} events today:"
msgstr[0] "" msgstr[0] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -385,7 +384,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1161,12 +1160,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -369,8 +369,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -398,7 +397,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1195,15 +1194,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizon is in ontwikkeling, we zullen regelmatig nieuwe functies toevoegen " "Mobilizon is in ontwikkeling, we zullen regelmatig nieuwe functies toevoegen "
"aan deze site via updates, tot <b>versie 1 van de software " "aan deze site via updates, tot <b>versie 1 van de software "
"beschikbaar is in de eerste helft van 2020</b>." "beschikbaar is in de eerste helft van 2020</b>."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -367,8 +367,7 @@ msgstr[0] "Avètz un eveniment uèi:"
msgstr[1] "Avètz %{total} eveniments uèi:" msgstr[1] "Avètz %{total} eveniments uèi:"
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -396,7 +395,7 @@ msgstr "Veire mos grops"
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "Per dire dacceptar aquesta invitacion, anatz als vòstres grops." msgstr "Per dire dacceptar aquesta invitacion, anatz als vòstres grops."
@ -1197,15 +1196,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Mercés de lutilizar pas dun biais real" msgstr "Mercés de lutilizar pas dun biais real"
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizon es en desvolopament, ajustarem de nòvas foncionalitats a aqueste " "Mobilizon es en desvolopament, ajustarem de nòvas foncionalitats a aqueste "
"site pendent de mesas a jorn regularas, fins a la publicacion de <b>" "site pendent de mesas a jorn regularas, fins a la publicacion de <b>"
"la version 1 del logicial al primièr semèstre 2020</b>." "la version 1 del logicial al primièr semèstre 2020</b>."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -375,8 +375,7 @@ msgstr[1] ""
msgstr[2] "" msgstr[2] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -404,7 +403,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1208,15 +1207,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Nie używaj go do żadnych rzeczywistych celów" msgstr "Nie używaj go do żadnych rzeczywistych celów"
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizon jest w fazie rozwoju. Nowe funkcje będą regularnie dodawane, do " "Mobilizon jest w fazie rozwoju. Nowe funkcje będą regularnie dodawane, do "
"wydania <b>pierwszej wersji oprogramowania w pierwszej połowie 2020 " "wydania <b>pierwszej wersji oprogramowania w pierwszej połowie 2020 "
"roku</b>." "roku</b>."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -358,8 +358,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -387,7 +386,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1168,12 +1167,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -406,8 +406,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -435,7 +434,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1287,15 +1286,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Por favor não utilize este serviço em nenhum caso real" msgstr "Por favor não utilize este serviço em nenhum caso real"
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
"Mobilizon está em desenvolvimento, Iremos adicionar novos recursos neste " "Mobilizon está em desenvolvimento, Iremos adicionar novos recursos neste "
"site durante as atualizações regulares, até a lançamento da <b>versão " "site durante as atualizações regulares, até a lançamento da <b>versão "
"1 do aplicativo no primeiro semestre de 2020</b>." "1 do aplicativo no primeiro semestre de 2020</b>."
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -367,8 +367,7 @@ msgstr[1] ""
msgstr[2] "" msgstr[2] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -396,7 +395,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1186,12 +1185,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -370,8 +370,7 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.text.eex:3
#: lib/web/templates/email/group_invite.text.eex:5
msgid "%{inviter} just invited you to join their group %{group}" msgid "%{inviter} just invited you to join their group %{group}"
msgstr "" msgstr ""
@ -399,7 +398,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:45 #: lib/web/templates/email/group_invite.html.eex:45
#: lib/web/templates/email/group_invite.text.eex:7 #: lib/web/templates/email/group_invite.text.eex:5
msgid "To accept this invitation, head over to your groups." msgid "To accept this invitation, head over to your groups."
msgstr "" msgstr ""
@ -1189,12 +1188,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message." msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:89 #: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>" msgid "<b>Please do not use it for real purposes.</b>"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/email.html.eex:91 #: lib/web/templates/email/email.html.eex:91
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1
msgid "So long, and thanks for the fish!"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:58
msgid "You have been removed from group %{group}"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/templates/email/group_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr ""

View file

@ -0,0 +1,65 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddPublishedAtToEntities do
use Ecto.Migration
alias Mobilizon.Storage.Repo
import Ecto.Query
def up do
alter table(:comments) do
add(:published_at, :utc_datetime)
end
flush()
copy_published_at_from_inserted_at("comments")
alter table(:resource) do
add(:published_at, :utc_datetime)
end
flush()
copy_published_at_from_inserted_at("resource")
alter table(:todo_lists) do
add(:published_at, :utc_datetime)
end
flush()
copy_published_at_from_inserted_at("todo_lists")
alter table(:todos) do
add(:published_at, :utc_datetime)
end
flush()
copy_published_at_from_inserted_at("todos")
end
def down do
alter table(:comments) do
remove(:published_at)
end
alter table(:resource) do
remove(:published_at)
end
alter table(:todo_lists) do
remove(:published_at)
end
alter table(:todos) do
remove(:published_at)
end
end
@spec copy_published_at_from_inserted_at(String.t()) :: any()
defp copy_published_at_from_inserted_at(table_name) do
from(c in table_name,
update: [set: [published_at: c.inserted_at]]
)
|> Repo.update_all([])
end
end

View file

@ -90,9 +90,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do
assert capture_log([level: :warn], fn -> assert capture_log([level: :warn], fn ->
assert :error == Transmogrifier.handle_incoming(reject_data) assert :error == Transmogrifier.handle_incoming(reject_data)
end) =~ end) =~
"Unable to process Reject activity \"http://mastodon.example.org/users/admin#rejects/follows/4\". Object \"#{ "Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected"
join_activity.data["id"]
}\" wasn't found."
# Organiser is not present since we use factories directly # Organiser is not present since we use factories directly
assert event.id assert event.id

View file

@ -39,7 +39,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UndoTest do
assert data["type"] == "Undo" assert data["type"] == "Undo"
assert data["object"]["type"] == "Announce" assert data["object"]["type"] == "Announce"
assert data["object"]["object"] == comment.url assert data["object"]["object"]["id"] == comment.url
assert data["object"]["id"] == assert data["object"]["id"] ==
"https://framapiaf.org/users/peertube/statuses/104584600044284729/activity" "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity"

View file

@ -33,7 +33,8 @@ defmodule Mobilizon.Federation.ActivityPub.UtilsTest do
"id" => Routes.page_url(Endpoint, :comment, reply.uuid), "id" => Routes.page_url(Endpoint, :comment, reply.uuid),
"inReplyTo" => comment.url, "inReplyTo" => comment.url,
"attributedTo" => reply.actor.url, "attributedTo" => reply.actor.url,
"mediaType" => "text/html" "mediaType" => "text/html",
"published" => reply.published_at |> DateTime.to_iso8601()
} == Converter.Comment.model_to_as(reply) } == Converter.Comment.model_to_as(reply)
end end

View file

@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
import Mobilizon.Factory import Mobilizon.Factory
alias Mobilizon.Actors.Member
alias Mobilizon.GraphQL.AbsintheHelpers alias Mobilizon.GraphQL.AbsintheHelpers
setup %{conn: conn} do setup %{conn: conn} do
@ -145,20 +146,15 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
actor: actor actor: actor
} do } do
group = insert(:group) group = insert(:group)
insert(:member, %{actor: actor, parent: group}) insert(:member, role: :administrator, parent: group)
%Member{id: member_id} = insert(:member, %{actor: actor, parent: group})
mutation = """ mutation = """
mutation { mutation {
leaveGroup( leaveGroup(
actor_id: #{actor.id},
group_id: #{group.id} group_id: #{group.id}
) { ) {
actor {
id id
},
parent {
id
}
} }
} }
""" """
@ -169,8 +165,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["leaveGroup"]["parent"]["id"] == to_string(group.id) assert json_response(res, 200)["data"]["leaveGroup"]["id"] == to_string(member_id)
assert json_response(res, 200)["data"]["leaveGroup"]["actor"]["id"] == to_string(actor.id)
end end
test "leave_group/3 should check if the member is the only administrator", %{ test "leave_group/3 should check if the member is the only administrator", %{
@ -185,15 +180,9 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
mutation = """ mutation = """
mutation { mutation {
leaveGroup( leaveGroup(
actor_id: #{actor.id},
group_id: #{group.id} group_id: #{group.id}
) { ) {
actor {
id id
},
parent {
id
}
} }
} }
""" """
@ -213,14 +202,11 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
mutation = """ mutation = """
mutation { mutation {
leaveGroup( leaveGroup(
actor_id: #{actor.id},
group_id: #{group.id} group_id: #{group.id}
) { ) {
actor {
id id
} }
} }
}
""" """
res = res =
@ -230,7 +216,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
assert hd(json_response(res, 200)["errors"])["message"] =~ "logged-in" assert hd(json_response(res, 200)["errors"])["message"] =~ "logged-in"
end end
test "leave_group/3 should check the actor is owned by the user", %{ test "leave_group/3 should check the group exists", %{
conn: conn, conn: conn,
user: user, user: user,
actor: actor actor: actor
@ -241,43 +227,11 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
mutation = """ mutation = """
mutation { mutation {
leaveGroup( leaveGroup(
actor_id: 1042,
group_id: #{group.id}
) {
actor {
id
}
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] =~ "not owned"
end
test "leave_group/3 should check the member exists", %{
conn: conn,
user: user,
actor: actor
} do
group = insert(:group)
insert(:member, %{actor: actor, parent: group})
mutation = """
mutation {
leaveGroup(
actor_id: #{actor.id},
group_id: 1042 group_id: 1042
) { ) {
actor {
id id
} }
} }
}
""" """
res = res =
@ -285,7 +239,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] =~ "Member not found" assert hd(json_response(res, 200)["errors"])["message"] =~ "Group not found"
end end
end end
@ -366,7 +320,6 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
test "invite_member/3 fails to invite a local actor to a group that invitor isn't in", %{ test "invite_member/3 fails to invite a local actor to a group that invitor isn't in", %{
conn: conn, conn: conn,
user: user, user: user,
actor: actor,
group: group, group: group,
target_actor: target_actor target_actor: target_actor
} do } do
@ -431,7 +384,6 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
test "invite_member/3 fails to invite a actor for a non-existing group", %{ test "invite_member/3 fails to invite a actor for a non-existing group", %{
conn: conn, conn: conn,
user: user, user: user,
actor: actor,
target_actor: target_actor target_actor: target_actor
} do } do
res = res =

View file

@ -649,18 +649,33 @@ defmodule Mobilizon.ActorsTest do
assert %Page{elements: [actor], total: 1} = Actors.list_members_for_group(group) assert %Page{elements: [actor], total: 1} = Actors.list_members_for_group(group)
end end
test "create_member/1 with valid data but same actors fails to create a member", %{ test "create_member/1 with valid data but same actors just updates the member", %{
actor: actor, actor: actor,
group: group group: group
} do } do
create_test_member(%{actor: actor, group: group}) %Member{id: member_id, url: member_url} = create_test_member(%{actor: actor, group: group})
valid_attrs = attrs =
@valid_attrs %{}
|> Map.put(:actor_id, actor.id) |> Map.put(:actor_id, actor.id)
|> Map.put(:parent_id, group.id) |> Map.put(:parent_id, group.id)
|> Map.put(:role, :member)
assert {:error, _member} = Actors.create_member(valid_attrs) assert {:ok,
%Member{
id: updated_member_id,
role: updated_member_role,
actor_id: actor_id,
parent_id: parent_id,
url: url
}} = Actors.create_member(attrs)
assert updated_member_role == :member
assert actor_id == actor.id
assert parent_id == group.id
assert url == member_url
assert updated_member_id == member_id
end end
test "create_member/1 with invalid data returns error changeset" do test "create_member/1 with invalid data returns error changeset" do

View file

@ -129,6 +129,7 @@ defmodule Mobilizon.Factory do
deleted_at: nil, deleted_at: nil,
tags: build_list(3, :tag), tags: build_list(3, :tag),
in_reply_to_comment: nil, in_reply_to_comment: nil,
published_at: DateTime.utc_now(),
url: Routes.page_url(Endpoint, :comment, uuid) url: Routes.page_url(Endpoint, :comment, uuid)
} }
end end
@ -285,7 +286,8 @@ defmodule Mobilizon.Factory do
title: sequence("todo list"), title: sequence("todo list"),
actor: build(:group), actor: build(:group),
id: uuid, id: uuid,
url: Routes.page_url(Endpoint, :todo_list, uuid) url: Routes.page_url(Endpoint, :todo_list, uuid),
published_at: DateTime.utc_now()
} }
end end
@ -300,7 +302,8 @@ defmodule Mobilizon.Factory do
due_date: Timex.shift(DateTime.utc_now(), hours: 2), due_date: Timex.shift(DateTime.utc_now(), hours: 2),
assigned_to: build(:actor), assigned_to: build(:actor),
url: Routes.page_url(Endpoint, :todo, uuid), url: Routes.page_url(Endpoint, :todo, uuid),
creator: build(:actor) creator: build(:actor),
published_at: DateTime.utc_now()
} }
end end
@ -317,6 +320,7 @@ defmodule Mobilizon.Factory do
creator: build(:actor), creator: build(:actor),
parent: nil, parent: nil,
url: Routes.page_url(Endpoint, :resource, uuid), url: Routes.page_url(Endpoint, :resource, uuid),
published_at: DateTime.utc_now(),
path: "/#{title}" path: "/#{title}"
} }
end end