Allow group admins to moderate new members
Closes #881 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
ae24fa17d5
commit
6eba531c89
|
@ -148,6 +148,11 @@ export default class GroupActivityItem extends mixins(ActivityMixin) {
|
||||||
case Openness.INVITE_ONLY:
|
case Openness.INVITE_ONLY:
|
||||||
details.push("The group can now only be joined with an invite.");
|
details.push("The group can now only be joined with an invite.");
|
||||||
break;
|
break;
|
||||||
|
case Openness.MODERATED:
|
||||||
|
details.push(
|
||||||
|
"The group can now be joined by anyone, but new members need to be approved by an administrator."
|
||||||
|
);
|
||||||
|
break;
|
||||||
case Openness.OPEN:
|
case Openness.OPEN:
|
||||||
details.push("The group can now be joined by anyone.");
|
details.push("The group can now be joined by anyone.");
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -9,13 +9,7 @@
|
||||||
:inline="true"
|
:inline="true"
|
||||||
slot="member"
|
slot="member"
|
||||||
>
|
>
|
||||||
<b>
|
<b> {{ displayName(activity.object.actor) }}</b></popover-actor-card
|
||||||
{{
|
|
||||||
$t("@{username}", {
|
|
||||||
username: usernameWithDomain(activity.object.actor),
|
|
||||||
})
|
|
||||||
}}</b
|
|
||||||
></popover-actor-card
|
|
||||||
>
|
>
|
||||||
<b slot="member" v-else>{{
|
<b slot="member" v-else>{{
|
||||||
subjectParams.member_actor_federated_username
|
subjectParams.member_actor_federated_username
|
||||||
|
@ -25,13 +19,7 @@
|
||||||
:inline="true"
|
:inline="true"
|
||||||
slot="profile"
|
slot="profile"
|
||||||
>
|
>
|
||||||
<b>
|
<b> {{ displayName(activity.author) }}</b></popover-actor-card
|
||||||
{{
|
|
||||||
$t("@{username}", {
|
|
||||||
username: usernameWithDomain(activity.author),
|
|
||||||
})
|
|
||||||
}}</b
|
|
||||||
></popover-actor-card
|
|
||||||
></i18n
|
></i18n
|
||||||
>
|
>
|
||||||
<small class="has-text-grey-dark activity-date">{{
|
<small class="has-text-grey-dark activity-date">{{
|
||||||
|
@ -41,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { usernameWithDomain } from "@/types/actor";
|
import { displayName } from "@/types/actor";
|
||||||
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
|
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
|
||||||
import { Component } from "vue-property-decorator";
|
import { Component } from "vue-property-decorator";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
@ -62,7 +50,7 @@ export const MEMBER_ROLE_VALUE: Record<string, number> = {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class MemberActivityItem extends mixins(ActivityMixin) {
|
export default class MemberActivityItem extends mixins(ActivityMixin) {
|
||||||
usernameWithDomain = usernameWithDomain;
|
displayName = displayName;
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
ActivityMemberSubject = ActivityMemberSubject;
|
ActivityMemberSubject = ActivityMemberSubject;
|
||||||
|
|
||||||
|
@ -83,6 +71,14 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
|
||||||
return "You added the member {member}.";
|
return "You added the member {member}.";
|
||||||
}
|
}
|
||||||
return "{profile} added the member {member}.";
|
return "{profile} added the member {member}.";
|
||||||
|
case ActivityMemberSubject.MEMBER_APPROVED:
|
||||||
|
if (this.isAuthorCurrentActor) {
|
||||||
|
return "You approved {member}'s membership.";
|
||||||
|
}
|
||||||
|
if (this.isObjectMemberCurrentActor) {
|
||||||
|
return "Your membership was approved by {profile}.";
|
||||||
|
}
|
||||||
|
return "{profile} approved {member}'s membership.";
|
||||||
case ActivityMemberSubject.MEMBER_JOINED:
|
case ActivityMemberSubject.MEMBER_JOINED:
|
||||||
return "{member} joined the group.";
|
return "{member} joined the group.";
|
||||||
case ActivityMemberSubject.MEMBER_UPDATED:
|
case ActivityMemberSubject.MEMBER_UPDATED:
|
||||||
|
@ -94,6 +90,12 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
|
||||||
}
|
}
|
||||||
return "{profile} updated the member {member}.";
|
return "{profile} updated the member {member}.";
|
||||||
case ActivityMemberSubject.MEMBER_REMOVED:
|
case ActivityMemberSubject.MEMBER_REMOVED:
|
||||||
|
if (this.subjectParams.member_role === MemberRole.NOT_APPROVED) {
|
||||||
|
if (this.isAuthorCurrentActor) {
|
||||||
|
return "You rejected {member}'s membership request.";
|
||||||
|
}
|
||||||
|
return "{profile} rejected {member}'s membership request.";
|
||||||
|
}
|
||||||
if (this.isAuthorCurrentActor) {
|
if (this.isAuthorCurrentActor) {
|
||||||
return "You excluded member {member}.";
|
return "You excluded member {member}.";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,65 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media">
|
<div class="card">
|
||||||
<div class="media-content">
|
<div class="card-content media">
|
||||||
<div class="content">
|
<div class="media-content">
|
||||||
<i18n
|
<div class="content">
|
||||||
tag="p"
|
<i18n
|
||||||
path="You have been invited by {invitedBy} to the following group:"
|
tag="p"
|
||||||
>
|
path="You have been invited by {invitedBy} to the following group:"
|
||||||
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
|
>
|
||||||
</i18n>
|
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
|
||||||
</div>
|
</i18n>
|
||||||
<div class="media subfield">
|
|
||||||
<div class="media-left">
|
|
||||||
<figure class="image is-48x48" v-if="member.parent.avatar">
|
|
||||||
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
|
|
||||||
</figure>
|
|
||||||
<b-icon v-else size="is-large" icon="account-group" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media subfield">
|
||||||
<div class="level">
|
<div class="media-left">
|
||||||
<div class="level-left">
|
<figure class="image is-48x48" v-if="member.parent.avatar">
|
||||||
<div class="level-item">
|
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
|
||||||
<router-link
|
</figure>
|
||||||
:to="{
|
<b-icon v-else size="is-large" icon="account-group" />
|
||||||
name: RouteName.GROUP,
|
</div>
|
||||||
params: {
|
<div class="media-content">
|
||||||
preferredUsername: usernameWithDomain(member.parent),
|
<div class="level">
|
||||||
},
|
<div class="level-left">
|
||||||
}"
|
<div class="level-item mr-3">
|
||||||
>
|
<router-link
|
||||||
<h3>{{ member.parent.name }}</h3>
|
:to="{
|
||||||
<p class="is-6 has-text-grey">
|
name: RouteName.GROUP,
|
||||||
<span v-if="member.parent.domain">
|
params: {
|
||||||
{{
|
preferredUsername: usernameWithDomain(member.parent),
|
||||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
},
|
||||||
}}
|
}"
|
||||||
</span>
|
>
|
||||||
<span v-else>{{
|
<h3 class="is-size-5">{{ member.parent.name }}</h3>
|
||||||
`@${member.parent.preferredUsername}`
|
<p class="is-size-7 has-text-grey-dark">
|
||||||
}}</span>
|
<span v-if="member.parent.domain">
|
||||||
</p>
|
{{
|
||||||
</router-link>
|
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{
|
||||||
|
`@${member.parent.preferredUsername}`
|
||||||
|
}}</span>
|
||||||
|
</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="level-right">
|
||||||
<div class="level-right">
|
<div class="level-item">
|
||||||
<div class="level-item">
|
<b-button
|
||||||
<b-button type="is-success" @click="$emit('accept', member.id)">
|
type="is-success"
|
||||||
{{ $t("Accept") }}
|
@click="$emit('accept', member.id)"
|
||||||
</b-button>
|
>
|
||||||
</div>
|
{{ $t("Accept") }}
|
||||||
<div class="level-item">
|
</b-button>
|
||||||
<b-button type="is-danger" @click="$emit('reject', member.id)">
|
</div>
|
||||||
{{ $t("Decline") }}
|
<div class="level-item">
|
||||||
</b-button>
|
<b-button
|
||||||
|
type="is-danger"
|
||||||
|
@click="$emit('reject', member.id)"
|
||||||
|
>
|
||||||
|
{{ $t("Decline") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,4 +90,7 @@ export default class InvitationCard extends Vue {
|
||||||
background: lighten($primary, 40%);
|
background: lighten($primary, 40%);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
h3 {
|
||||||
|
color: $violet-3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<section v-if="invitations && invitations.length > 0">
|
<section class="card my-3" v-if="invitations && invitations.length > 0">
|
||||||
<InvitationCard
|
<InvitationCard
|
||||||
v-for="member in invitations"
|
v-for="member in invitations"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
|
@ -13,8 +13,9 @@
|
||||||
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
import { PERSON_STATUS_GROUP } from "@/graphql/actor";
|
||||||
import { IMember } from "@/types/actor/member.model";
|
import { IMember } from "@/types/actor/member.model";
|
||||||
|
import { IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -26,18 +27,25 @@ export default class Invitations extends Vue {
|
||||||
|
|
||||||
async acceptInvitation(id: string): Promise<void> {
|
async acceptInvitation(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
|
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||||
{
|
mutation: ACCEPT_INVITATION,
|
||||||
mutation: ACCEPT_INVITATION,
|
variables: {
|
||||||
variables: {
|
id,
|
||||||
id,
|
},
|
||||||
},
|
refetchQueries({ data }) {
|
||||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
const profile = data?.acceptInvitation?.actor as IPerson;
|
||||||
}
|
const group = data?.acceptInvitation?.parent as IGroup;
|
||||||
);
|
if (profile && group) {
|
||||||
if (data) {
|
return [
|
||||||
this.$emit("accept-invitation", data.acceptInvitation);
|
{
|
||||||
}
|
query: PERSON_STATUS_GROUP,
|
||||||
|
variables: { id: profile.id, group: usernameWithDomain(group) },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
|
@ -48,18 +56,25 @@ export default class Invitations extends Vue {
|
||||||
|
|
||||||
async rejectInvitation(id: string): Promise<void> {
|
async rejectInvitation(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
|
await this.$apollo.mutate<{ rejectInvitation: IMember }>({
|
||||||
{
|
mutation: REJECT_INVITATION,
|
||||||
mutation: REJECT_INVITATION,
|
variables: {
|
||||||
variables: {
|
id,
|
||||||
id,
|
},
|
||||||
},
|
refetchQueries({ data }) {
|
||||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
const profile = data?.rejectInvitation?.actor as IPerson;
|
||||||
}
|
const group = data?.rejectInvitation?.parent as IGroup;
|
||||||
);
|
if (profile && group) {
|
||||||
if (data) {
|
return [
|
||||||
this.$emit("reject-invitation", data.rejectInvitation);
|
{
|
||||||
}
|
query: PERSON_STATUS_GROUP,
|
||||||
|
variables: { id: profile.id, group: usernameWithDomain(group) },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
|
|
|
@ -37,9 +37,10 @@ export const ACCEPT_INVITATION = gql`
|
||||||
export const REJECT_INVITATION = gql`
|
export const REJECT_INVITATION = gql`
|
||||||
mutation RejectInvitation($id: ID!) {
|
mutation RejectInvitation($id: ID!) {
|
||||||
rejectInvitation(id: $id) {
|
rejectInvitation(id: $id) {
|
||||||
id
|
...MemberFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${MEMBER_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GROUP_MEMBERS = gql`
|
export const GROUP_MEMBERS = gql`
|
||||||
|
@ -72,13 +73,22 @@ export const UPDATE_MEMBER = gql`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const REMOVE_MEMBER = gql`
|
export const REMOVE_MEMBER = gql`
|
||||||
mutation RemoveMember($groupId: ID!, $memberId: ID!) {
|
mutation RemoveMember($memberId: ID!, $exclude: Boolean) {
|
||||||
removeMember(groupId: $groupId, memberId: $memberId) {
|
removeMember(memberId: $memberId, exclude: $exclude) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const APPROVE_MEMBER = gql`
|
||||||
|
mutation ApproveMember($memberId: ID!) {
|
||||||
|
approveMember(memberId: $memberId) {
|
||||||
|
...MemberFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${MEMBER_FRAGMENT}
|
||||||
|
`;
|
||||||
|
|
||||||
export const JOIN_GROUP = gql`
|
export const JOIN_GROUP = gql`
|
||||||
mutation JoinGroup($groupId: ID!) {
|
mutation JoinGroup($groupId: ID!) {
|
||||||
joinGroup(groupId: $groupId) {
|
joinGroup(groupId: $groupId) {
|
||||||
|
|
|
@ -881,6 +881,7 @@
|
||||||
"{member} was invited by {profile}.": "{member} was invited by {profile}.",
|
"{member} was invited by {profile}.": "{member} was invited by {profile}.",
|
||||||
"You added the member {member}.": "You added the member {member}.",
|
"You added the member {member}.": "You added the member {member}.",
|
||||||
"{profile} added the member {member}.": "{profile} added the member {member}.",
|
"{profile} added the member {member}.": "{profile} added the member {member}.",
|
||||||
|
"{member} joined the group.": "{member} joined the group.",
|
||||||
"{member} rejected the invitation to join the group.": "{member} rejected the invitation to join the group.",
|
"{member} rejected the invitation to join the group.": "{member} rejected the invitation to join the group.",
|
||||||
"{member} accepted the invitation to join the group.": "{member} accepted the invitation to join the group.",
|
"{member} accepted the invitation to join the group.": "{member} accepted the invitation to join the group.",
|
||||||
"You excluded member {member}.": "You excluded member {member}.",
|
"You excluded member {member}.": "You excluded member {member}.",
|
||||||
|
@ -1233,5 +1234,17 @@
|
||||||
"Any type": "Any type",
|
"Any type": "Any type",
|
||||||
"In person": "In person",
|
"In person": "In person",
|
||||||
"In the past": "In the past",
|
"In the past": "In the past",
|
||||||
"Only registered users may fetch remote events from their URL.": "Only registered users may fetch remote events from their URL."
|
"Only registered users may fetch remote events from their URL.": "Only registered users may fetch remote events from their URL.",
|
||||||
|
"Moderate new members": "Moderate new members",
|
||||||
|
"Anyone can request being a member, but an administrator needs to approve the membership.": "Anyone can request being a member, but an administrator needs to approve the membership.",
|
||||||
|
"Cancel membership request": "Cancel membership request",
|
||||||
|
"group's upcoming public events": "group's upcoming public events",
|
||||||
|
"access to the group's private content as well": "access to the group's private content as well",
|
||||||
|
"Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.": "Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.",
|
||||||
|
"The group can now be joined by anyone, but new members need to be approved by an administrator.": "The group can now be joined by anyone, but new members need to be approved by an administrator.",
|
||||||
|
"You approved {member}'s membership.": "You approved {member}'s membership.",
|
||||||
|
"Your membership was approved by {profile}.": "Your membership was approved by {profile}.",
|
||||||
|
"{profile} approved {member}'s membership.": "{profile} approved {member}'s membership.",
|
||||||
|
"You rejected {member}'s membership request.": "You rejected {member}'s membership request.",
|
||||||
|
"{profile} rejected {member}'s membership request.": "{profile} rejected {member}'s membership request."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1077,7 +1077,7 @@
|
||||||
"You excluded member {member}.": "Vous avez exclu le ou la membre {member}.",
|
"You excluded member {member}.": "Vous avez exclu le ou la membre {member}.",
|
||||||
"You have been disconnected": "Vous avez été déconnecté⋅e",
|
"You have been disconnected": "Vous avez été déconnecté⋅e",
|
||||||
"You have been invited by {invitedBy} to the following group:": "Vous avez été invité par {invitedBy} à rejoindre le groupe suivant :",
|
"You have been invited by {invitedBy} to the following group:": "Vous avez été invité par {invitedBy} à rejoindre le groupe suivant :",
|
||||||
"You have been removed from this group's members.": "Vous avez été exclu des membres de ce groupe.",
|
"You have been removed from this group's members.": "Vous avez été exclu⋅e des membres de ce groupe.",
|
||||||
"You have cancelled your participation": "Vous avez annulé votre participation",
|
"You have cancelled your participation": "Vous avez annulé votre participation",
|
||||||
"You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
|
"You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
|
||||||
"You have one event today.": "Vous n'avez pas d'événement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
|
"You have one event today.": "Vous n'avez pas d'événement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
|
||||||
|
@ -1231,6 +1231,7 @@
|
||||||
"{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.",
|
"{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.",
|
||||||
"{profile} (by default)": "{profile} (par défault)",
|
"{profile} (by default)": "{profile} (par défault)",
|
||||||
"{profile} added the member {member}.": "{profile} a ajouté le ou la membre {member}.",
|
"{profile} added the member {member}.": "{profile} a ajouté le ou la membre {member}.",
|
||||||
|
"{member} joined the group.": "{member} a rejoint le groupe.",
|
||||||
"{profile} archived the discussion {discussion}.": "{profile} a archivé la discussion {discussion}.",
|
"{profile} archived the discussion {discussion}.": "{profile} a archivé la discussion {discussion}.",
|
||||||
"{profile} created the discussion {discussion}.": "{profile} a créé la discussion {discussion}.",
|
"{profile} created the discussion {discussion}.": "{profile} a créé la discussion {discussion}.",
|
||||||
"{profile} created the folder {resource}.": "{profile} a créé le dossier {resource}.",
|
"{profile} created the folder {resource}.": "{profile} a créé le dossier {resource}.",
|
||||||
|
@ -1337,5 +1338,17 @@
|
||||||
"Any type": "N'importe quel type",
|
"Any type": "N'importe quel type",
|
||||||
"In person": "En personne",
|
"In person": "En personne",
|
||||||
"In the past": "Dans le passé",
|
"In the past": "Dans le passé",
|
||||||
"Only registered users may fetch remote events from their URL.": "Seul⋅es les utilisateur⋅ices enregistré⋅es peuvent récupérer des événements depuis leur URL."
|
"Only registered users may fetch remote events from their URL.": "Seul⋅es les utilisateur⋅ices enregistré⋅es peuvent récupérer des événements depuis leur URL.",
|
||||||
|
"Moderate new members": "Modérer les nouvelles et nouveaux membres",
|
||||||
|
"Anyone can request being a member, but an administrator needs to approve the membership.": "N'importe qui peut demander à être membre, mais un⋅e administrateur⋅ice devra approuver leur adhésion.",
|
||||||
|
"Cancel membership request": "Annuler la demande d'adhésion",
|
||||||
|
"group's upcoming public events": "prochains événements publics du groupe",
|
||||||
|
"access to the group's private content as well": "accédez également au contenu privé du groupe",
|
||||||
|
"Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.": "Suivre le groupe vous permettra d'être informé⋅e des {group_upcoming_public_events}, alors que rejoindre le groupe signfie que vous {access_to_group_private_content_as_well}, y compris les discussion de groupe, les resources du groupe et les billets réservés au groupe.",
|
||||||
|
"The group can now be joined by anyone, but new members need to be approved by an administrator.": "Le groupe peut maintenant être rejoint par n'importe qui, mais les nouvelles et nouveaux membres doivent être approuvées par un⋅e modérateur⋅ice.",
|
||||||
|
"You approved {member}'s membership.": "Vous avez approuvé la demande d'adhésion de {member}.",
|
||||||
|
"Your membership was approved by {profile}.": "Votre demande d'adhésion a été approuvée par {profile}.",
|
||||||
|
"{profile} approved {member}'s membership.": "{profile} a approuvé la demande d'adhésion de {member}.",
|
||||||
|
"You rejected {member}'s membership request.": "Vous avez rejeté la demande d'adhésion de {member}.",
|
||||||
|
"{profile} rejected {member}'s membership request.": "{profile} a rejeté la demande d'adhésion de {member}."
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,10 @@ export default class GroupMixin extends Vue {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isCurrentActorAPendingGroupMember(): boolean {
|
||||||
|
return this.hasCurrentActorThisRole([MemberRole.NOT_APPROVED]);
|
||||||
|
}
|
||||||
|
|
||||||
hasCurrentActorThisRole(givenRole: string | string[]): boolean {
|
hasCurrentActorThisRole(givenRole: string | string[]): boolean {
|
||||||
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -23,29 +23,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
<invitations
|
|
||||||
v-if="isCurrentActorAnInvitedGroupMember"
|
|
||||||
:invitations="[groupMember]"
|
|
||||||
@acceptInvitation="acceptInvitation"
|
|
||||||
@reject-invitation="rejectInvitation"
|
|
||||||
/>
|
|
||||||
<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 &&
|
|
||||||
isCurrentActorOnADifferentDomainThanGroup
|
|
||||||
"
|
|
||||||
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" v-if="group">
|
<header class="block-container presentation" v-if="group">
|
||||||
<div class="banner-container">
|
<div class="banner-container">
|
||||||
<lazy-image-wrapper :picture="group.banner" />
|
<lazy-image-wrapper :picture="group.banner" />
|
||||||
|
@ -137,7 +114,7 @@
|
||||||
<b-tooltip
|
<b-tooltip
|
||||||
v-if="
|
v-if="
|
||||||
(!isCurrentActorAGroupMember || previewPublic) &&
|
(!isCurrentActorAGroupMember || previewPublic) &&
|
||||||
group.openness !== Openness.OPEN
|
group.openness === Openness.INVITE_ONLY
|
||||||
"
|
"
|
||||||
:label="$t('This group is invite-only')"
|
:label="$t('This group is invite-only')"
|
||||||
position="is-bottom"
|
position="is-bottom"
|
||||||
|
@ -148,7 +125,9 @@
|
||||||
>
|
>
|
||||||
<b-button
|
<b-button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
(!isCurrentActorAGroupMember || previewPublic) &&
|
((!isCurrentActorAGroupMember &&
|
||||||
|
!isCurrentActorAPendingGroupMember) ||
|
||||||
|
previewPublic) &&
|
||||||
currentActor.id
|
currentActor.id
|
||||||
"
|
"
|
||||||
@click="joinGroup"
|
@click="joinGroup"
|
||||||
|
@ -157,6 +136,14 @@
|
||||||
:disabled="previewPublic"
|
:disabled="previewPublic"
|
||||||
>{{ $t("Join group") }}</b-button
|
>{{ $t("Join group") }}</b-button
|
||||||
>
|
>
|
||||||
|
<b-button
|
||||||
|
outlined
|
||||||
|
v-else-if="isCurrentActorAPendingGroupMember"
|
||||||
|
@click="leaveGroup"
|
||||||
|
@keyup.enter="leaveGroup"
|
||||||
|
type="is-primary"
|
||||||
|
>{{ $t("Cancel membership request") }}</b-button
|
||||||
|
>
|
||||||
<b-button
|
<b-button
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
:to="{
|
:to="{
|
||||||
|
@ -310,6 +297,49 @@
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<invitations
|
||||||
|
v-if="isCurrentActorAnInvitedGroupMember"
|
||||||
|
:invitations="[groupMember]"
|
||||||
|
/>
|
||||||
|
<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 &&
|
||||||
|
isCurrentActorOnADifferentDomainThanGroup
|
||||||
|
"
|
||||||
|
type="is-info"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"Since you are a new member, private content can take a few minutes to appear."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</b-message>
|
||||||
|
<b-message
|
||||||
|
v-if="
|
||||||
|
!isCurrentActorAGroupMember &&
|
||||||
|
!isCurrentActorAPendingGroupMember &&
|
||||||
|
!isCurrentActorPendingFollow &&
|
||||||
|
!isCurrentActorFollowing
|
||||||
|
"
|
||||||
|
type="is-info"
|
||||||
|
has-icon
|
||||||
|
class="m-3"
|
||||||
|
>
|
||||||
|
<i18n
|
||||||
|
path="Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts."
|
||||||
|
>
|
||||||
|
<b slot="group_upcoming_public_events">{{
|
||||||
|
$t("group's upcoming public events")
|
||||||
|
}}</b>
|
||||||
|
<b slot="access_to_group_private_content_as_well">{{
|
||||||
|
$t("access to the group's private content as well")
|
||||||
|
}}</b>
|
||||||
|
</i18n>
|
||||||
|
</b-message>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
@ -893,31 +923,6 @@ export default class Group extends mixins(GroupMixin) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptInvitation(): void {
|
|
||||||
if (this.groupMember) {
|
|
||||||
const index = this.person.memberships.elements.findIndex(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectInvitation({ id: memberId }: { id: string }): void {
|
|
||||||
const index = this.person.memberships.elements.findIndex(
|
|
||||||
(membership) =>
|
|
||||||
membership.role === MemberRole.INVITED && membership.id === memberId
|
|
||||||
);
|
|
||||||
if (index > -1) {
|
|
||||||
this.person.memberships.elements.splice(index, 1);
|
|
||||||
this.person.memberships.total -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reportGroup(content: string, forward: boolean): Promise<void> {
|
async reportGroup(content: string, forward: boolean): Promise<void> {
|
||||||
this.isReportModalActive = false;
|
this.isReportModalActive = false;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|
|
@ -195,6 +195,20 @@
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
<b-table-column field="actions" :label="$t('Actions')" v-slot="props">
|
<b-table-column field="actions" :label="$t('Actions')" v-slot="props">
|
||||||
<div class="buttons" v-if="props.row.actor.id !== currentActor.id">
|
<div class="buttons" v-if="props.row.actor.id !== currentActor.id">
|
||||||
|
<b-button
|
||||||
|
type="is-success"
|
||||||
|
v-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||||
|
@click="approveMember(props.row)"
|
||||||
|
icon-left="check"
|
||||||
|
>{{ $t("Approve member") }}</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
type="is-danger"
|
||||||
|
v-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||||
|
@click="rejectMember(props.row)"
|
||||||
|
icon-left="exit-to-app"
|
||||||
|
>{{ $t("Reject member") }}</b-button
|
||||||
|
>
|
||||||
<b-button
|
<b-button
|
||||||
v-if="
|
v-if="
|
||||||
[MemberRole.MEMBER, MemberRole.MODERATOR].includes(
|
[MemberRole.MEMBER, MemberRole.MODERATOR].includes(
|
||||||
|
@ -217,7 +231,7 @@
|
||||||
>
|
>
|
||||||
<b-button
|
<b-button
|
||||||
v-if="props.row.role === MemberRole.MEMBER"
|
v-if="props.row.role === MemberRole.MEMBER"
|
||||||
@click="removeMember(props.row.id)"
|
@click="removeMember(props.row)"
|
||||||
type="is-danger"
|
type="is-danger"
|
||||||
icon-left="exit-to-app"
|
icon-left="exit-to-app"
|
||||||
>{{ $t("Remove") }}</b-button
|
>{{ $t("Remove") }}</b-button
|
||||||
|
@ -250,8 +264,9 @@ import {
|
||||||
GROUP_MEMBERS,
|
GROUP_MEMBERS,
|
||||||
REMOVE_MEMBER,
|
REMOVE_MEMBER,
|
||||||
UPDATE_MEMBER,
|
UPDATE_MEMBER,
|
||||||
|
APPROVE_MEMBER,
|
||||||
} from "../../graphql/member";
|
} from "../../graphql/member";
|
||||||
import { usernameWithDomain } from "../../types/actor";
|
import { usernameWithDomain, displayName } from "../../types/actor";
|
||||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -332,7 +347,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
|
||||||
this.$notifier.success(
|
this.$notifier.success(
|
||||||
this.$t("{username} was invited to {group}", {
|
this.$t("{username} was invited to {group}", {
|
||||||
username: this.newMemberUsername,
|
username: this.newMemberUsername,
|
||||||
group: this.group.name || usernameWithDomain(this.group),
|
group: displayName(this.group),
|
||||||
}) as string
|
}) as string
|
||||||
);
|
);
|
||||||
this.newMemberUsername = "";
|
this.newMemberUsername = "";
|
||||||
|
@ -375,7 +390,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeMember(memberId: string): Promise<void> {
|
async removeMember(oldMember: IMember): Promise<void> {
|
||||||
const { roles, MEMBERS_PER_PAGE, group, page } = this;
|
const { roles, MEMBERS_PER_PAGE, group, page } = this;
|
||||||
const variables = {
|
const variables = {
|
||||||
name: usernameWithDomain(group),
|
name: usernameWithDomain(group),
|
||||||
|
@ -388,7 +403,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
|
||||||
mutation: REMOVE_MEMBER,
|
mutation: REMOVE_MEMBER,
|
||||||
variables: {
|
variables: {
|
||||||
groupId: this.group.id,
|
groupId: this.group.id,
|
||||||
memberId,
|
memberId: oldMember.id,
|
||||||
},
|
},
|
||||||
refetchQueries: [
|
refetchQueries: [
|
||||||
{
|
{
|
||||||
|
@ -397,12 +412,18 @@ export default class GroupMembers extends mixins(GroupMixin) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
this.$notifier.success(
|
let message = this.$t("The member was removed from the group {group}", {
|
||||||
this.$t("The member was removed from the group {group}", {
|
group: displayName(this.group),
|
||||||
username: this.newMemberUsername,
|
}) as string;
|
||||||
group: this.group.name || usernameWithDomain(this.group),
|
if (oldMember.role === MemberRole.NOT_APPROVED) {
|
||||||
}) as string
|
message = this.$t(
|
||||||
);
|
"The membership request from {profile} was rejected",
|
||||||
|
{
|
||||||
|
group: displayName(oldMember.actor),
|
||||||
|
}
|
||||||
|
) as string;
|
||||||
|
}
|
||||||
|
this.$notifier.success(message);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
|
@ -414,29 +435,49 @@ export default class GroupMembers extends mixins(GroupMixin) {
|
||||||
promoteMember(member: IMember): void {
|
promoteMember(member: IMember): void {
|
||||||
if (!member.id) return;
|
if (!member.id) return;
|
||||||
if (member.role === MemberRole.MODERATOR) {
|
if (member.role === MemberRole.MODERATOR) {
|
||||||
this.updateMember(member.id, MemberRole.ADMINISTRATOR);
|
this.updateMember(member, MemberRole.ADMINISTRATOR);
|
||||||
}
|
}
|
||||||
if (member.role === MemberRole.MEMBER) {
|
if (member.role === MemberRole.MEMBER) {
|
||||||
this.updateMember(member.id, MemberRole.MODERATOR);
|
this.updateMember(member, MemberRole.MODERATOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
demoteMember(member: IMember): void {
|
demoteMember(member: IMember): void {
|
||||||
if (!member.id) return;
|
if (!member.id) return;
|
||||||
if (member.role === MemberRole.MODERATOR) {
|
if (member.role === MemberRole.MODERATOR) {
|
||||||
this.updateMember(member.id, MemberRole.MEMBER);
|
this.updateMember(member, MemberRole.MEMBER);
|
||||||
}
|
}
|
||||||
if (member.role === MemberRole.ADMINISTRATOR) {
|
if (member.role === MemberRole.ADMINISTRATOR) {
|
||||||
this.updateMember(member.id, MemberRole.MODERATOR);
|
this.updateMember(member, MemberRole.MODERATOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMember(memberId: string, role: MemberRole): Promise<void> {
|
async approveMember(member: IMember): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate<{ approveMember: IMember }>({
|
||||||
|
mutation: APPROVE_MEMBER,
|
||||||
|
variables: { memberId: member.id },
|
||||||
|
});
|
||||||
|
this.$notifier.success(this.$t("The member was approved") as string);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
|
this.$notifier.error(error.graphQLErrors[0].message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectMember(member: IMember): void {
|
||||||
|
if (!member.id) return;
|
||||||
|
this.removeMember(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMember(oldMember: IMember, role: MemberRole): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate<{ updateMember: IMember }>({
|
await this.$apollo.mutate<{ updateMember: IMember }>({
|
||||||
mutation: UPDATE_MEMBER,
|
mutation: UPDATE_MEMBER,
|
||||||
variables: {
|
variables: {
|
||||||
memberId,
|
memberId: oldMember.id,
|
||||||
role,
|
role,
|
||||||
},
|
},
|
||||||
refetchQueries: [
|
refetchQueries: [
|
||||||
|
@ -455,8 +496,14 @@ export default class GroupMembers extends mixins(GroupMixin) {
|
||||||
successMessage = "The member role was updated to administrator";
|
successMessage = "The member role was updated to administrator";
|
||||||
break;
|
break;
|
||||||
case MemberRole.MEMBER:
|
case MemberRole.MEMBER:
|
||||||
|
if (oldMember.role === MemberRole.NOT_APPROVED) {
|
||||||
|
successMessage = "The member was approved";
|
||||||
|
} else {
|
||||||
|
successMessage = "The member role was updated to simple member";
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
successMessage = "The member role was updated to simple member";
|
successMessage = "The member role was updated";
|
||||||
}
|
}
|
||||||
this.$notifier.success(this.$t(successMessage) as string);
|
this.$notifier.success(this.$t(successMessage) as string);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
@ -128,6 +128,19 @@
|
||||||
}}</small>
|
}}</small>
|
||||||
</b-radio>
|
</b-radio>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<b-radio
|
||||||
|
v-model="editableGroup.openness"
|
||||||
|
name="groupOpenness"
|
||||||
|
:native-value="Openness.MODERATED"
|
||||||
|
>{{ $t("Moderate new members") }}<br />
|
||||||
|
<small>{{
|
||||||
|
$t(
|
||||||
|
"Anyone can request being a member, but an administrator needs to approve the membership."
|
||||||
|
)
|
||||||
|
}}</small>
|
||||||
|
</b-radio>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<b-radio
|
<b-radio
|
||||||
v-model="editableGroup.openness"
|
v-model="editableGroup.openness"
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
|
||||||
alias Mobilizon.Federation.ActivityStream
|
alias Mobilizon.Federation.ActivityStream
|
||||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||||
alias Mobilizon.Service.Notifications.Scheduler
|
alias Mobilizon.Service.Notifications.Scheduler
|
||||||
|
alias Mobilizon.Web.Email.Member, as: EmailMember
|
||||||
alias Mobilizon.Web.Endpoint
|
alias Mobilizon.Web.Endpoint
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
|
||||||
maybe_relay_if_group_activity: 1
|
maybe_relay_if_group_activity: 1
|
||||||
]
|
]
|
||||||
|
|
||||||
@type acceptable_types :: :join | :follow | :invite
|
@type acceptable_types :: :join | :follow | :invite | :member
|
||||||
@type acceptable_entities ::
|
@type acceptable_entities ::
|
||||||
accept_join_entities | accept_follow_entities | accept_invite_entities
|
accept_join_entities | accept_follow_entities | accept_invite_entities
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
|
||||||
:join -> accept_join(entity, additional)
|
:join -> accept_join(entity, additional)
|
||||||
:follow -> accept_follow(entity, additional)
|
:follow -> accept_follow(entity, additional)
|
||||||
:invite -> accept_invite(entity, additional)
|
:invite -> accept_invite(entity, additional)
|
||||||
|
:member -> accept_member(entity, additional)
|
||||||
end
|
end
|
||||||
|
|
||||||
with {:ok, entity, update_data} <- accept_res do
|
with {:ok, entity, update_data} <- accept_res do
|
||||||
|
@ -158,12 +160,47 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec maybe_refresh_group(Member.t()) :: :ok | nil
|
@spec accept_member(Member.t(), map()) ::
|
||||||
defp maybe_refresh_group(%Member{
|
{:ok, Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||||
parent: %Actor{domain: parent_domain, url: parent_url},
|
defp accept_member(
|
||||||
actor: %Actor{} = actor
|
%Member{actor_id: actor_id, actor: actor, parent: %Actor{} = group} = member,
|
||||||
}) do
|
%{moderator: %Actor{url: actor_url} = moderator}
|
||||||
unless is_nil(parent_domain),
|
) do
|
||||||
do: Refresher.fetch_group(parent_url, actor)
|
with %Actor{} <- Actors.get_actor!(actor_id),
|
||||||
|
{:ok, %Member{id: member_id} = member} <-
|
||||||
|
Actors.update_member(member, %{role: :member}) do
|
||||||
|
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||||
|
subject: "member_approved",
|
||||||
|
moderator: moderator
|
||||||
|
)
|
||||||
|
|
||||||
|
Absinthe.Subscription.publish(Endpoint, actor,
|
||||||
|
group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
EmailMember.send_notification_to_approved_member(member)
|
||||||
|
|
||||||
|
Cachex.del(:activity_pub, "member_#{member_id}")
|
||||||
|
|
||||||
|
maybe_refresh_group(member)
|
||||||
|
|
||||||
|
accept_data = %{
|
||||||
|
"type" => "Accept",
|
||||||
|
"attributedTo" => member.parent.url,
|
||||||
|
"to" => [member.parent.members_url],
|
||||||
|
"cc" => [member.parent.url],
|
||||||
|
"actor" => actor_url,
|
||||||
|
"object" => Convertible.model_to_as(member),
|
||||||
|
"id" => "#{Endpoint.url()}/accept/member/#{member_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, member, accept_data}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec maybe_refresh_group(Member.t()) :: {:ok, Actor.t()} | {:error, atom()} | {:error}
|
||||||
|
defp maybe_refresh_group(%Member{
|
||||||
|
parent: %Actor{} = group
|
||||||
|
}),
|
||||||
|
do: Refresher.refresh_profile(group)
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,14 +69,20 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Leave do
|
||||||
end
|
end
|
||||||
|
|
||||||
def leave(
|
def leave(
|
||||||
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
|
%Actor{
|
||||||
%Actor{id: actor_id, url: actor_url},
|
type: :Group,
|
||||||
|
domain: group_domain,
|
||||||
|
id: group_id,
|
||||||
|
url: group_url,
|
||||||
|
members_url: group_members_url
|
||||||
|
},
|
||||||
|
%Actor{id: actor_id, url: actor_url, domain: actor_domain},
|
||||||
local,
|
local,
|
||||||
additional
|
additional
|
||||||
) do
|
) do
|
||||||
case Actors.get_member(actor_id, group_id) do
|
case Actors.get_member(actor_id, group_id) do
|
||||||
{:ok, %Member{id: member_id} = member} ->
|
{:ok, %Member{id: member_id} = member} ->
|
||||||
if Map.get(additional, :force_member_removal, false) ||
|
if Map.get(additional, :force_member_removal, false) || group_domain != actor_domain ||
|
||||||
!Actors.is_only_administrator?(member_id, group_id) do
|
!Actors.is_only_administrator?(member_id, group_id) do
|
||||||
with {:ok, %Member{} = member} <- Actors.delete_member(member) do
|
with {:ok, %Member{} = member} <- Actors.delete_member(member) do
|
||||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit")
|
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit")
|
||||||
|
|
|
@ -28,6 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Reject do
|
||||||
:join -> reject_join(entity, additional)
|
:join -> reject_join(entity, additional)
|
||||||
:follow -> reject_follow(entity, additional)
|
:follow -> reject_follow(entity, additional)
|
||||||
:invite -> reject_invite(entity, additional)
|
:invite -> reject_invite(entity, additional)
|
||||||
|
:member -> reject_member(entity, additional)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, activity} = create_activity(update_data, local)
|
{:ok, activity} = create_activity(update_data, local)
|
||||||
|
@ -118,4 +119,28 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Reject do
|
||||||
{:ok, member, accept_data}
|
{:ok, member, accept_data}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec reject_member(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||||
|
defp reject_member(
|
||||||
|
%Member{actor_id: actor_id} = member,
|
||||||
|
%{moderator: %Actor{url: actor_url}}
|
||||||
|
) do
|
||||||
|
with %Actor{} <- Actors.get_actor(actor_id),
|
||||||
|
{:ok, %Member{url: member_url, id: member_id} = member} <-
|
||||||
|
Actors.delete_member(member),
|
||||||
|
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||||
|
subject: "member_rejected"
|
||||||
|
),
|
||||||
|
accept_data <- %{
|
||||||
|
"type" => "Reject",
|
||||||
|
"actor" => actor_url,
|
||||||
|
"attributedTo" => member.parent.url,
|
||||||
|
"to" => [member.parent.members_url],
|
||||||
|
"cc" => [member.parent.url],
|
||||||
|
"object" => member_url,
|
||||||
|
"id" => "#{Endpoint.url()}/reject/member/#{member_id}"
|
||||||
|
} do
|
||||||
|
{:ok, member, accept_data}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,23 +21,25 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Remove do
|
||||||
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) ::
|
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) ::
|
||||||
{:ok, Activity.t(), Member.t()} | {:error, :member_not_found | Ecto.Changeset.t()}
|
{:ok, Activity.t(), Member.t()} | {:error, :member_not_found | Ecto.Changeset.t()}
|
||||||
def remove(
|
def remove(
|
||||||
%Member{} = member,
|
%Member{id: member_id},
|
||||||
%Actor{type: :Group, url: group_url, members_url: group_members_url},
|
%Actor{type: :Group, url: group_url, members_url: group_members_url},
|
||||||
%Actor{url: moderator_url} = moderator,
|
%Actor{url: moderator_url} = moderator,
|
||||||
local,
|
local,
|
||||||
_additional \\ %{}
|
_additional \\ %{}
|
||||||
) do
|
) do
|
||||||
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
|
with %Member{actor: %Actor{url: actor_url}} = member <- Actors.get_member(member_id),
|
||||||
%Member{} = member <- Actors.get_member(member_id) do
|
{:ok, %Member{}} <- Actors.delete_member(member) do
|
||||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||||
moderator: moderator,
|
moderator: moderator,
|
||||||
subject: "member_removed"
|
subject: "member_removed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Cachex.del(:activity_pub, "member_#{member_id}")
|
||||||
|
|
||||||
EmailMember.send_notification_to_removed_member(member)
|
EmailMember.send_notification_to_removed_member(member)
|
||||||
|
|
||||||
remove_data = %{
|
remove_data = %{
|
||||||
"to" => [group_members_url],
|
"to" => [actor_url, group_members_url],
|
||||||
"type" => "Remove",
|
"type" => "Remove",
|
||||||
"actor" => moderator_url,
|
"actor" => moderator_url,
|
||||||
"object" => member.url,
|
"object" => member.url,
|
||||||
|
|
|
@ -740,14 +740,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
) do
|
) do
|
||||||
Logger.info("Handle incoming to remove a member from a group")
|
Logger.info("Handle incoming to remove a member from a group")
|
||||||
|
|
||||||
with {:ok, %Actor{id: moderator_id} = moderator} <-
|
with {:ok, %Actor{} = moderator} <-
|
||||||
data |> Utils.get_actor() |> ActivityPubActor.get_or_fetch_actor_by_url(),
|
data |> Utils.get_actor() |> ActivityPubActor.get_or_fetch_actor_by_url(),
|
||||||
{:ok, person_id} <- get_remove_object(object),
|
{:ok, person_id} <- get_remove_object(object),
|
||||||
{:ok, %Actor{type: :Group, id: group_id} = group} <-
|
{:ok, %Actor{type: :Group, id: group_id} = group} <-
|
||||||
origin |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(),
|
origin |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(),
|
||||||
{:is_admin, {:ok, %Member{role: role}}}
|
{:is_admin, true} <-
|
||||||
when role in [:moderator, :administrator, :creator] <-
|
{:is_admin, can_remove_actor_from_group?(moderator, group)},
|
||||||
{:is_admin, Actors.get_member(moderator_id, group_id)},
|
|
||||||
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
|
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
|
||||||
{:is_member, Actors.get_member(person_id, group_id)} do
|
{:is_member, Actors.get_member(person_id, group_id)} do
|
||||||
Actions.Remove.remove(member, group, moderator, false)
|
Actions.Remove.remove(member, group, moderator, false)
|
||||||
|
@ -866,6 +865,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
{:error, _err} ->
|
{:error, _err} ->
|
||||||
case get_member(join_object) do
|
case get_member(join_object) do
|
||||||
|
{:ok, %Member{role: :not_approved} = member} ->
|
||||||
|
do_handle_incoming_accept_join_group(member, :member, %{moderator: actor_accepting})
|
||||||
|
|
||||||
{:ok, %Member{invited_by: nil} = member} ->
|
{:ok, %Member{invited_by: nil} = member} ->
|
||||||
do_handle_incoming_accept_join_group(member, :join)
|
do_handle_incoming_accept_join_group(member, :join)
|
||||||
|
|
||||||
|
@ -922,15 +924,17 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
defp do_handle_incoming_accept_join_group(
|
defp do_handle_incoming_accept_join_group(
|
||||||
%Member{role: role, parent: _group} = member,
|
%Member{role: role, parent: _group} = member,
|
||||||
type
|
type,
|
||||||
|
additional \\ %{}
|
||||||
)
|
)
|
||||||
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do
|
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite, :member] do
|
||||||
# Or maybe for groups it's the group that sends the Accept activity
|
# Or maybe for groups it's the group that sends the Accept activity
|
||||||
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
|
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
|
||||||
Actions.Accept.accept(
|
Actions.Accept.accept(
|
||||||
type,
|
type,
|
||||||
member,
|
member,
|
||||||
false
|
false,
|
||||||
|
additional
|
||||||
) do
|
) do
|
||||||
{:ok, activity, member}
|
{:ok, activity, member}
|
||||||
end
|
end
|
||||||
|
@ -1194,4 +1198,17 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
Actions.Create.create(:discussion, object_data, false)
|
Actions.Create.create(:discussion, object_data, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_remove_actor_from_group?(Actor.t(), Actor.t()) :: boolean()
|
||||||
|
defp can_remove_actor_from_group?(%Actor{} = moderator, %Actor{} = group) do
|
||||||
|
case Actors.get_member(moderator.id, group.id) do
|
||||||
|
{:ok, %Member{role: role}} when role in [:moderator, :administrator, :creator] ->
|
||||||
|
true
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# If member moderator not found, it's probably because no one on this instance is member of this group yet
|
||||||
|
# Therefore we can't access the list of admin/moderators and we just trust the origin domain
|
||||||
|
moderator.domain == group.domain
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -151,7 +151,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||||
}) do
|
}) do
|
||||||
{:ok, %Member{} = member} ->
|
{:ok, %Member{} = member} ->
|
||||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined")
|
subject =
|
||||||
|
case Mobilizon.Actors.get_default_member_role(group) do
|
||||||
|
:not_approved -> "member_request"
|
||||||
|
:member -> "member_joined"
|
||||||
|
end
|
||||||
|
|
||||||
|
Mobilizon.Service.Activity.Member.insert_activity(member, subject: subject)
|
||||||
|
|
||||||
Absinthe.Subscription.publish(Endpoint, actor,
|
Absinthe.Subscription.publish(Endpoint, actor,
|
||||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
|
group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
|
||||||
|
|
|
@ -141,6 +141,37 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def approve_member(_parent, %{member_id: member_id}, %{
|
||||||
|
context: %{current_actor: %Actor{} = moderator}
|
||||||
|
}) do
|
||||||
|
case Actors.get_member(member_id) do
|
||||||
|
%Member{} = member ->
|
||||||
|
with {:ok, _activity, %Member{} = member} <-
|
||||||
|
Actions.Accept.accept(:member, member, true, %{moderator: moderator}) do
|
||||||
|
{:ok, member}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, :member_not_found} ->
|
||||||
|
{:error, dgettext("errors", "You are not a moderator or admin for this group")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO : Maybe remove me ? Remove member with exclude parameter does the same
|
||||||
|
def reject_member(_parent, %{member_id: member_id}, %{
|
||||||
|
context: %{current_actor: %Actor{} = moderator}
|
||||||
|
}) do
|
||||||
|
case Actors.get_member(member_id) do
|
||||||
|
%Member{} = member ->
|
||||||
|
with {:ok, _activity, %Member{} = member} <-
|
||||||
|
Actions.Reject.reject(:member, member, true, %{moderator: moderator}) do
|
||||||
|
{:ok, member}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, :member_not_found} ->
|
||||||
|
{:error, dgettext("errors", "You are not a moderator or admin for this group")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec update_member(any(), map(), Absinthe.Resolution.t()) ::
|
@spec update_member(any(), map(), Absinthe.Resolution.t()) ::
|
||||||
{:ok, Member.t()} | {:error, String.t()}
|
{:ok, Member.t()} | {:error, String.t()}
|
||||||
def update_member(_parent, %{member_id: member_id, role: role}, %{
|
def update_member(_parent, %{member_id: member_id, role: role}, %{
|
||||||
|
@ -168,18 +199,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||||
|
|
||||||
@spec remove_member(any(), map(), Absinthe.Resolution.t()) ::
|
@spec remove_member(any(), map(), Absinthe.Resolution.t()) ::
|
||||||
{:ok, Member.t()} | {:error, String.t()}
|
{:ok, Member.t()} | {:error, String.t()}
|
||||||
def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{
|
def remove_member(_parent, %{member_id: member_id, exclude: _exclude}, %{
|
||||||
context: %{current_actor: %Actor{id: moderator_id} = moderator}
|
context: %{current_actor: %Actor{id: moderator_id} = moderator}
|
||||||
}) do
|
}) do
|
||||||
with %Member{role: role} = member when role != :rejected <- Actors.get_member(member_id),
|
case Actors.get_member(member_id) do
|
||||||
%Actor{type: :Group} = group <- Actors.get_actor(group_id),
|
nil ->
|
||||||
{:has_rights_to_remove, {:ok, %Member{role: role}}}
|
{:error,
|
||||||
when role in [:moderator, :administrator, :creator] <-
|
dgettext(
|
||||||
{:has_rights_to_remove, Actors.get_member(moderator_id, group_id)},
|
"errors",
|
||||||
{:ok, _activity, %Member{}} <-
|
"This member does not exist"
|
||||||
Actions.Remove.remove(member, group, moderator, true) do
|
)}
|
||||||
{:ok, member}
|
|
||||||
else
|
|
||||||
%Member{role: :rejected} ->
|
%Member{role: :rejected} ->
|
||||||
{:error,
|
{:error,
|
||||||
dgettext(
|
dgettext(
|
||||||
|
@ -187,15 +217,41 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||||
"This member already has been rejected."
|
"This member already has been rejected."
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{:has_rights_to_remove, _} ->
|
%Member{parent_id: group_id} = member ->
|
||||||
{:error,
|
case Actors.get_member(moderator_id, group_id) do
|
||||||
dgettext(
|
{:ok, %Member{role: role}} when role in [:moderator, :administrator, :creator] ->
|
||||||
"errors",
|
%Actor{type: :Group} = group = Actors.get_actor(group_id)
|
||||||
"You don't have the right to remove this member."
|
|
||||||
)}
|
with {:ok, _activity, %Member{}} <-
|
||||||
|
Actions.Remove.remove(member, group, moderator, true) do
|
||||||
|
{:ok, member}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %Member{}} ->
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"You don't have the role needed to remove this member."
|
||||||
|
)}
|
||||||
|
|
||||||
|
{:error, :member_not_found} ->
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"You don't have the right to remove this member."
|
||||||
|
)}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_member(_parent, _args, _resolution),
|
||||||
|
do:
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"You must be logged-in to remove a member"
|
||||||
|
)}
|
||||||
|
|
||||||
# Rejected members can be invited again
|
# Rejected members can be invited again
|
||||||
@spec check_member_not_existant_or_rejected(String.t() | integer, String.t() | integer()) ::
|
@spec check_member_not_existant_or_rejected(String.t() | integer, String.t() | integer()) ::
|
||||||
boolean()
|
boolean()
|
||||||
|
|
|
@ -81,6 +81,24 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
|
||||||
resolve(&Member.reject_invitation/3)
|
resolve(&Member.reject_invitation/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc """
|
||||||
|
Approve a membership request
|
||||||
|
"""
|
||||||
|
field :approve_member, :member do
|
||||||
|
arg(:member_id, non_null(:id), description: "The member ID")
|
||||||
|
|
||||||
|
resolve(&Member.approve_member/3)
|
||||||
|
end
|
||||||
|
|
||||||
|
@desc """
|
||||||
|
Reject a membership request
|
||||||
|
"""
|
||||||
|
field :reject_member, :member do
|
||||||
|
arg(:member_id, non_null(:id), description: "The member ID")
|
||||||
|
|
||||||
|
resolve(&Member.reject_member/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
Update a member's role
|
Update a member's role
|
||||||
"""
|
"""
|
||||||
|
@ -93,9 +111,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
|
||||||
|
|
||||||
@desc "Remove a member from a group"
|
@desc "Remove a member from a group"
|
||||||
field :remove_member, :member do
|
field :remove_member, :member do
|
||||||
arg(:group_id, non_null(:id), description: "The group ID")
|
|
||||||
arg(:member_id, non_null(:id), description: "The member ID")
|
arg(:member_id, non_null(:id), description: "The member ID")
|
||||||
|
|
||||||
|
arg(:exclude, :boolean,
|
||||||
|
default_value: false,
|
||||||
|
description: "Whether the member should be excluded from the group"
|
||||||
|
)
|
||||||
|
|
||||||
resolve(&Member.remove_member/3)
|
resolve(&Member.remove_member/3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,6 +50,14 @@ defmodule Mobilizon.Service.Activity.Renderer.Member do
|
||||||
dgettext("activity", "%{profile} added the member %{member}.", args)
|
dgettext("activity", "%{profile} added the member %{member}.", args)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp text(:member_approved, args) do
|
||||||
|
dgettext("activity", "%{profile} approved the membership request from %{member}.", args)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp text(:member_rejected, args) do
|
||||||
|
dgettext("activity", "%{profile} rejected the membership request from %{member}.", args)
|
||||||
|
end
|
||||||
|
|
||||||
defp text(:member_updated, args) do
|
defp text(:member_updated, args) do
|
||||||
dgettext("activity", "%{profile} updated the member %{member}.", args)
|
dgettext("activity", "%{profile} updated the member %{member}.", args)
|
||||||
end
|
end
|
||||||
|
|
|
@ -247,7 +247,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
|
||||||
:direct
|
:direct
|
||||||
|
|
||||||
:one_day ->
|
:one_day ->
|
||||||
calculate_next_day_notification(Date.utc_today(), timezone)
|
calculate_next_day_notification(Date.utc_today(), timezone: timezone)
|
||||||
|
|
||||||
:one_hour ->
|
:one_hour ->
|
||||||
DateTime.utc_now()
|
DateTime.utc_now()
|
||||||
|
|
|
@ -46,13 +46,65 @@ defmodule Mobilizon.Web.Email.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Only send notification to local members
|
||||||
|
def send_notification_to_approved_member(%Member{actor: %Actor{user_id: nil}}), do: :ok
|
||||||
|
|
||||||
|
def send_notification_to_approved_member(%Member{
|
||||||
|
actor: %Actor{user_id: user_id},
|
||||||
|
parent: %Actor{} = group
|
||||||
|
}) do
|
||||||
|
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
|
||||||
|
Gettext.put_locale(locale)
|
||||||
|
|
||||||
|
subject =
|
||||||
|
gettext(
|
||||||
|
"Your membership request for group %{group} has been approved",
|
||||||
|
group: Actor.display_name(group)
|
||||||
|
)
|
||||||
|
|
||||||
|
Email.base_email(to: email, subject: subject)
|
||||||
|
|> assign(:locale, locale)
|
||||||
|
|> assign(:group, group)
|
||||||
|
|> assign(:subject, subject)
|
||||||
|
|> render(:group_membership_approval)
|
||||||
|
|> Email.Mailer.send_email_later()
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Only send notification to local members
|
# 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: nil}}), do: :ok
|
||||||
|
|
||||||
|
# Member rejection
|
||||||
def send_notification_to_removed_member(%Member{
|
def send_notification_to_removed_member(%Member{
|
||||||
actor: %Actor{user_id: user_id},
|
actor: %Actor{user_id: user_id},
|
||||||
parent: %Actor{} = group,
|
parent: %Actor{} = group,
|
||||||
role: :rejected
|
role: :not_approved
|
||||||
|
}) do
|
||||||
|
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
|
||||||
|
Gettext.put_locale(locale)
|
||||||
|
|
||||||
|
subject =
|
||||||
|
gettext(
|
||||||
|
"Your membership request for group %{group} has been rejected",
|
||||||
|
group: Actor.display_name(group)
|
||||||
|
)
|
||||||
|
|
||||||
|
Email.base_email(to: email, subject: subject)
|
||||||
|
|> assign(:locale, locale)
|
||||||
|
|> assign(:group, group)
|
||||||
|
|> assign(:subject, subject)
|
||||||
|
|> render(:group_membership_rejection)
|
||||||
|
|> Email.Mailer.send_email_later()
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_notification_to_removed_member(%Member{
|
||||||
|
actor: %Actor{user_id: user_id},
|
||||||
|
parent: %Actor{} = group
|
||||||
}) do
|
}) do
|
||||||
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
|
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
|
||||||
Gettext.put_locale(locale)
|
Gettext.put_locale(locale)
|
||||||
|
@ -60,7 +112,7 @@ defmodule Mobilizon.Web.Email.Member do
|
||||||
subject =
|
subject =
|
||||||
gettext(
|
gettext(
|
||||||
"You have been removed from group %{group}",
|
"You have been removed from group %{group}",
|
||||||
group: group.name
|
group: Actor.display_name(group)
|
||||||
)
|
)
|
||||||
|
|
||||||
Email.base_email(to: email, subject: subject)
|
Email.base_email(to: email, subject: subject)
|
||||||
|
|
69
lib/web/templates/email/group_membership_approval.html.heex
Normal file
69
lib/web/templates/email/group_membership_approval.html.heex
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<!-- 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 "You're in!" %>
|
||||||
|
</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("Your membership to group %{link_start}<b>%{group}</b>%{link_end} has been approved.", group: Mobilizon.Actors.Actor.display_name(@group), link_start: "<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(@group))}\">", link_end: "</a>") |> raw %>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- BULLETPROOF BUTTON -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||||
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
|
||||||
|
<a href={"#{ Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(@group)) }"} target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
|
||||||
|
<%= gettext "View the group" %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<%= gettext "You're in!" %>
|
||||||
|
==
|
||||||
|
<%= gettext "Your membership to group %{group} has been approved.", group: Mobilizon.Actors.Actor.display_name(@group) %>
|
||||||
|
|
||||||
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(@group)) %>
|
49
lib/web/templates/email/group_membership_rejection.html.heex
Normal file
49
lib/web/templates/email/group_membership_rejection.html.heex
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<!-- 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 "Sorry, not this time!" %>
|
||||||
|
</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 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("Your membership request to join group %{link_start}<b>%{group}</b>%{link_end} has been rejected.", group: Mobilizon.Actors.Actor.display_name(@group), link_start: "<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(@group))}\">", link_end: "</a>") |> raw %>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<%= gettext "Sorry, not this time!" %>
|
||||||
|
==
|
||||||
|
<%= gettext "Your membership request to join group %{group} has been rejected.", group: Mobilizon.Actors.Actor.display_name(@group) %>
|
||||||
|
<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(@group)) %>
|
|
@ -203,7 +203,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
|
||||||
{:ok, activity, _member} = Actions.Remove.remove(member, group, moderator, true)
|
{:ok, activity, _member} = Actions.Remove.remove(member, group, moderator, true)
|
||||||
assert activity.data["type"] == "Remove"
|
assert activity.data["type"] == "Remove"
|
||||||
assert activity.data["actor"] == moderator.url
|
assert activity.data["actor"] == moderator.url
|
||||||
assert activity.data["to"] == [group.members_url]
|
assert activity.data["to"] == [member.actor.url, group.members_url]
|
||||||
assert activity.data["object"] == member.url
|
assert activity.data["object"] == member.url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -524,6 +524,93 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Member resolver to remove a member from a group" do
|
describe "Member resolver to remove a member from a group" do
|
||||||
# TODO write tests for me plz
|
@remove_member_mutation """
|
||||||
|
mutation RemoveMember($memberId: ID!) {
|
||||||
|
removeMember(memberId: $memberId) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
setup %{conn: conn, actor: actor, user: user} do
|
||||||
|
group = insert(:group)
|
||||||
|
target_actor = insert(:actor, user: user)
|
||||||
|
|
||||||
|
{:ok, conn: conn, actor: actor, user: user, group: group, target_actor: target_actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_member/3 fails when not connected", %{
|
||||||
|
conn: conn,
|
||||||
|
group: group,
|
||||||
|
target_actor: target_actor
|
||||||
|
} do
|
||||||
|
%Member{id: member_id} =
|
||||||
|
insert(:member, %{actor: target_actor, parent: group, role: :member})
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @remove_member_mutation,
|
||||||
|
variables: %{
|
||||||
|
memberId: member_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hd(res["errors"])["message"] == "You must be logged-in to remove a member"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_member/3 fails when not a member of the group", %{
|
||||||
|
conn: conn,
|
||||||
|
group: group,
|
||||||
|
target_actor: target_actor
|
||||||
|
} do
|
||||||
|
user = insert(:user)
|
||||||
|
actor = insert(:actor, user: user)
|
||||||
|
Mobilizon.Users.update_user_default_actor(user, actor)
|
||||||
|
|
||||||
|
%Member{id: member_id} =
|
||||||
|
insert(:member, %{actor: target_actor, parent: group, role: :member})
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @remove_member_mutation,
|
||||||
|
variables: %{
|
||||||
|
memberId: member_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hd(res["errors"])["message"] == "You don't have the right to remove this member."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_member/3 removes a member", %{
|
||||||
|
conn: conn,
|
||||||
|
user: user,
|
||||||
|
actor: actor,
|
||||||
|
group: group,
|
||||||
|
target_actor: target_actor
|
||||||
|
} do
|
||||||
|
Mobilizon.Users.update_user_default_actor(user, actor)
|
||||||
|
insert(:member, actor: actor, parent: group, role: :administrator)
|
||||||
|
|
||||||
|
%Member{id: member_id} =
|
||||||
|
insert(:member, %{actor: target_actor, parent: group, role: :member})
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @remove_member_mutation,
|
||||||
|
variables: %{
|
||||||
|
memberId: member_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["errors"] == nil
|
||||||
|
assert res["data"]["removeMember"]["id"] == member_id
|
||||||
|
|
||||||
|
assert Mobilizon.Actors.get_member(member_id) == nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue