Various group-related improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-06-16 11:25:53 +02:00
parent 6cc233a6d3
commit f8e73ca990
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
9 changed files with 197 additions and 112 deletions

View File

@ -98,10 +98,9 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title { .discussion-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, font-family: Roboto, Helvetica, Arial, serif;
Arial, serif; font-size: 16px;
font-size: 1.25rem; font-weight: 500;
font-weight: 700;
flex: 1; flex: 1;
} }
} }

View File

@ -43,10 +43,9 @@ export default class PostListItem extends Vue {
.post-minimalist-title { .post-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, font-family: Roboto, Helvetica, Arial, serif;
serif; font-size: 16px;
font-size: 1rem; font-weight: 500;
font-weight: 700;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View File

@ -1046,6 +1046,6 @@
"Move resource to the root folder": "Move resource to the root folder", "Move resource to the root folder": "Move resource to the root folder",
"Share this group": "Share this group", "Share this group": "Share this group",
"This group is accessible only through it's link. Be careful where you post this link.": "This group is accessible only through it's link. Be careful where you post this link.", "This group is accessible only through it's link. Be careful where you post this link.": "This group is accessible only through it's link. Be careful where you post this link.",
"{count} members": "{count} members", "{count} members": "No members|One member|{count} members",
"Share": "Share" "Share": "Share"
} }

View File

@ -1137,6 +1137,6 @@
"Move resource to the root folder": "Déplacer la resource dans le dossier racine", "Move resource to the root folder": "Déplacer la resource dans le dossier racine",
"Share this group": "Partager ce groupe", "Share this group": "Partager ce groupe",
"This group is accessible only through it's link. Be careful where you post this link.": "Ce groupe est accessible uniquement à travers son lien. Faites attention où vous le diffusez.", "This group is accessible only through it's link. Be careful where you post this link.": "Ce groupe est accessible uniquement à travers son lien. Faites attention où vous le diffusez.",
"{count} members": "{count} membres", "{count} members": "Aucun membre|Un⋅e membre|{count} membres",
"Share": "Partager" "Share": "Partager"
} }

View File

@ -5,13 +5,7 @@ import {
} from "@/graphql/actor"; } from "@/graphql/actor";
import { FETCH_GROUP } from "@/graphql/group"; import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
Group,
IActor,
IGroup,
IPerson,
usernameWithDomain,
} from "@/types/actor";
import { MemberRole } from "@/types/enums"; import { MemberRole } from "@/types/enums";
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
@ -50,14 +44,14 @@ const now = new Date();
variables() { variables() {
return { return {
actorId: this.currentActor.id, actorId: this.currentActor.id,
group: this.group.preferredUsername, group: this.group?.preferredUsername,
}; };
}, },
skip() { skip() {
return ( return (
!this.currentActor || !this.currentActor ||
!this.currentActor.id || !this.currentActor.id ||
!this.group.preferredUsername !this.group?.preferredUsername
); );
}, },
}, },
@ -65,7 +59,7 @@ const now = new Date();
return ( return (
!this.currentActor || !this.currentActor ||
!this.currentActor.id || !this.currentActor.id ||
!this.group.preferredUsername !this.group?.preferredUsername
); );
}, },
}, },
@ -73,7 +67,7 @@ const now = new Date();
}, },
}) })
export default class GroupMixin extends Vue { export default class GroupMixin extends Vue {
group: IGroup = new Group(); group!: IGroup;
currentActor!: IActor; currentActor!: IActor;

View File

@ -57,7 +57,8 @@ export class Actor implements IActor {
} }
export function usernameWithDomain(actor: IActor, force = false): string { export function usernameWithDomain(actor: IActor, force = false): string {
if (actor.domain) { if (!actor) return "";
if (actor?.domain) {
return `${actor.preferredUsername}@${actor.domain}`; return `${actor.preferredUsername}@${actor.domain}`;
} }
if (force) { if (force) {

View File

@ -10,7 +10,7 @@
</li> </li>
<li class="is-active"> <li class="is-active">
<router-link <router-link
v-if="group.preferredUsername" v-if="group && group.preferredUsername"
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
@ -44,9 +44,9 @@
) )
}} }}
</b-message> </b-message>
<header class="block-container presentation"> <header class="block-container presentation" v-if="group">
<div class="banner-container"> <div class="banner-container">
<lazy-image-wrapper :picture="group.picture" /> <lazy-image-wrapper :picture="group.banner" />
</div> </div>
<div class="header"> <div class="header">
<div class="avatar-container"> <div class="avatar-container">
@ -65,7 +65,10 @@
<br /> <br />
</div> </div>
<div class="group-metadata"> <div class="group-metadata">
<div class="block-column members" v-if="isCurrentActorAGroupMember"> <div
class="block-column members"
v-if="isCurrentActorAGroupMember && !previewPublic"
>
<div> <div>
<figure <figure
class="image is-32x32" class="image is-32x32"
@ -88,7 +91,11 @@
</figure> </figure>
</div> </div>
<p> <p>
{{ $t("{count} members", { count: group.members.total }) }} {{
$tc("{count} members", group.members.total, {
count: group.members.total,
})
}}
<router-link <router-link
v-if="isCurrentActorAGroupAdmin" v-if="isCurrentActorAGroupAdmin"
:to="{ :to="{
@ -188,7 +195,7 @@
<b-button <b-button
outlined outlined
icon-left="timeline-text" icon-left="timeline-text"
v-if="isCurrentActorAGroupMember" v-if="isCurrentActorAGroupMember && !previewPublic"
tag="router-link" tag="router-link"
:to="{ :to="{
name: RouteName.TIMELINE, name: RouteName.TIMELINE,
@ -199,7 +206,7 @@
<b-button <b-button
outlined outlined
icon-left="cog" icon-left="cog"
v-if="isCurrentActorAGroupAdmin" v-if="isCurrentActorAGroupAdmin && !previewPublic"
tag="router-link" tag="router-link"
:to="{ :to="{
name: RouteName.GROUP_PUBLIC_SETTINGS, name: RouteName.GROUP_PUBLIC_SETTINGS,
@ -207,17 +214,50 @@
}" }"
>{{ $t("Group settings") }}</b-button >{{ $t("Group settings") }}</b-button
> >
<b-tooltip
v-if="
(!isCurrentActorAGroupMember || previewPublic) &&
group.openness !== Openness.OPEN
"
:label="$t('This group is invite-only')"
position="is-bottom"
>
<b-button disabled type="is-primary">{{
$t("Join group")
}}</b-button></b-tooltip
>
<b-button
v-else-if="
(!isCurrentActorAGroupMember || previewPublic) &&
currentActor.id
"
@click="joinGroup"
type="is-primary"
:disabled="previewPublic"
>{{ $t("Join group") }}</b-button
>
<b-button
tag="router-link"
:to="{
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(group) },
}"
v-else-if="!isCurrentActorAGroupMember || previewPublic"
:disabled="previewPublic"
type="is-primary"
>{{ $t("Join group") }}</b-button
>
<b-button <b-button
outlined outlined
icon-left="share" icon-left="share"
@click="triggerShare()" @click="triggerShare()"
v-if="!isCurrentActorAGroupMember" v-if="!isCurrentActorAGroupMember || previewPublic"
> >
{{ $t("Share") }} {{ $t("Share") }}
</b-button> </b-button>
<b-dropdown <b-dropdown
class="menu-dropdown" class="menu-dropdown"
v-if="isCurrentActorAGroupMember" v-if="isCurrentActorAGroupMember || previewPublic"
position="is-bottom-left" position="is-bottom-left"
aria-role="menu" aria-role="menu"
> >
@ -228,13 +268,25 @@
icon-left="dots-horizontal" icon-left="dots-horizontal"
aria-label="Other actions" aria-label="Other actions"
/> />
<b-dropdown-item aria-role="menuitem" @click="triggerShare()"> <b-dropdown-item aria-role="menuitem">
<b-switch v-model="previewPublic">{{
$t("Public preview")
}}</b-switch>
</b-dropdown-item>
<b-dropdown-item
v-if="!previewPublic"
aria-role="menuitem"
@click="triggerShare()"
>
<span> <span>
<b-icon icon="share" /> <b-icon icon="share" />
{{ $t("Share") }} {{ $t("Share") }}
</span> </span>
</b-dropdown-item> </b-dropdown-item>
<hr class="dropdown-divider" /> <hr
class="dropdown-divider"
v-if="isCurrentActorAGroupMember"
/>
<b-dropdown-item has-link aria-role="menuitem"> <b-dropdown-item has-link aria-role="menuitem">
<a <a
:href="`@${preferredUsername}/feed/atom`" :href="`@${preferredUsername}/feed/atom`"
@ -255,8 +307,8 @@
</b-dropdown-item> </b-dropdown-item>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
<b-dropdown-item <b-dropdown-item
aria-role="menuitem"
v-if="ableToReport" v-if="ableToReport"
aria-role="menuitem"
@click="isReportModalActive = true" @click="isReportModalActive = true"
> >
<span> <span>
@ -266,7 +318,7 @@
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
aria-role="menuitem" aria-role="menuitem"
v-if="isCurrentActorAGroupMember" v-if="isCurrentActorAGroupMember && !previewPublic"
@click="leaveGroup" @click="leaveGroup"
> >
<span> <span>
@ -280,7 +332,10 @@
</div> </div>
</header> </header>
</div> </div>
<div v-if="isCurrentActorAGroupMember" class="block-container"> <div
v-if="isCurrentActorAGroupMember && !previewPublic"
class="block-container"
>
<!-- Private things --> <!-- Private things -->
<div class="block-column"> <div class="block-column">
<!-- Group discussions --> <!-- Group discussions -->
@ -300,9 +355,9 @@
:discussion="discussion" :discussion="discussion"
/> />
</div> </div>
<div v-else class="content has-text-grey-dark has-text-centered"> <empty-content v-else icon="chat" :inline="true">
<p>{{ $t("No discussions yet") }}</p> {{ $t("No discussions yet") }}
</div> </empty-content>
</template> </template>
<template v-slot:create> <template v-slot:create>
<router-link <router-link
@ -343,12 +398,9 @@
/> />
</div> </div>
</div> </div>
<div <empty-content v-else icon="link" :inline="true">
v-else-if="group" {{ $t("No resources yet") }}
class="content has-text-grey-dark has-text-centered" </empty-content>
>
<p>{{ $t("No resources yet") }}</p>
</div>
</template> </template>
<template v-slot:create> <template v-slot:create>
<router-link <router-link
@ -386,12 +438,9 @@
class="organized-event" class="organized-event"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="calendar" :inline="true">
v-else-if="group" {{ $t("No public upcoming events") }}
class="content has-text-grey-dark has-text-centered" </empty-content>
>
<p>{{ $t("No public upcoming events") }}</p>
</div>
<b-skeleton animated v-else></b-skeleton> <b-skeleton animated v-else></b-skeleton>
</template> </template>
<template v-slot:create> <template v-slot:create>
@ -424,12 +473,9 @@
:post="post" :post="post"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="bullhorn" :inline="true">
v-else-if="group" {{ $t("No posts yet") }}
class="content has-text-grey-dark has-text-centered" </empty-content>
>
<p>{{ $t("No posts yet") }}</p>
</div>
</template> </template>
<template v-slot:create> <template v-slot:create>
<router-link <router-link
@ -448,10 +494,18 @@
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger"> <b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
{{ $t("No group found") }} {{ $t("No group found") }}
</b-message> </b-message>
<div v-else class="public-container"> <div v-else-if="group" class="public-container">
<aside class="group-metadata"> <aside class="group-metadata">
<div class="sticky"> <div class="sticky">
<event-metadata-block :title="$t('Members')" icon="account-group">
{{
$tc("{count} members", group.members.total, {
count: group.members.total,
})
}}
</event-metadata-block>
<event-metadata-block <event-metadata-block
v-if="physicalAddress"
:title="$t('Location')" :title="$t('Location')"
:icon=" :icon="
physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth' physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'
@ -493,12 +547,9 @@
v-html="group.summary" v-html="group.summary"
v-if="group.summary && group.summary !== '<p></p>'" v-if="group.summary && group.summary !== '<p></p>'"
/> />
<div <empty-content v-else-if="group" icon="image-text" :inline="true">
v-else-if="group" {{ $t("This group doesn't have a description yet.") }}
class="content has-text-grey-dark has-text-centered" </empty-content>
>
<p>{{ $t("This group doesn't have a description yet.") }}</p>
</div>
</section> </section>
<section> <section>
<subtitle>{{ $t("Upcoming events") }}</subtitle> <subtitle>{{ $t("Upcoming events") }}</subtitle>
@ -513,18 +564,9 @@
class="organized-event" class="organized-event"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="calendar" :inline="true">
v-else-if="group && group.organizedEvents.elements.length == 0" {{ $t("No public upcoming events") }}
class="content has-text-grey-dark has-text-centered" </empty-content>
>
<p>{{ $t("No public upcoming events") }}</p>
</div>
<div
v-else-if="group"
class="content has-text-grey-dark has-text-centered"
>
<p>{{ $t("No public upcoming events") }}</p>
</div>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <router-link
v-if="group.organizedEvents.total > 0" v-if="group.organizedEvents.total > 0"
@ -545,12 +587,9 @@
:post="post" :post="post"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="bullhorn" :inline="true">
v-else-if="group" {{ $t("No posts yet") }}
class="content has-text-grey-dark has-text-centered" </empty-content>
>
<p>{{ $t("No posts yet") }}</p>
</div>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <router-link
v-if="group.posts.total > 0" v-if="group.posts.total > 0"
@ -581,6 +620,7 @@
:active.sync="isReportModalActive" :active.sync="isReportModalActive"
has-modal-card has-modal-card
ref="reportModal" ref="reportModal"
v-if="group"
> >
<report-modal <report-modal
:on-confirm="reportGroup" :on-confirm="reportGroup"
@ -589,7 +629,12 @@
@close="$refs.reportModal.close()" @close="$refs.reportModal.close()"
/> />
</b-modal> </b-modal>
<b-modal :active.sync="isShareModalActive" has-modal-card ref="shareModal"> <b-modal
v-if="group"
:active.sync="isShareModalActive"
has-modal-card
ref="shareModal"
>
<share-group-modal :group="group" /> <share-group-modal :group="group" />
</b-modal> </b-modal>
</div> </div>
@ -625,6 +670,7 @@ import { PERSON_MEMBERSHIP_GROUP } from "@/graphql/actor";
import { LEAVE_GROUP } from "@/graphql/group"; import { LEAVE_GROUP } from "@/graphql/group";
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue"; import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue"; import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
@Component({ @Component({
apollo: { apollo: {
@ -644,6 +690,7 @@ import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
ReportModal, ReportModal,
LazyImageWrapper, LazyImageWrapper,
EventMetadataBlock, EventMetadataBlock,
EmptyContent,
"map-leaflet": () => "map-leaflet": () =>
import(/* webpackChunkName: "map" */ "../../components/Map.vue"), import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
ShareGroupModal: () => ShareGroupModal: () =>
@ -683,6 +730,8 @@ export default class Group extends mixins(GroupMixin) {
isShareModalActive = false; isShareModalActive = false;
previewPublic = false;
@Watch("currentActor") @Watch("currentActor")
watchCurrentActor(currentActor: IActor, oldActor: IActor): void { watchCurrentActor(currentActor: IActor, oldActor: IActor): void {
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) { if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
@ -815,13 +864,11 @@ export default class Group extends mixins(GroupMixin) {
} }
get groupTitle(): undefined | string { get groupTitle(): undefined | string {
if (!this.group) return undefined; return this.group?.name || this.group?.preferredUsername;
return this.group.name || this.group.preferredUsername;
} }
get groupSummary(): undefined | string { get groupSummary(): undefined | string {
if (!this.group) return undefined; return this.group?.summary;
return this.group.summary;
} }
get groupMember(): IMember | undefined { get groupMember(): IMember | undefined {
@ -918,6 +965,7 @@ export default class Group extends mixins(GroupMixin) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~bulma/sass/utilities/mixins.sass";
div.container { div.container {
margin-bottom: 3rem; margin-bottom: 3rem;
@ -937,13 +985,6 @@ div.container {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.public-container {
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
padding: 0;
}
.block-container { .block-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1025,15 +1066,19 @@ div.container {
.block-column { .block-column {
flex: 1; flex: 1;
margin: 0 0.5rem; margin: 0;
max-width: 576px; max-width: 576px;
@include desktop {
margin: 0 0.5rem;
&:first-child { &:first-child {
margin-left: 0; margin-left: 0;
} }
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
}
section { section {
background: $white; background: $white;
@ -1092,6 +1137,17 @@ div.container {
margin-top: 16px; margin-top: 16px;
align-items: flex-end; align-items: flex-end;
::v-deep .icon {
border-radius: 290486px;
border: 1px solid #cdcaea;
background: white;
height: 5rem;
width: 5rem;
i::before {
font-size: 60px;
}
}
figure { figure {
position: relative; position: relative;
@ -1122,6 +1178,10 @@ div.container {
& > .buttons { & > .buttons {
justify-content: center; justify-content: center;
::v-deep .b-tooltip {
padding-right: 0.5em;
}
} }
.members { .members {
@ -1143,11 +1203,19 @@ div.container {
} }
.public-container { .public-container {
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
padding: 0;
margin-top: 1rem;
.group-metadata { .group-metadata {
min-width: 20rem; min-width: 20rem;
flex: 1; flex: 1;
padding-left: 1rem; padding-left: 1rem;
margin: 2rem auto; @include mobile {
padding-left: 0;
}
.sticky { .sticky {
position: sticky; position: sticky;
@ -1158,14 +1226,28 @@ div.container {
} }
.main-content { .main-content {
min-width: 20rem; min-width: 20rem;
padding: 1rem;
flex: 2; flex: 2;
background: white; background: white;
margin: 2rem auto;
@include desktop {
padding: 10px;
}
@include mobile {
margin-top: 1rem;
}
h2 {
margin: 0 auto 10px;
}
} }
section { section {
margin-top: 2rem; margin-top: 0;
.posts-wrapper {
margin-bottom: 1rem;
}
} }
} }

View File

@ -4,11 +4,12 @@
<ul> <ul>
<li> <li>
<router-link <router-link
v-if="group"
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
}" }"
>{{ group.name }}</router-link >{{ group.name || usernameWithDomain(group) }}</router-link
> >
</li> </li>
<li> <li>
@ -78,7 +79,7 @@
<b-radio <b-radio
v-model="editableGroup.visibility" v-model="editableGroup.visibility"
name="groupVisibility" name="groupVisibility"
:native-value="GroupVisibility.PRIVATE" :native-value="GroupVisibility.UNLISTED"
>{{ $t("Only accessible through link") }}<br /> >{{ $t("Only accessible through link") }}<br />
<small>{{ <small>{{
$t( $t(
@ -186,6 +187,7 @@ import { IConfig } from "@/types/config.model";
import { ServerParseError } from "@apollo/client/link/http"; import { ServerParseError } from "@apollo/client/link/http";
import { ErrorResponse } from "@apollo/client/link/error"; import { ErrorResponse } from "@apollo/client/link/error";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { buildFileFromIMedia } from "@/utils/image";
@Component({ @Component({
components: { components: {
@ -274,7 +276,19 @@ export default class GroupSettings extends mixins(GroupMixin) {
} }
@Watch("group") @Watch("group")
async watchUpdateGroup(): Promise<void> { async watchUpdateGroup(oldGroup: IGroup, newGroup: IGroup): Promise<void> {
if (
oldGroup?.avatar !== undefined &&
oldGroup?.avatar !== newGroup?.avatar
) {
this.avatarFile = await buildFileFromIMedia(this.group.avatar);
}
if (
oldGroup?.banner !== undefined &&
oldGroup?.banner !== newGroup?.banner
) {
this.bannerFile = await buildFileFromIMedia(this.group.banner);
}
this.editableGroup = { ...this.group }; this.editableGroup = { ...this.group };
} }

View File

@ -86,7 +86,7 @@ import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_POSTS } from "../../graphql/post"; import { FETCH_GROUP_POSTS } from "../../graphql/post";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import { IGroup, IPerson, usernameWithDomain } from "../../types/actor"; import { usernameWithDomain } from "../../types/actor";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import PostElementItem from "../../components/Post/PostElementItem.vue"; import PostElementItem from "../../components/Post/PostElementItem.vue";
@ -138,14 +138,10 @@ const POSTS_PAGE_LIMIT = 10;
export default class PostList extends mixins(GroupMixin) { export default class PostList extends mixins(GroupMixin) {
@Prop({ required: true, type: String }) preferredUsername!: string; @Prop({ required: true, type: String }) preferredUsername!: string;
group!: IGroup;
posts!: Paginate<IPost>; posts!: Paginate<IPost>;
memberships!: IMember[]; memberships!: IMember[];
currentActor!: IPerson;
postsPage = 1; postsPage = 1;
RouteName = RouteName; RouteName = RouteName;