Allow group admins to moderate new members

Closes #881

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2021-11-12 15:42:52 +01:00
parent ae24fa17d5
commit 6eba531c89
28 changed files with 795 additions and 212 deletions

View File

@@ -148,6 +148,11 @@ export default class GroupActivityItem extends mixins(ActivityMixin) {
case Openness.INVITE_ONLY:
details.push("The group can now only be joined with an invite.");
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:
details.push("The group can now be joined by anyone.");
break;

View File

@@ -9,13 +9,7 @@
:inline="true"
slot="member"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.object.actor),
})
}}</b
></popover-actor-card
<b> {{ displayName(activity.object.actor) }}</b></popover-actor-card
>
<b slot="member" v-else>{{
subjectParams.member_actor_federated_username
@@ -25,13 +19,7 @@
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
<b> {{ displayName(activity.author) }}</b></popover-actor-card
></i18n
>
<small class="has-text-grey-dark activity-date">{{
@@ -41,7 +29,7 @@
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { displayName } from "@/types/actor";
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
@@ -62,7 +50,7 @@ export const MEMBER_ROLE_VALUE: Record<string, number> = {
},
})
export default class MemberActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
displayName = displayName;
RouteName = RouteName;
ActivityMemberSubject = ActivityMemberSubject;
@@ -83,6 +71,14 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
return "You 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:
return "{member} joined the group.";
case ActivityMemberSubject.MEMBER_UPDATED:
@@ -94,6 +90,12 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
}
return "{profile} updated the member {member}.";
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) {
return "You excluded member {member}.";
}

View File

@@ -1,57 +1,65 @@
<template>
<div class="media">
<div class="media-content">
<div class="content">
<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">
<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 class="card">
<div class="card-content media">
<div class="media-content">
<div class="content">
<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-content">
<div class="level">
<div class="level-left">
<div class="level-item">
<router-link
:to="{
name: RouteName.GROUP,
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="member.parent.domain">
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
<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 class="media-content">
<div class="level">
<div class="level-left">
<div class="level-item mr-3">
<router-link
:to="{
name: RouteName.GROUP,
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3 class="is-size-5">{{ member.parent.name }}</h3>
<p class="is-size-7 has-text-grey-dark">
<span v-if="member.parent.domain">
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<b-button type="is-success" @click="$emit('accept', member.id)">
{{ $t("Accept") }}
</b-button>
</div>
<div class="level-item">
<b-button type="is-danger" @click="$emit('reject', member.id)">
{{ $t("Decline") }}
</b-button>
<div class="level-right">
<div class="level-item">
<b-button
type="is-success"
@click="$emit('accept', member.id)"
>
{{ $t("Accept") }}
</b-button>
</div>
<div class="level-item">
<b-button
type="is-danger"
@click="$emit('reject', member.id)"
>
{{ $t("Decline") }}
</b-button>
</div>
</div>
</div>
</div>
@@ -82,4 +90,7 @@ export default class InvitationCard extends Vue {
background: lighten($primary, 40%);
padding: 10px;
}
h3 {
color: $violet-3;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<section v-if="invitations && invitations.length > 0">
<section class="card my-3" v-if="invitations && invitations.length > 0">
<InvitationCard
v-for="member in invitations"
:key="member.id"
@@ -13,8 +13,9 @@
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { Component, Prop, Vue } from "vue-property-decorator";
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 { IGroup, IPerson, usernameWithDomain } from "@/types/actor";
@Component({
components: {
@@ -26,18 +27,25 @@ export default class Invitations extends Vue {
async acceptInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
{
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("accept-invitation", data.acceptInvitation);
}
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries({ data }) {
const profile = data?.acceptInvitation?.actor as IPerson;
const group = data?.acceptInvitation?.parent as IGroup;
if (profile && group) {
return [
{
query: PERSON_STATUS_GROUP,
variables: { id: profile.id, group: usernameWithDomain(group) },
},
];
}
return [];
},
});
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -48,18 +56,25 @@ export default class Invitations extends Vue {
async rejectInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
{
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("reject-invitation", data.rejectInvitation);
}
await this.$apollo.mutate<{ rejectInvitation: IMember }>({
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries({ data }) {
const profile = data?.rejectInvitation?.actor as IPerson;
const group = data?.rejectInvitation?.parent as IGroup;
if (profile && group) {
return [
{
query: PERSON_STATUS_GROUP,
variables: { id: profile.id, group: usernameWithDomain(group) },
},
];
}
return [];
},
});
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {