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

@@ -23,29 +23,6 @@
</ul>
</nav>
<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">
<div class="banner-container">
<lazy-image-wrapper :picture="group.banner" />
@@ -137,7 +114,7 @@
<b-tooltip
v-if="
(!isCurrentActorAGroupMember || previewPublic) &&
group.openness !== Openness.OPEN
group.openness === Openness.INVITE_ONLY
"
:label="$t('This group is invite-only')"
position="is-bottom"
@@ -148,7 +125,9 @@
>
<b-button
v-else-if="
(!isCurrentActorAGroupMember || previewPublic) &&
((!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember) ||
previewPublic) &&
currentActor.id
"
@click="joinGroup"
@@ -157,6 +136,14 @@
:disabled="previewPublic"
>{{ $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
tag="router-link"
:to="{
@@ -310,6 +297,49 @@
</b-dropdown>
</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>
</header>
</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> {
this.isReportModalActive = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -195,6 +195,20 @@
</b-table-column>
<b-table-column field="actions" :label="$t('Actions')" v-slot="props">
<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
v-if="
[MemberRole.MEMBER, MemberRole.MODERATOR].includes(
@@ -217,7 +231,7 @@
>
<b-button
v-if="props.row.role === MemberRole.MEMBER"
@click="removeMember(props.row.id)"
@click="removeMember(props.row)"
type="is-danger"
icon-left="exit-to-app"
>{{ $t("Remove") }}</b-button
@@ -250,8 +264,9 @@ import {
GROUP_MEMBERS,
REMOVE_MEMBER,
UPDATE_MEMBER,
APPROVE_MEMBER,
} from "../../graphql/member";
import { usernameWithDomain } from "../../types/actor";
import { usernameWithDomain, displayName } from "../../types/actor";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
@Component({
@@ -332,7 +347,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
this.$notifier.success(
this.$t("{username} was invited to {group}", {
username: this.newMemberUsername,
group: this.group.name || usernameWithDomain(this.group),
group: displayName(this.group),
}) as string
);
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 variables = {
name: usernameWithDomain(group),
@@ -388,7 +403,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
mutation: REMOVE_MEMBER,
variables: {
groupId: this.group.id,
memberId,
memberId: oldMember.id,
},
refetchQueries: [
{
@@ -397,12 +412,18 @@ export default class GroupMembers extends mixins(GroupMixin) {
},
],
});
this.$notifier.success(
this.$t("The member was removed from the group {group}", {
username: this.newMemberUsername,
group: this.group.name || usernameWithDomain(this.group),
}) as string
);
let message = this.$t("The member was removed from the group {group}", {
group: displayName(this.group),
}) as string;
if (oldMember.role === MemberRole.NOT_APPROVED) {
message = this.$t(
"The membership request from {profile} was rejected",
{
group: displayName(oldMember.actor),
}
) as string;
}
this.$notifier.success(message);
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -414,29 +435,49 @@ export default class GroupMembers extends mixins(GroupMixin) {
promoteMember(member: IMember): void {
if (!member.id) return;
if (member.role === MemberRole.MODERATOR) {
this.updateMember(member.id, MemberRole.ADMINISTRATOR);
this.updateMember(member, MemberRole.ADMINISTRATOR);
}
if (member.role === MemberRole.MEMBER) {
this.updateMember(member.id, MemberRole.MODERATOR);
this.updateMember(member, MemberRole.MODERATOR);
}
}
demoteMember(member: IMember): void {
if (!member.id) return;
if (member.role === MemberRole.MODERATOR) {
this.updateMember(member.id, MemberRole.MEMBER);
this.updateMember(member, MemberRole.MEMBER);
}
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 {
await this.$apollo.mutate<{ updateMember: IMember }>({
mutation: UPDATE_MEMBER,
variables: {
memberId,
memberId: oldMember.id,
role,
},
refetchQueries: [
@@ -455,8 +496,14 @@ export default class GroupMembers extends mixins(GroupMixin) {
successMessage = "The member role was updated to administrator";
break;
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:
successMessage = "The member role was updated to simple member";
successMessage = "The member role was updated";
}
this.$notifier.success(this.$t(successMessage) as string);
} catch (error: any) {

View File

@@ -128,6 +128,19 @@
}}</small>
</b-radio>
</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">
<b-radio
v-model="editableGroup.openness"