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",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@vue/cli-plugin-babel": "~4.5.2",
"@vue/cli-plugin-e2e-cypress": "~4.5.2",
"@vue/cli-plugin-eslint": "~4.5.2",
"@vue/cli-plugin-pwa": "~4.5.2",
"@vue/cli-plugin-router": "~4.5.2",
"@vue/cli-plugin-typescript": "~4.5.2",
"@vue/cli-plugin-unit-mocha": "~4.5.2",
"@vue/cli-service": "~4.5.2",
"@vue/cli-plugin-babel": "~4.5.3",
"@vue/cli-plugin-e2e-cypress": "~4.5.3",
"@vue/cli-plugin-eslint": "~4.5.3",
"@vue/cli-plugin-pwa": "~4.5.3",
"@vue/cli-plugin-router": "~4.5.3",
"@vue/cli-plugin-typescript": "~4.5.3",
"@vue/cli-plugin-unit-mocha": "~4.5.3",
"@vue/cli-service": "~4.5.3",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",

View file

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

View file

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

View file

@ -8,26 +8,102 @@
</div>
<div class="body">
<div class="meta">
<div class="name">
<span>@{{ comment.actor.preferredUsername }}</span>
</div>
<span class="first-line name" v-if="!comment.deletedAt">
<strong>{{ comment.actor.name }}</strong>
<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">
<span :title="comment.insertedAt | formatDateTimeString">
{{ $timeAgo.format(comment.insertedAt, "twitter") || $t("Right now") }}</span
<span :title="comment.updatedAt | formatDateTimeString">
{{ $timeAgo.format(new Date(comment.updatedAt), "twitter") || $t("Right now") }}</span
>
</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>
</article>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
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 {
@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>
<style lang="scss" scoped>
@ -52,10 +128,20 @@ article.comment {
flex: 1 1 auto;
overflow: hidden;
strong {
display: block;
line-height: 1rem;
}
span {
color: #3c376e;
}
}
.icons {
display: inline;
cursor: pointer;
}
}
div.description-content {
@ -108,5 +194,11 @@ article.comment {
padding-top: 1rem;
flex: 0;
}
.edition {
.button {
margin-top: 0.75rem;
}
}
}
</style>

View file

@ -4,14 +4,25 @@
:to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }"
>
<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 />
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
<div class="title-info-wrapper">
<div class="title-and-date">
<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>
</router-link>
</template>
@ -28,7 +39,7 @@ export default class DiscussionListItem extends Vue {
get htmlTextEllipsis() {
const element = document.createElement("div");
if (this.discussion.lastComment) {
if (this.discussion.lastComment && this.discussion.lastComment.text) {
element.innerHTML = this.discussion.lastComment.text
.replace(/<br\s*\/?>/gi, " ")
.replace(/<p>/gi, " ");
@ -53,11 +64,17 @@ export default class DiscussionListItem extends Vue {
.title-info-wrapper {
flex: 2;
.title-and-date {
display: flex;
align-items: center;
.discussion-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-size: 1.25rem;
font-weight: 700;
flex: 1;
}
}
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 */
replyToComment(comment: IComment) {
if (!comment.actor) return;
const actorModel = new Actor(comment.actor);
if (!this.editor) return;
this.editor.commands.mention({

View file

@ -2,13 +2,9 @@
<div class="media">
<div class="media-content">
<div class="content">
<p>
{{
$t("You have been invited by {invitedBy} to the following group:", {
invitedBy: member.invitedBy.name,
})
}}
</p>
<i18n tag="p" path="You have been invited by {invitedBy} to the following group:">
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
</i18n>
</div>
<div class="media subfield">
<div class="media-left">
@ -43,7 +39,7 @@
</b-button>
</div>
<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") }}
</b-button>
</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 { 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`
query($username: String!) {
@ -349,6 +346,13 @@ export const PERSON_MEMBERSHIPS = gql`
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`
mutation SuspendProfile($id: ID!) {
suspendProfile(id: $id) {

View file

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

View file

@ -5,6 +5,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql`
id
title
slug
updatedAt
lastComment {
id
text
@ -15,6 +16,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql`
url
}
}
deletedAt
}
}
`;
@ -110,6 +112,7 @@ export const GET_DISCUSSION = gql`
}
insertedAt
updatedAt
deletedAt
}
}
...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";
export const INVITE_MEMBER = gql`
mutation InviteMember($groupId: ID!, $targetActorUsername: String!) {
inviteMember(groupId: $groupId, targetActorUsername: $targetActorUsername) {
export const MEMBER_FRAGMENT = gql`
fragment MemberFragment on Member {
id
role
parent {
id
preferredUsername
domain
name
avatar {
url
}
}
actor {
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`
mutation AcceptInvitation($id: ID!) {
acceptInvitation(id: $id) {
...MemberFragment
}
}
${MEMBER_FRAGMENT}
`;
export const REJECT_INVITATION = gql`
mutation RejectInvitation($id: ID!) {
rejectInvitation(id: $id) {
id
}
}
@ -33,6 +62,7 @@ export const GROUP_MEMBERS = gql`
preferredUsername
members(page: $page, limit: $limit, roles: $roles) {
elements {
id
role
actor {
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 discussions yet": "No discussions yet",
"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 discussions yet": "Pas encore de discussions",
"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;
actor: IActor;
invitedBy?: IPerson;
insertedAt: string;
updatedAt: string;
}
export class Group extends Actor implements IGroup {

View file

@ -7,7 +7,7 @@ export interface IComment {
url?: string;
text: string;
local: boolean;
actor: IActor;
actor: IActor | null;
inReplyToComment?: IComment;
originComment?: IComment;
replies: IComment[];
@ -56,7 +56,7 @@ export class CommentModel implements IComment {
this.text = hash.text;
this.inReplyToComment = hash.inReplyToComment;
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.replies = hash.replies;
this.updatedAt = hash.updatedAt;

View file

@ -19,7 +19,8 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
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 RouteName from "../../router/name";

View file

@ -77,6 +77,8 @@
v-for="comment in discussion.comments.elements"
:key="comment.id"
:comment="comment"
@update-comment="updateComment"
@delete-comment="deleteComment"
/>
<b-button
v-if="discussion.comments.elements.length < discussion.comments.total"
@ -87,7 +89,12 @@
<b-field :label="$t('Text')">
<editor v-model="newComment" />
</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>
</section>
</div>
@ -107,6 +114,7 @@ import DiscussionComment from "@/components/Discussion/DiscussionComment.vue";
import { GraphQLError } from "graphql";
import RouteName from "../../router/name";
import { IComment } from "../../types/comment.model";
import { DELETE_COMMENT, UPDATE_COMMENT } from "@/graphql/comment";
@Component({
apollo: {
@ -191,6 +199,8 @@ export default class discussion extends Vue {
usernameWithDomain = usernameWithDomain;
async reply() {
if (this.newComment === "") return;
await this.$apollo.mutate({
mutation: REPLY_TO_DISCUSSION,
variables: {
@ -223,6 +233,80 @@ export default class discussion extends Vue {
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() {
if (!this.hasMoreComments) return;
this.page += 1;

View file

@ -46,7 +46,7 @@
</template>
<script lang="ts">
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 DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
import RouteName from "../../router/name";
@ -56,6 +56,7 @@ import RouteName from "../../router/name";
apollo: {
group: {
query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
name: this.preferredUsername,

View file

@ -33,7 +33,8 @@
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
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 RouteName from "../../router/name";

View file

@ -19,6 +19,17 @@
</li>
</ul>
</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">
<div class="block-column media">
<div class="media-left">
@ -35,6 +46,7 @@
>
<b-skeleton v-else :animated="true" />
<br />
<div class="buttons">
<router-link
v-if="isCurrentActorAGroupAdmin"
:to="{
@ -44,6 +56,14 @@
class="button is-outlined"
>{{ $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 class="block-column members" v-if="isCurrentActorAGroupMember">
@ -56,7 +76,7 @@
role: member.role,
})
"
v-for="member in group.members.elements"
v-for="member in members"
:key="member.actor.id"
>
<img
@ -71,6 +91,7 @@
<p>
{{ $t("{count} team members", { count: group.members.total }) }}
<router-link
v-if="isCurrentActorAGroupAdmin"
:to="{
name: RouteName.GROUP_MEMBERS_SETTINGS,
params: { preferredUsername: usernameWithDomain(group) },
@ -255,7 +276,8 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
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 {
IActor,
IGroup,
@ -263,6 +285,7 @@ import {
usernameWithDomain,
Group as GroupModel,
MemberRole,
IMember,
} from "@/types/actor";
import Subtitle from "@/components/Utils/Subtitle.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 { Address } from "@/types/address.model";
import GroupSection from "../../components/Group/GroupSection.vue";
import Invitations from "@/components/Group/Invitations.vue";
import addMinutes from "date-fns/addMinutes";
@Component({
apollo: {
@ -308,6 +333,7 @@ import GroupSection from "../../components/Group/GroupSection.vue";
FolderItem,
ResourceItem,
GroupSection,
Invitations,
"map-leaflet": () => import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
},
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() {
if (!this.group) return undefined;
return this.group.preferredUsername;
@ -358,15 +407,47 @@ export default class Group extends Vue {
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() {
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 {
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 {
return (
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 {
if (!this.group.physicalAddress) return null;
return new Address(this.group.physicalAddress);

View file

@ -18,7 +18,7 @@
<script lang="ts">
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 GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
import RouteName from "../../router/name";

View file

@ -1,7 +1,7 @@
<template>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<ul v-if="group">
<li>
<router-link
:to="{
@ -134,6 +134,14 @@
}}
</span>
</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 slot="empty">
<section class="section">
@ -150,7 +158,7 @@
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
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 { IMember, MemberRole } from "../../types/actor/group.model";
@ -206,7 +214,32 @@ export default class GroupMembers extends Vue {
groupId: this.group.id,
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")
@ -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>

View file

@ -100,7 +100,7 @@
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
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 { Address, IAddress } from "../../types/address.model";
import { IMember, Group } from "../../types/actor/group.model";

View file

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

View file

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

View file

@ -79,7 +79,8 @@
</template>
<script lang="ts">
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 { CONFIG } from "../../graphql/config";
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 Editor from "@/components/Editor.vue";
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 { CONFIG } from "../../graphql/config";
import { FETCH_POST, CREATE_POST } from "../../graphql/post";

View file

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

View file

@ -44,7 +44,7 @@
</template>
<script lang="ts">
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 { ITodoList } from "@/types/todos";
import { CREATE_TODO_LIST } from "@/graphql/todos";

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,8 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
def maybe_preload(%Comment{url: 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}),
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.ActivityStream.{Converter, Convertible}
alias Mobilizon.Tombstone
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Group, Participation}
require Logger
@ -313,7 +314,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
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}
else
{:object_not_found, nil} ->
@ -341,8 +343,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Actor{id: actor_id, suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor_url),
:ok <- Logger.debug("Fetching contained object"),
{:ok, entity} <-
object |> Utils.get_url() |> fetch_object_optionnally_authenticated(actor),
{:ok, entity} <- process_announce_data(object, actor),
:ok <- eventually_create_share(object, entity, actor_id) do
{:ok, nil, entity}
else
@ -396,6 +397,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
%{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} =
update_data
) do
Logger.info("Handle incoming to update a note")
with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
@ -520,9 +523,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Leave", "object" => object, "actor" => actor, "id" => _id} = data
) do
def handle_incoming(%{"type" => "Leave", "object" => object, "actor" => actor} = data) do
with actor <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
object <- Utils.get_url(object),
@ -565,6 +566,39 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
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
# # Accept
@ -761,6 +795,16 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
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
@spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) ::
{:ok, struct()} | {:error, any()}
@ -787,17 +831,24 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
:ok
end
# Comment initiates a whole discussion only if it has full title
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
defp is_data_for_comment_or_discussion?(object_data) do
(not Map.has_key?(object_data, :title) or
is_nil(object_data.title) or object_data.title == "") and
is_data_a_discussion_initialization?(object_data) and
is_nil(object_data.discussion_id)
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
defp transform_object_data_for_discussion(object_data) do
# Basic comment
if is_data_for_comment_or_discussion?(object_data) do
if is_data_a_discussion_initialization?(object_data) do
object_data
else
# Conversation
@ -880,4 +931,16 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, Convertible.model_to_as(object)}
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

View file

@ -36,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
@impl Entity
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
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, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
comment_as_data <- Convertible.model_to_as(new_comment),
@ -125,6 +125,20 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
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
defp handle_event_for_comment(event_id) when not is_nil(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
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()}"
case Actors.get_local_group_by_url(attributed_to) do
@ -358,15 +361,14 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def make_announce_data(
%Actor{} = actor,
%{"id" => url, "type" => type, "actor" => object_actor_url} = _object,
%{"actor" => object_actor_url} = object,
activity_id,
public
)
when type in ["Note", "Event", "ResourceCollection", "Document", "Todo"] do
) do
do_make_announce_data(
actor,
object_actor_url,
url,
object,
activity_id,
public
)
@ -375,7 +377,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
defp do_make_announce_data(
%Actor{type: actor_type} = actor,
object_actor_url,
object_url,
object,
activity_id,
public
) do
@ -394,7 +396,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
data = %{
"type" => "Announce",
"actor" => actor.url,
"object" => object_url,
"object" => object,
"to" => to,
"cc" => cc
}

View file

@ -68,10 +68,18 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
tags: tags,
mentions: mentions,
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
{:ok, %Actor{suspended: true}} ->
:error
@ -98,7 +106,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
comment.actor.url,
"uuid" => comment.uuid,
"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 =

View file

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

View file

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

View file

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

View file

@ -28,7 +28,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
"type" => "TodoList",
"actor" => group_url,
"id" => todo_list.url,
"name" => todo_list.title
"name" => todo_list.title,
"published" => todo_list.published_at |> DateTime.to_iso8601()
}
end
@ -43,7 +44,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
%{
title: object["name"],
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
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
%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,
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
{:ok, comment}
@ -64,15 +64,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
def delete_comment(
_parent,
%{actor_id: actor_id, comment_id: comment_id},
%{comment_id: comment_id},
%{
context: %{
current_user: %User{role: role} = user
}
}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
with {:actor, %Actor{id: actor_id} = actor} <- {:actor, Users.get_actor_for_user(user)},
%CommentModel{deleted_at: nil} = comment <-
Discussions.get_comment_with_preload(comment_id) do
cond do

View file

@ -213,40 +213,25 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
"""
def leave_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{group_id: group_id},
%{
context: %{
current_user: user
current_user: %User{} = user
}
}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
{: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
}
}
}
with {:actor, %Actor{} = actor} <- {:actor, Users.get_actor_for_user(user)},
{:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
{:ok, _activity, %Member{} = member} <- ActivityPub.leave(group, actor, true) do
{:ok, member}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{: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"}
end
end
@ -278,32 +263,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:ok, %Page{total: 0, elements: []}}
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
Map.merge(
group,

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
alias Mobilizon.{Actors, Users}
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Refresher
alias Mobilizon.Storage.Page
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,
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, member}
else
@ -91,7 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member 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),
%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},
{:ok, _activity, %Member{} = member} <-
ActivityPub.accept(
@ -100,7 +102,51 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
true
) do
# Launch an async task to refresh the group profile, fetch resources, discussions, members
Refresher.fetch_group(member.parent.url, actor)
{:ok, member}
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

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

View file

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

View file

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

View file

@ -558,7 +558,7 @@ defmodule Mobilizon.Actors do
@doc """
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
Member
|> Repo.get(id)
@ -642,7 +642,14 @@ defmodule Mobilizon.Actors do
with {:ok, %Member{} = member} <-
%Member{}
|> 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])}
end
end
@ -739,6 +746,20 @@ defmodule Mobilizon.Actors do
|> Repo.all()
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 """
Gets a single bot.
Raises `Ecto.NoResultsError` if the bot does not exist.
@ -1240,7 +1261,7 @@ defmodule Mobilizon.Actors do
from(
m in Member,
where: m.actor_id == ^actor_id,
preload: [:parent]
preload: [:parent, :invited_by]
)
end

View file

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

View file

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

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Resources.Resource do
use Ecto.Schema
import 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
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__,
actor: Actor.t(),
creator: Actor.t(),
local: boolean
local: boolean,
published_at: DateTime.t()
}
@primary_key {:id, :binary_id, autogenerate: true}
@ -34,6 +35,7 @@ defmodule Mobilizon.Resources.Resource do
field(:type, TypeEnum)
field(:path, :string)
field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
embeds_one :metadata, Metadata, on_replace: :delete do
field(:type, :string)
@ -58,7 +60,7 @@ defmodule Mobilizon.Resources.Resource do
timestamps()
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]
@attrs @required_attrs ++ @optional_attrs
@metadata_attrs [
@ -82,6 +84,7 @@ defmodule Mobilizon.Resources.Resource do
|> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2)
|> ensure_url(:resource)
|> maybe_add_published_at()
|> validate_resource_or_folder()
|> validate_required(@required_attrs)
|> 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
Resource
|> where(actor_id: ^group_id)
|> order_by(desc: :updated_at)
|> order_by(desc: :published_at)
|> preload([r], [:actor, :creator])
|> Page.build_page(page, limit)
end

View file

@ -4,14 +4,15 @@ defmodule Mobilizon.Storage.Ecto do
"""
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.Router.Helpers, as: Routes
@doc """
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
from(query, order_by: [{^direction, ^sort}])
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.
Most of the time just go with the given URL.
"""
@spec ensure_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
def ensure_url(%Ecto.Changeset{data: %{url: nil}} = changeset, route) do
@spec ensure_url(Changeset.t(), atom()) :: Changeset.t()
def ensure_url(%Changeset{data: %{url: nil}} = changeset, route) do
case fetch_change(changeset, :url) do
{:ok, _url} ->
changeset
@ -33,10 +34,10 @@ defmodule Mobilizon.Storage.Ecto do
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()
defp generate_url(%Ecto.Changeset{} = changeset, route) do
@spec generate_url(Changeset.t(), atom()) :: Changeset.t()
defp generate_url(%Changeset{} = changeset, route) do
uuid = Ecto.UUID.generate()
changeset
@ -46,4 +47,13 @@ defmodule Mobilizon.Storage.Ecto do
apply(Routes, String.to_existing_atom("page_url"), [Endpoint, route, uuid])
)
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

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Todos.Todo do
use Ecto.Schema
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.Todos.TodoList
@ -16,7 +16,8 @@ defmodule Mobilizon.Todos.Todo do
todo_list: TodoList.t(),
creator: Actor.t(),
assigned_to: Actor.t(),
local: boolean
local: boolean,
published_at: DateTime.t()
}
@primary_key {:id, :binary_id, autogenerate: true}
@ -26,6 +27,7 @@ defmodule Mobilizon.Todos.Todo do
field(:url, :string)
field(:due_date, :utc_datetime)
field(:local, :boolean, default: true)
field(:published_at, :utc_datetime)
belongs_to(:todo_list, TodoList, type: :binary_id)
belongs_to(:creator, Actor)
belongs_to(:assigned_to, Actor)
@ -33,7 +35,7 @@ defmodule Mobilizon.Todos.Todo do
timestamps()
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]
@attrs @required_attrs ++ @optional_attrs
@ -42,6 +44,7 @@ defmodule Mobilizon.Todos.Todo do
todo
|> cast(attrs, @attrs)
|> ensure_url(:todo)
|> maybe_add_published_at()
|> validate_required(@required_attrs)
end
end

View file

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

View file

@ -16,13 +16,30 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
@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 """
Gets a local actor by username.
"""
@spec get_local_actor_by_name(String.t()) ::
{:commit, Actor.t()} | {:ignore, nil}
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
%Actor{} = actor ->
{:commit, actor}

View file

@ -17,6 +17,7 @@ defmodule Mobilizon.Web.Cache do
Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username))
end
defdelegate get_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_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()
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)
end

View file

@ -15,10 +15,14 @@ defmodule Mobilizon.Web.Email.Group do
@doc """
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(
%Member{actor: %Actor{user_id: user_id}, parent: %Actor{} = group, role: :invited} =
member,
locale \\ "en"
locale
) do
with %User{email: email} <- Users.get_user!(user_id) do
Gettext.put_locale(locale)
@ -43,5 +47,33 @@ defmodule Mobilizon.Web.Email.Group do
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()
end

View file

@ -35,7 +35,7 @@
<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 "%{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>
</td>
</tr>

View file

@ -1,5 +1,6 @@
<%= gettext "Come along!" %>
==
<%= 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." %>
<%= 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,
"actor" => activity.actor,
# Not sure if needed since this is used into outbox
"published" => Timex.now(),
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"to" => activity.recipients,
"object" =>
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_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"},
"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"},
"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"},

View file

@ -369,8 +369,7 @@ msgstr[4] ""
msgstr[5] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -398,7 +397,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1199,12 +1198,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -389,7 +388,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1175,12 +1174,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -397,7 +396,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1192,15 +1191,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr "No ho facis servir més que proves, sisplau"
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
msgstr ""
"Mobilizon està en desenvolupament. Hi anirem afegint funcionalitats dins de "
"les actualitzacions freqüents. Treurem la {b_start}versió 1.0 a la primera "
"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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -389,7 +388,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1175,12 +1174,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -402,7 +401,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1200,15 +1199,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
msgstr ""
"Mobilizon befindet sich in der Entwicklung, wir werden neue Funktionen "
"während regulären Updates hinzufügen, bis <b>Version 1 der Software "
"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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -373,7 +372,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1163,3 +1162,35 @@ msgstr ""
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -396,7 +395,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1177,12 +1176,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Please do not use it in any real way"
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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:"
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr "%{inviter} kutsui sinut ryhmään %{group}"
@ -427,7 +426,7 @@ msgstr "Näytä omat ryhmät"
#, elixir-format
#: 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."
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."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Älä käytä todellisiin tarkoituksiin."
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
msgstr ""
"Mobilizonin kehitystyö on vielä käynnissä, ja tälle sivulle lisätään "
"säännöllisesti uusia ominaisuuksia, kunnes ohjelman versio 1 julkaistaan "
"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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -389,7 +388,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1171,12 +1170,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -385,7 +384,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1161,12 +1160,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -398,7 +397,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1195,15 +1194,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
msgstr ""
"Mobilizon is in ontwikkeling, we zullen regelmatig nieuwe functies toevoegen "
"aan deze site via updates, tot <b>versie 1 van de software "
"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:"
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -396,7 +395,7 @@ msgstr "Veire mos grops"
#, elixir-format
#: 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."
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."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Mercés de lutilizar pas dun biais real"
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
msgstr ""
"Mobilizon es en desvolopament, ajustarem de nòvas foncionalitats a aqueste "
"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>."
#, 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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -404,7 +403,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1208,15 +1207,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr "Nie używaj go do żadnych rzeczywistych celów"
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
msgstr ""
"Mobilizon jest w fazie rozwoju. Nowe funkcje będą regularnie dodawane, do "
"wydania <b>pierwszej wersji oprogramowania w pierwszej połowie 2020 "
"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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -387,7 +386,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1168,12 +1167,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -435,7 +434,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1287,15 +1286,47 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
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
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 está em desenvolvimento, Iremos adicionar novos recursos neste "
"site durante as atualizações regulares, até a lançamento da <b>versão "
"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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -396,7 +395,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1186,12 +1185,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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] ""
#, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38
#: lib/web/templates/email/group_invite.text.eex:5
#: lib/web/templates/email/group_invite.text.eex:3
msgid "%{inviter} just invited you to join their group %{group}"
msgstr ""
@ -399,7 +398,7 @@ msgstr ""
#, elixir-format
#: 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."
msgstr ""
@ -1189,12 +1188,44 @@ msgstr ""
msgid "If you didn't trigger the change yourself, please ignore this message."
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/web/templates/email/email.html.eex:89
msgid "<b>Please do not use it for real purposes.</b>"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: 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>."
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 :error == Transmogrifier.handle_incoming(reject_data)
end) =~
"Unable to process Reject activity \"http://mastodon.example.org/users/admin#rejects/follows/4\". Object \"#{
join_activity.data["id"]
}\" wasn't found."
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected"
# Organiser is not present since we use factories directly
assert event.id

View file

@ -39,7 +39,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UndoTest do
assert data["type"] == "Undo"
assert data["object"]["type"] == "Announce"
assert data["object"]["object"] == comment.url
assert data["object"]["object"]["id"] == comment.url
assert data["object"]["id"] ==
"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),
"inReplyTo" => comment.url,
"attributedTo" => reply.actor.url,
"mediaType" => "text/html"
"mediaType" => "text/html",
"published" => reply.published_at |> DateTime.to_iso8601()
} == Converter.Comment.model_to_as(reply)
end

View file

@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
import Mobilizon.Factory
alias Mobilizon.Actors.Member
alias Mobilizon.GraphQL.AbsintheHelpers
setup %{conn: conn} do
@ -145,20 +146,15 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
actor: actor
} do
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 {
leaveGroup(
actor_id: #{actor.id},
group_id: #{group.id}
) {
actor {
id
},
parent {
id
}
}
}
"""
@ -169,8 +165,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
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"]["actor"]["id"] == to_string(actor.id)
assert json_response(res, 200)["data"]["leaveGroup"]["id"] == to_string(member_id)
end
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 {
leaveGroup(
actor_id: #{actor.id},
group_id: #{group.id}
) {
actor {
id
},
parent {
id
}
}
}
"""
@ -213,14 +202,11 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
mutation = """
mutation {
leaveGroup(
actor_id: #{actor.id},
group_id: #{group.id}
) {
actor {
id
}
}
}
"""
res =
@ -230,7 +216,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
assert hd(json_response(res, 200)["errors"])["message"] =~ "logged-in"
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,
user: user,
actor: actor
@ -241,43 +227,11 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
mutation = """
mutation {
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
) {
actor {
id
}
}
}
"""
res =
@ -285,7 +239,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
|> auth_conn(user)
|> 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
@ -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", %{
conn: conn,
user: user,
actor: actor,
group: group,
target_actor: target_actor
} 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", %{
conn: conn,
user: user,
actor: actor,
target_actor: target_actor
} do
res =

View file

@ -649,18 +649,33 @@ defmodule Mobilizon.ActorsTest do
assert %Page{elements: [actor], total: 1} = Actors.list_members_for_group(group)
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,
group: group
} do
create_test_member(%{actor: actor, group: group})
%Member{id: member_id, url: member_url} = create_test_member(%{actor: actor, group: group})
valid_attrs =
@valid_attrs
attrs =
%{}
|> Map.put(:actor_id, actor.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
test "create_member/1 with invalid data returns error changeset" do

View file

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