Improve group related UI
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
9639a066ff
commit
6cc233a6d3
48
js/src/components/Account/ActorInline.vue
Normal file
48
js/src/components/Account/ActorInline.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div class="actor-inline">
|
||||
<div class="actor-avatar">
|
||||
<figure class="image is-24x24" v-if="actor.avatar">
|
||||
<img class="is-rounded" :src="actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||
</div>
|
||||
|
||||
<div class="actor-name">
|
||||
<p>
|
||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { IActor, usernameWithDomain } from "../../types/actor";
|
||||
|
||||
@Component
|
||||
export default class ActorInline extends Vue {
|
||||
@Prop({ required: true, type: Object }) actor!: IActor;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div.actor-inline {
|
||||
align-items: flex-start;
|
||||
display: inline-flex;
|
||||
text-align: inherit;
|
||||
align-items: top;
|
||||
|
||||
div.actor-avatar {
|
||||
flex-basis: auto;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
div.actor-name {
|
||||
flex-basis: auto;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
text-align: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -41,7 +41,7 @@
|
|||
></popover-actor-card
|
||||
></i18n
|
||||
>
|
||||
<small class="has-text-grey activity-date">{{
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
activity.insertedAt | formatTimeString
|
||||
}}</small>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
></popover-actor-card
|
||||
></i18n
|
||||
>
|
||||
<small class="has-text-grey activity-date">{{
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
activity.insertedAt | formatTimeString
|
||||
}}</small>
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
v-for="detail in details"
|
||||
:key="detail"
|
||||
tag="p"
|
||||
class="has-text-grey"
|
||||
class="has-text-grey-dark"
|
||||
>
|
||||
<popover-actor-card
|
||||
:actor="activity.author"
|
||||
|
@ -63,7 +63,7 @@
|
|||
subjectParams.old_group_name
|
||||
}}</b>
|
||||
</i18n>
|
||||
<small class="has-text-grey activity-date">{{
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
activity.insertedAt | formatTimeString
|
||||
}}</small>
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
></popover-actor-card
|
||||
></i18n
|
||||
>
|
||||
<small class="has-text-grey activity-date">{{
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
activity.insertedAt | formatTimeString
|
||||
}}</small>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
></popover-actor-card
|
||||
></i18n
|
||||
>
|
||||
<small class="has-text-grey activity-date">{{
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
activity.insertedAt | formatTimeString
|
||||
}}</small>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
></popover-actor-card
|
||||
></i18n
|
||||
>
|
||||
<small class="has-text-grey activity-date">{{
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
activity.insertedAt | formatTimeString
|
||||
}}</small>
|
||||
</div>
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
|
||||
<div class="has-text-grey-dark" v-if="!discussion.lastComment.deletedAt">
|
||||
{{ htmlTextEllipsis }}
|
||||
</div>
|
||||
<div v-else class="has-text-grey">
|
||||
<div v-else class="has-text-grey-dark">
|
||||
{{ $t("[This comment has been deleted]") }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -55,20 +55,21 @@ section {
|
|||
}
|
||||
|
||||
div.group-section-title {
|
||||
--title-color: $violet-2;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: $secondary;
|
||||
color: #3a384c;
|
||||
color: var(--title-color);
|
||||
|
||||
&.privateSection {
|
||||
color: $violet-2;
|
||||
background: $purple-2;
|
||||
color: $purple-3;
|
||||
background: $violet-2;
|
||||
}
|
||||
|
||||
::v-deep & > a {
|
||||
align-self: center;
|
||||
margin-right: 5px;
|
||||
color: $orange-3;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
|
138
js/src/components/Group/ShareGroupModal.vue
Normal file
138
js/src/components/Group/ShareGroupModal.vue
Normal file
|
@ -0,0 +1,138 @@
|
|||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t("Share this group") }}</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body is-flex" v-if="group">
|
||||
<div class="container has-text-centered">
|
||||
<b-notification
|
||||
type="is-warning"
|
||||
v-if="group.visibility !== GroupVisibility.PUBLIC"
|
||||
:closable="false"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"This group is accessible only through it's link. Be careful where you post this link."
|
||||
)
|
||||
}}
|
||||
</b-notification>
|
||||
<b-field>
|
||||
<b-input ref="groupURLInput" :value="group.url" expanded />
|
||||
<p class="control">
|
||||
<b-tooltip
|
||||
:label="$t('URL copied to clipboard')"
|
||||
:active="showCopiedTooltip"
|
||||
always
|
||||
type="is-success"
|
||||
position="is-left"
|
||||
>
|
||||
<b-button
|
||||
type="is-primary"
|
||||
icon-right="content-paste"
|
||||
native-type="button"
|
||||
@click="copyURL"
|
||||
@keyup.enter="copyURL"
|
||||
/>
|
||||
</b-tooltip>
|
||||
</p>
|
||||
</b-field>
|
||||
<div>
|
||||
<!-- <b-icon icon="mastodon" size="is-large" type="is-primary" />-->
|
||||
|
||||
<a :href="twitterShareUrl" target="_blank" rel="nofollow noopener"
|
||||
><b-icon icon="twitter" size="is-large" type="is-primary"
|
||||
/></a>
|
||||
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"
|
||||
><b-icon icon="facebook" size="is-large" type="is-primary"
|
||||
/></a>
|
||||
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
|
||||
><b-icon icon="linkedin" size="is-large" type="is-primary"
|
||||
/></a>
|
||||
<a
|
||||
:href="diasporaShareUrl"
|
||||
class="diaspora"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
|
||||
<DiasporaLogo alt="diaspora-logo" />
|
||||
</span>
|
||||
</a>
|
||||
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
|
||||
><b-icon icon="email" size="is-large" type="is-primary"
|
||||
/></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||
import { GroupVisibility } from "@/types/enums";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
|
||||
import { displayName, IGroup } from "@/types/actor";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
DiasporaLogo,
|
||||
},
|
||||
})
|
||||
export default class ShareGroupModal extends Vue {
|
||||
@Prop({ type: Object, required: true }) group!: IGroup;
|
||||
|
||||
@Ref("groupURLInput") readonly groupURLInput!: any;
|
||||
|
||||
GroupVisibility = GroupVisibility;
|
||||
|
||||
showCopiedTooltip = false;
|
||||
|
||||
get twitterShareUrl(): string {
|
||||
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
|
||||
this.group.url
|
||||
)}&text=${displayName(this.group)}`;
|
||||
}
|
||||
|
||||
get facebookShareUrl(): string {
|
||||
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
||||
this.group.url
|
||||
)}`;
|
||||
}
|
||||
|
||||
get linkedInShareUrl(): string {
|
||||
return `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(
|
||||
this.group.url
|
||||
)}&title=${displayName(this.group)}`;
|
||||
}
|
||||
|
||||
get emailShareUrl(): string {
|
||||
return `mailto:?to=&body=${this.group.url}&subject=${displayName(
|
||||
this.group
|
||||
)}`;
|
||||
}
|
||||
|
||||
get diasporaShareUrl(): string {
|
||||
return `https://share.diasporafoundation.org/?title=${encodeURIComponent(
|
||||
displayName(this.group)
|
||||
)}&url=${encodeURIComponent(this.group.url)}`;
|
||||
}
|
||||
|
||||
copyURL(): void {
|
||||
this.groupURLInput.$refs.input.select();
|
||||
document.execCommand("copy");
|
||||
this.showCopiedTooltip = true;
|
||||
setTimeout(() => {
|
||||
this.showCopiedTooltip = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.diaspora span svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
</style>
|
|
@ -112,7 +112,12 @@
|
|||
<span @click="setIdentity(identity)">
|
||||
<div class="media-left">
|
||||
<figure class="image is-32x32" v-if="identity.avatar">
|
||||
<img class="is-rounded" :src="identity.avatar.url" alt />
|
||||
<img
|
||||
class="is-rounded"
|
||||
loading="lazy"
|
||||
:src="identity.avatar.url"
|
||||
alt
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||
</div>
|
||||
|
@ -133,11 +138,6 @@
|
|||
:to="{ name: RouteName.UPDATE_IDENTITY }"
|
||||
>{{ $t("My account") }}</b-navbar-item
|
||||
>
|
||||
|
||||
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
|
||||
<!-- {{ $t('Create group') }}-->
|
||||
<!-- </b-navbar-item>-->
|
||||
|
||||
<b-navbar-item
|
||||
v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR"
|
||||
tag="router-link"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="title-info-wrapper">
|
||||
<p class="post-minimalist-title">{{ post.title }}</p>
|
||||
<small class="has-text-grey">{{
|
||||
<small class="has-text-grey-dark">{{
|
||||
formatDistanceToNow(new Date(post.publishAt || post.insertedAt), {
|
||||
locale: $dateFnsLocale,
|
||||
addSuffix: true,
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<div class="title-wrapper">
|
||||
<img
|
||||
class="favicon"
|
||||
alt=""
|
||||
v-if="resource.metadata && resource.metadata.faviconUrl"
|
||||
:src="resource.metadata.faviconUrl"
|
||||
/>
|
||||
|
@ -31,7 +32,8 @@
|
|||
<div class="metadata-wrapper">
|
||||
<span class="host" v-if="!inline || preview">{{ urlHostname }}</span>
|
||||
<span
|
||||
class="published-at is-hidden-mobile"
|
||||
class="published-at"
|
||||
:class="{ 'is-hidden-mobile': !inline }"
|
||||
v-if="resource.updatedAt || resource.publishedAt"
|
||||
>{{
|
||||
(resource.updatedAt || resource.publishedAt)
|
||||
|
|
|
@ -80,10 +80,22 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
|
|||
avatar {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
banner {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
organizedEvents(
|
||||
afterDatetime: $afterDateTime
|
||||
|
|
|
@ -41,6 +41,11 @@ export const POST_FRAGMENT = gql`
|
|||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
height
|
||||
width
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
}
|
||||
${TAG_FRAGMENT}
|
||||
|
|
|
@ -1043,5 +1043,9 @@
|
|||
"User settings": "User settings",
|
||||
"You changed your email or password": "You changed your email or password",
|
||||
"Organized by you": "Organized by you",
|
||||
"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",
|
||||
"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",
|
||||
"Share": "Share"
|
||||
}
|
||||
|
|
|
@ -1134,5 +1134,9 @@
|
|||
"{username} was invited to {group}": "{username} a été invité à {group}",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"Organized by you": "Organisé par vous",
|
||||
"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",
|
||||
"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",
|
||||
"Share": "Partager"
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { IDiscussion } from "../discussions";
|
|||
import type { IPost } from "../post.model";
|
||||
import type { IAddress } from "../address.model";
|
||||
import { Address } from "../address.model";
|
||||
import { ActorType, Openness } from "../enums";
|
||||
import { ActorType, GroupVisibility, Openness } from "../enums";
|
||||
import type { IMember } from "./member.model";
|
||||
import type { ITodoList } from "../todolist";
|
||||
import { IActivity } from "../activity.model";
|
||||
|
@ -20,6 +20,7 @@ export interface IGroup extends IActor {
|
|||
organizedEvents: Paginate<IEvent>;
|
||||
physicalAddress: IAddress;
|
||||
openness: Openness;
|
||||
visibility: GroupVisibility;
|
||||
manuallyApprovesFollowers: boolean;
|
||||
activity: Paginate<IActivity>;
|
||||
}
|
||||
|
@ -43,6 +44,7 @@ export class Group extends Actor implements IGroup {
|
|||
|
||||
this.patch(hash);
|
||||
}
|
||||
visibility: GroupVisibility = GroupVisibility.PUBLIC;
|
||||
activity: Paginate<IActivity> = { elements: [], total: 0 };
|
||||
|
||||
openness: Openness = Openness.INVITE_ONLY;
|
||||
|
|
|
@ -45,21 +45,145 @@
|
|||
}}
|
||||
</b-message>
|
||||
<header class="block-container presentation">
|
||||
<div class="block-column media">
|
||||
<div class="media-left">
|
||||
<div class="banner-container">
|
||||
<lazy-image-wrapper :picture="group.picture" />
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="avatar-container">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img class="is-rounded" :src="group.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="title-container">
|
||||
<h1 v-if="group.name">{{ group.name }}</h1>
|
||||
<b-skeleton v-else :animated="true" />
|
||||
<small class="has-text-grey" v-if="group.preferredUsername"
|
||||
<small class="has-text-grey-dark" v-if="group.preferredUsername"
|
||||
>@{{ usernameWithDomain(group) }}</small
|
||||
>
|
||||
<b-skeleton v-else :animated="true" />
|
||||
<br />
|
||||
</div>
|
||||
<div class="group-metadata">
|
||||
<div class="block-column members" v-if="isCurrentActorAGroupMember">
|
||||
<div>
|
||||
<figure
|
||||
class="image is-32x32"
|
||||
:title="
|
||||
$t(`@{username} ({role})`, {
|
||||
username: usernameWithDomain(member.actor),
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in members"
|
||||
:key="member.actor.id"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="member.actor.avatar.url"
|
||||
v-if="member.actor.avatar"
|
||||
alt
|
||||
/>
|
||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||
</figure>
|
||||
</div>
|
||||
<p>
|
||||
{{ $t("{count} members", { count: group.members.total }) }}
|
||||
<router-link
|
||||
v-if="isCurrentActorAGroupAdmin"
|
||||
:to="{
|
||||
name: RouteName.GROUP_MEMBERS_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Add / Remove…") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div class="block-column address">
|
||||
<address v-if="physicalAddress">
|
||||
<p
|
||||
class="addressDescription"
|
||||
:title="physicalAddress.poiInfos.name"
|
||||
>
|
||||
{{ physicalAddress.poiInfos.name }}
|
||||
</p>
|
||||
<p>{{ physicalAddress.poiInfos.alternativeName }}</p>
|
||||
</address>
|
||||
<span
|
||||
class="map-show-button"
|
||||
@click="showMap = !showMap"
|
||||
v-if="physicalAddress && physicalAddress.geom"
|
||||
>{{ $t("Show map") }}</span
|
||||
>
|
||||
<p class="buttons">
|
||||
<b-tooltip
|
||||
v-if="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="currentActor.id"
|
||||
@click="joinGroup"
|
||||
type="is-primary"
|
||||
>{{ $t("Join group") }}</b-button
|
||||
>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.GROUP_JOIN,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
v-else
|
||||
type="is-primary"
|
||||
>{{ $t("Join group") }}</b-button
|
||||
>
|
||||
<b-dropdown
|
||||
class="menu-dropdown"
|
||||
aria-role="list"
|
||||
position="is-bottom-left"
|
||||
>
|
||||
<b-button
|
||||
slot="trigger"
|
||||
role="button"
|
||||
icon-right="dots-horizontal"
|
||||
>
|
||||
</b-button>
|
||||
<b-dropdown-item
|
||||
aria-role="listitem"
|
||||
v-if="ableToReport"
|
||||
@click="isReportModalActive = true"
|
||||
>
|
||||
<span>
|
||||
<b-icon icon="flag" />
|
||||
{{ $t("Report") }}
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<hr class="dropdown-divider" />
|
||||
<b-dropdown-item has-link aria-role="listitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/atom`"
|
||||
:title="$t('Atom feed for events and posts')"
|
||||
>
|
||||
<b-icon icon="rss" />
|
||||
{{ $t("RSS/Atom Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item has-link aria-role="listitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/ics`"
|
||||
:title="$t('ICS feed for events')"
|
||||
>
|
||||
<b-icon icon="calendar-sync" />
|
||||
{{ $t("ICS/WebCal Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</p>
|
||||
</div> -->
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
outlined
|
||||
|
@ -83,21 +207,55 @@
|
|||
}"
|
||||
>{{ $t("Group settings") }}</b-button
|
||||
>
|
||||
<b-button
|
||||
outlined
|
||||
icon-left="share"
|
||||
@click="triggerShare()"
|
||||
v-if="!isCurrentActorAGroupMember"
|
||||
>
|
||||
{{ $t("Share") }}
|
||||
</b-button>
|
||||
<b-dropdown
|
||||
class="menu-dropdown"
|
||||
aria-role="list"
|
||||
v-if="isCurrentActorAGroupMember"
|
||||
position="is-bottom-left"
|
||||
aria-role="menu"
|
||||
>
|
||||
<b-button
|
||||
slot="trigger"
|
||||
outlined
|
||||
role="button"
|
||||
icon-right="dots-horizontal"
|
||||
>
|
||||
</b-button>
|
||||
icon-left="dots-horizontal"
|
||||
aria-label="Other actions"
|
||||
/>
|
||||
<b-dropdown-item aria-role="menuitem" @click="triggerShare()">
|
||||
<span>
|
||||
<b-icon icon="share" />
|
||||
{{ $t("Share") }}
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<hr class="dropdown-divider" />
|
||||
<b-dropdown-item has-link aria-role="menuitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/atom`"
|
||||
:title="$t('Atom feed for events and posts')"
|
||||
>
|
||||
<b-icon icon="rss" />
|
||||
{{ $t("RSS/Atom Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item has-link aria-role="menuitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/ics`"
|
||||
:title="$t('ICS feed for events')"
|
||||
>
|
||||
<b-icon icon="calendar-sync" />
|
||||
{{ $t("ICS/WebCal Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<hr class="dropdown-divider" />
|
||||
<b-dropdown-item
|
||||
aria-role="listitem"
|
||||
aria-role="menuitem"
|
||||
v-if="ableToReport"
|
||||
@click="isReportModalActive = true"
|
||||
>
|
||||
|
@ -107,7 +265,7 @@
|
|||
</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
aria-role="listitem"
|
||||
aria-role="menuitem"
|
||||
v-if="isCurrentActorAGroupMember"
|
||||
@click="leaveGroup"
|
||||
>
|
||||
|
@ -116,153 +274,10 @@
|
|||
{{ $t("Leave") }}
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<hr class="dropdown-divider" />
|
||||
<b-dropdown-item has-link aria-role="listitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/atom`"
|
||||
:title="$t('Atom feed for events and posts')"
|
||||
>
|
||||
<b-icon icon="rss" />
|
||||
{{ $t("RSS/Atom Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item has-link aria-role="listitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/ics`"
|
||||
:title="$t('ICS feed for events')"
|
||||
>
|
||||
<b-icon icon="calendar-sync" />
|
||||
{{ $t("ICS/WebCal Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-column members" v-if="isCurrentActorAGroupMember">
|
||||
<div>
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
:title="
|
||||
$t(`@{username} ({role})`, {
|
||||
username: usernameWithDomain(member.actor),
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in members"
|
||||
:key="member.actor.id"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="member.actor.avatar.url"
|
||||
v-if="member.actor.avatar"
|
||||
alt
|
||||
/>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</figure>
|
||||
</div>
|
||||
<p>
|
||||
{{ $t("{count} team members", { count: group.members.total }) }}
|
||||
<router-link
|
||||
v-if="isCurrentActorAGroupAdmin"
|
||||
:to="{
|
||||
name: RouteName.GROUP_MEMBERS_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Add / Remove…") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="block-column address" v-else>
|
||||
<address v-if="physicalAddress">
|
||||
<p
|
||||
class="addressDescription"
|
||||
:title="physicalAddress.poiInfos.name"
|
||||
>
|
||||
{{ physicalAddress.poiInfos.name }}
|
||||
</p>
|
||||
<p>{{ physicalAddress.poiInfos.alternativeName }}</p>
|
||||
</address>
|
||||
<span
|
||||
class="map-show-button"
|
||||
@click="showMap = !showMap"
|
||||
v-if="physicalAddress && physicalAddress.geom"
|
||||
>{{ $t("Show map") }}</span
|
||||
>
|
||||
<p class="buttons">
|
||||
<b-tooltip
|
||||
v-if="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="currentActor.id"
|
||||
@click="joinGroup"
|
||||
type="is-primary"
|
||||
>{{ $t("Join group") }}</b-button
|
||||
>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.GROUP_JOIN,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
v-else
|
||||
type="is-primary"
|
||||
>{{ $t("Join group") }}</b-button
|
||||
>
|
||||
<b-dropdown
|
||||
class="menu-dropdown"
|
||||
aria-role="list"
|
||||
position="is-bottom-left"
|
||||
>
|
||||
<b-button
|
||||
slot="trigger"
|
||||
role="button"
|
||||
icon-right="dots-horizontal"
|
||||
>
|
||||
</b-button>
|
||||
<b-dropdown-item
|
||||
aria-role="listitem"
|
||||
v-if="ableToReport"
|
||||
@click="isReportModalActive = true"
|
||||
>
|
||||
<span>
|
||||
<b-icon icon="flag" />
|
||||
{{ $t("Report") }}
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<hr class="dropdown-divider" />
|
||||
<b-dropdown-item has-link aria-role="listitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/atom`"
|
||||
:title="$t('Atom feed for events and posts')"
|
||||
>
|
||||
<b-icon icon="rss" />
|
||||
{{ $t("RSS/Atom Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item has-link aria-role="listitem">
|
||||
<a
|
||||
:href="`@${preferredUsername}/feed/ics`"
|
||||
:title="$t('ICS feed for events')"
|
||||
>
|
||||
<b-icon icon="calendar-sync" />
|
||||
{{ $t("ICS/WebCal Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
v-if="group.banner && group.banner.url"
|
||||
:src="group.banner.url"
|
||||
alt=""
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
<div v-if="isCurrentActorAGroupMember" class="block-container">
|
||||
|
@ -285,7 +300,7 @@
|
|||
:discussion="discussion"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="content has-text-grey has-text-centered">
|
||||
<div v-else class="content has-text-grey-dark has-text-centered">
|
||||
<p>{{ $t("No discussions yet") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -330,7 +345,7 @@
|
|||
</div>
|
||||
<div
|
||||
v-else-if="group"
|
||||
class="content has-text-grey has-text-centered"
|
||||
class="content has-text-grey-dark has-text-centered"
|
||||
>
|
||||
<p>{{ $t("No resources yet") }}</p>
|
||||
</div>
|
||||
|
@ -351,7 +366,7 @@
|
|||
<div class="block-column">
|
||||
<!-- Events -->
|
||||
<group-section
|
||||
:title="$t('Upcoming events')"
|
||||
:title="$t('Events')"
|
||||
icon="calendar"
|
||||
:privateSection="false"
|
||||
:route="{
|
||||
|
@ -373,7 +388,7 @@
|
|||
</div>
|
||||
<div
|
||||
v-else-if="group"
|
||||
class="content has-text-grey has-text-centered"
|
||||
class="content has-text-grey-dark has-text-centered"
|
||||
>
|
||||
<p>{{ $t("No public upcoming events") }}</p>
|
||||
</div>
|
||||
|
@ -411,7 +426,7 @@
|
|||
</div>
|
||||
<div
|
||||
v-else-if="group"
|
||||
class="content has-text-grey has-text-centered"
|
||||
class="content has-text-grey-dark has-text-centered"
|
||||
>
|
||||
<p>{{ $t("No posts yet") }}</p>
|
||||
</div>
|
||||
|
@ -434,71 +449,119 @@
|
|||
{{ $t("No group found") }}
|
||||
</b-message>
|
||||
<div v-else class="public-container">
|
||||
<section>
|
||||
<subtitle>{{ $t("About") }}</subtitle>
|
||||
<div
|
||||
v-html="group.summary"
|
||||
v-if="group.summary && group.summary !== '<p></p>'"
|
||||
/>
|
||||
<div v-else-if="group" class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("This group doesn't have a description yet.") }}</p>
|
||||
<aside class="group-metadata">
|
||||
<div class="sticky">
|
||||
<event-metadata-block
|
||||
:title="$t('Location')"
|
||||
:icon="
|
||||
physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'
|
||||
"
|
||||
>
|
||||
<div class="address-wrapper">
|
||||
<span v-if="!physicalAddress">{{
|
||||
$t("No address defined")
|
||||
}}</span>
|
||||
<div class="address" v-if="physicalAddress">
|
||||
<div>
|
||||
<address>
|
||||
<p
|
||||
class="addressDescription"
|
||||
:title="physicalAddress.poiInfos.name"
|
||||
>
|
||||
{{ physicalAddress.poiInfos.name }}
|
||||
</p>
|
||||
<p class="has-text-grey-dark">
|
||||
{{ physicalAddress.poiInfos.alternativeName }}
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
<span
|
||||
class="map-show-button"
|
||||
@click="showMap = !showMap"
|
||||
v-if="physicalAddress.geom"
|
||||
>{{ $t("Show map") }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</event-metadata-block>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div
|
||||
class="organized-events-wrapper"
|
||||
v-if="group && group.organizedEvents.total > 0"
|
||||
>
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
class="organized-event"
|
||||
</aside>
|
||||
<div class="main-content">
|
||||
<section>
|
||||
<subtitle>{{ $t("About") }}</subtitle>
|
||||
<div
|
||||
v-html="group.summary"
|
||||
v-if="group.summary && group.summary !== '<p></p>'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="group && group.organizedEvents.elements.length == 0"
|
||||
class="content has-text-grey has-text-centered"
|
||||
>
|
||||
<p>{{ $t("No public upcoming events") }}</p>
|
||||
</div>
|
||||
<div v-else-if="group" class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No public upcoming events") }}</p>
|
||||
</div>
|
||||
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
|
||||
<router-link
|
||||
v-if="group.organizedEvents.total > 0"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
query: { future: group.organizedEvents.elements.length > 0 },
|
||||
}"
|
||||
>{{ $t("View all events") }}</router-link
|
||||
>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Latest posts") }}</subtitle>
|
||||
<div v-if="group.posts.total > 0" class="posts-wrapper">
|
||||
<post-list-item
|
||||
v-for="post in group.posts.elements"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="group" class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No posts yet") }}</p>
|
||||
</div>
|
||||
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
|
||||
<router-link
|
||||
v-if="group.posts.total > 0"
|
||||
:to="{
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all posts") }}</router-link
|
||||
>
|
||||
</section>
|
||||
<div
|
||||
v-else-if="group"
|
||||
class="content has-text-grey-dark has-text-centered"
|
||||
>
|
||||
<p>{{ $t("This group doesn't have a description yet.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div
|
||||
class="organized-events-wrapper"
|
||||
v-if="group && group.organizedEvents.total > 0"
|
||||
>
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
class="organized-event"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="group && group.organizedEvents.elements.length == 0"
|
||||
class="content has-text-grey-dark has-text-centered"
|
||||
>
|
||||
<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>
|
||||
<router-link
|
||||
v-if="group.organizedEvents.total > 0"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
query: { future: group.organizedEvents.elements.length > 0 },
|
||||
}"
|
||||
>{{ $t("View all events") }}</router-link
|
||||
>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Latest posts") }}</subtitle>
|
||||
<div v-if="group.posts.total > 0" class="posts-wrapper">
|
||||
<post-list-item
|
||||
v-for="post in group.posts.elements"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="group"
|
||||
class="content has-text-grey-dark has-text-centered"
|
||||
>
|
||||
<p>{{ $t("No posts yet") }}</p>
|
||||
</div>
|
||||
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
|
||||
<router-link
|
||||
v-if="group.posts.total > 0"
|
||||
:to="{
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all posts") }}</router-link
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
<b-modal
|
||||
v-if="physicalAddress && physicalAddress.geom"
|
||||
:active.sync="showMap"
|
||||
|
@ -526,13 +589,16 @@
|
|||
@close="$refs.reportModal.close()"
|
||||
/>
|
||||
</b-modal>
|
||||
<b-modal :active.sync="isShareModalActive" has-modal-card ref="shareModal">
|
||||
<share-group-modal :group="group" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { IActor, usernameWithDomain } from "@/types/actor";
|
||||
import { displayName, IActor, usernameWithDomain } from "@/types/actor";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
||||
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
|
||||
|
@ -557,6 +623,8 @@ import GroupSection from "../../components/Group/GroupSection.vue";
|
|||
import ReportModal from "../../components/Report/ReportModal.vue";
|
||||
import { PERSON_MEMBERSHIP_GROUP } from "@/graphql/actor";
|
||||
import { LEAVE_GROUP } from "@/graphql/group";
|
||||
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
|
||||
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
|
@ -574,8 +642,14 @@ import { LEAVE_GROUP } from "@/graphql/group";
|
|||
GroupSection,
|
||||
Invitations,
|
||||
ReportModal,
|
||||
LazyImageWrapper,
|
||||
EventMetadataBlock,
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||
ShareGroupModal: () =>
|
||||
import(
|
||||
/* webpackChunkName: "shareGroupModal" */ "../../components/Group/ShareGroupModal.vue"
|
||||
),
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
|
@ -607,6 +681,8 @@ export default class Group extends mixins(GroupMixin) {
|
|||
|
||||
isReportModalActive = false;
|
||||
|
||||
isShareModalActive = false;
|
||||
|
||||
@Watch("currentActor")
|
||||
watchCurrentActor(currentActor: IActor, oldActor: IActor): void {
|
||||
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
|
||||
|
@ -717,6 +793,27 @@ export default class Group extends mixins(GroupMixin) {
|
|||
}
|
||||
}
|
||||
|
||||
triggerShare(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-start
|
||||
if (navigator.share) {
|
||||
navigator
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.share({
|
||||
title: displayName(this.group),
|
||||
url: this.group.url,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch((error: any) => console.log("Error sharing", error));
|
||||
} else {
|
||||
this.isShareModalActive = true;
|
||||
// send popup
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-end
|
||||
}
|
||||
|
||||
get groupTitle(): undefined | string {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.name || this.group.preferredUsername;
|
||||
|
@ -822,17 +919,31 @@ export default class Group extends mixins(GroupMixin) {
|
|||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div.container {
|
||||
background: white;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem 0;
|
||||
|
||||
.header,
|
||||
.public-container {
|
||||
margin: auto 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: $white;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.header .breadcrumb {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.public-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row-reverse;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -840,8 +951,9 @@ div.container {
|
|||
|
||||
&.presentation {
|
||||
border: 2px solid $purple-2;
|
||||
padding: 10px 0;
|
||||
padding: 0 0 10px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
|
||||
h1 {
|
||||
color: $purple-1;
|
||||
|
@ -858,28 +970,16 @@ div.container {
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
& > img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.3;
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.members {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
div {
|
||||
& > .banner-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
figure:not(:first-child) {
|
||||
margin-left: -10px;
|
||||
justify-content: center;
|
||||
height: 30vh;
|
||||
::v-deep img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -925,9 +1025,19 @@ div.container {
|
|||
|
||||
.block-column {
|
||||
flex: 1;
|
||||
margin: 0 1rem;
|
||||
margin: 0 0.5rem;
|
||||
max-width: 576px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
background: $white;
|
||||
|
||||
.posts-wrapper {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
@ -965,9 +1075,95 @@ div.container {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
height: 0;
|
||||
margin-top: 16px;
|
||||
align-items: flex-end;
|
||||
|
||||
figure {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-metadata {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
& > .buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.members {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
figure:not(:first-child) {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.public-container {
|
||||
.group-metadata {
|
||||
min-width: 20rem;
|
||||
flex: 1;
|
||||
padding-left: 1rem;
|
||||
margin: 2rem auto;
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
background: white;
|
||||
top: 50px;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
.main-content {
|
||||
min-width: 20rem;
|
||||
padding: 1rem;
|
||||
flex: 2;
|
||||
background: white;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
>
|
||||
<form @submit.prevent="updateGroup">
|
||||
<b-field :label="$t('Group name')">
|
||||
<b-input v-model="group.name" />
|
||||
<b-input v-model="editableGroup.name" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Group short description')">
|
||||
<editor mode="basic" v-model="group.summary" :maxSize="500"
|
||||
<editor mode="basic" v-model="editableGroup.summary" :maxSize="500"
|
||||
/></b-field>
|
||||
<b-field :label="$t('Avatar')">
|
||||
<picture-upload
|
||||
|
@ -62,7 +62,7 @@
|
|||
<p class="label">{{ $t("Group visibility") }}</p>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="group.visibility"
|
||||
v-model="editableGroup.visibility"
|
||||
name="groupVisibility"
|
||||
:native-value="GroupVisibility.PUBLIC"
|
||||
>
|
||||
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="group.visibility"
|
||||
v-model="editableGroup.visibility"
|
||||
name="groupVisibility"
|
||||
:native-value="GroupVisibility.PRIVATE"
|
||||
>{{ $t("Only accessible through link") }}<br />
|
||||
|
@ -110,7 +110,7 @@
|
|||
<p class="label">{{ $t("New members") }}</p>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="group.openness"
|
||||
v-model="editableGroup.openness"
|
||||
name="groupOpenness"
|
||||
:native-value="Openness.OPEN"
|
||||
>
|
||||
|
@ -124,7 +124,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="group.openness"
|
||||
v-model="editableGroup.openness"
|
||||
name="groupOpenness"
|
||||
:native-value="Openness.INVITE_ONLY"
|
||||
>{{ $t("Manually invite new members") }}<br />
|
||||
|
@ -140,14 +140,14 @@
|
|||
:label="$t('Followers')"
|
||||
:message="$t('Followers will receive new public events and posts.')"
|
||||
>
|
||||
<b-checkbox v-model="group.manuallyApprovesFollowers">
|
||||
<b-checkbox v-model="editableGroup.manuallyApprovesFollowers">
|
||||
{{ $t("Manually approve new followers") }}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
<full-address-auto-complete
|
||||
:label="$t('Group address')"
|
||||
v-model="group.physicalAddress"
|
||||
v-model="editableGroup.physicalAddress"
|
||||
:value="currentAddress"
|
||||
/>
|
||||
|
||||
|
@ -171,14 +171,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from "vue-property-decorator";
|
||||
import { Component, Watch } from "vue-property-decorator";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import { Route } from "vue-router";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import { mixins } from "vue-class-component";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { GroupVisibility, Openness } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
import { UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import { Address, IAddress } from "../../types/address.model";
|
||||
|
@ -186,6 +185,7 @@ import { CONFIG } from "@/graphql/config";
|
|||
import { IConfig } from "@/types/config.model";
|
||||
import { ServerParseError } from "@apollo/client/link/http";
|
||||
import { ErrorResponse } from "@apollo/client/link/error";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
|
@ -225,9 +225,12 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
|||
|
||||
showCopiedTooltip = false;
|
||||
|
||||
editableGroup!: IGroup;
|
||||
|
||||
async updateGroup(): Promise<void> {
|
||||
try {
|
||||
const variables = this.buildVariables();
|
||||
console.log(variables);
|
||||
await this.$apollo.mutate<{ updateGroup: IGroup }>({
|
||||
mutation: UPDATE_GROUP,
|
||||
variables,
|
||||
|
@ -270,18 +273,26 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
|||
}, 2000);
|
||||
}
|
||||
|
||||
@Watch("group")
|
||||
async watchUpdateGroup(): Promise<void> {
|
||||
this.editableGroup = { ...this.group };
|
||||
}
|
||||
|
||||
private buildVariables() {
|
||||
let avatarObj = {};
|
||||
let bannerObj = {};
|
||||
const variables = { ...this.group };
|
||||
const variables = { ...this.editableGroup };
|
||||
const physicalAddress = {
|
||||
...variables.physicalAddress,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.__typename;
|
||||
if (variables.physicalAddress) {
|
||||
if (physicalAddress) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.physicalAddress.__typename;
|
||||
delete physicalAddress.__typename;
|
||||
}
|
||||
delete variables.avatar;
|
||||
delete variables.banner;
|
||||
|
@ -291,7 +302,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
|||
avatar: {
|
||||
media: {
|
||||
name: this.avatarFile.name,
|
||||
alt: `${this.group.preferredUsername}'s avatar`,
|
||||
alt: `${this.editableGroup.preferredUsername}'s avatar`,
|
||||
file: this.avatarFile,
|
||||
},
|
||||
},
|
||||
|
@ -303,14 +314,20 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
|||
banner: {
|
||||
media: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
alt: `${this.editableGroup.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...variables,
|
||||
id: this.group.id,
|
||||
name: this.editableGroup.name,
|
||||
summary: this.editableGroup.summary,
|
||||
visibility: this.editableGroup.visibility,
|
||||
openness: this.editableGroup.openness,
|
||||
manuallyApprovesFollowers: this.editableGroup.manuallyApprovesFollowers,
|
||||
physicalAddress,
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
};
|
||||
|
@ -322,7 +339,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
|||
}
|
||||
|
||||
get currentAddress(): IAddress {
|
||||
return new Address(this.group.physicalAddress);
|
||||
return new Address(this.editableGroup.physicalAddress);
|
||||
}
|
||||
|
||||
get avatarMaxSize(): number | undefined {
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<picture-upload
|
||||
v-model="pictureFile"
|
||||
:textFallback="$t('Headline picture')"
|
||||
:defaultImage="post.picture"
|
||||
:defaultImage="editablePost.picture"
|
||||
/>
|
||||
|
||||
<b-field
|
||||
|
@ -61,21 +61,21 @@
|
|||
size="is-large"
|
||||
aria-required="true"
|
||||
required
|
||||
v-model="post.title"
|
||||
v-model="editablePost.title"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<tag-input v-model="post.tags" :data="tags" path="title" />
|
||||
<tag-input v-model="editablePost.tags" :data="tags" path="title" />
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("Post") }}</label>
|
||||
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
|
||||
<editor v-model="post.body" />
|
||||
<editor v-model="editablePost.body" />
|
||||
</div>
|
||||
<subtitle>{{ $t("Who can view this post") }}</subtitle>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
v-model="editablePost.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PUBLIC"
|
||||
>{{ $t("Visible everywhere on the web") }}</b-radio
|
||||
|
@ -83,7 +83,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
v-model="editablePost.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link") }}</b-radio
|
||||
|
@ -91,7 +91,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
v-model="editablePost.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PRIVATE"
|
||||
>{{ $t("Only accessible to members of the group") }}</b-radio
|
||||
|
@ -166,7 +166,7 @@ import {
|
|||
|
||||
import { IPost } from "../../types/post.model";
|
||||
import Editor from "../../components/Editor.vue";
|
||||
import { IActor, IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import { IActor, usernameWithDomain } from "../../types/actor";
|
||||
import TagInput from "../../components/Event/TagInput.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
|
@ -249,8 +249,6 @@ export default class EditPost extends mixins(GroupMixin) {
|
|||
tags: [],
|
||||
};
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
PostVisibility = PostVisibility;
|
||||
|
||||
pictureFile: File | null = null;
|
||||
|
@ -259,6 +257,8 @@ export default class EditPost extends mixins(GroupMixin) {
|
|||
|
||||
RouteName = RouteName;
|
||||
|
||||
editablePost!: IPost;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async mounted(): Promise<void> {
|
||||
|
@ -270,6 +270,7 @@ export default class EditPost extends mixins(GroupMixin) {
|
|||
if (oldPost.picture !== newPost.picture) {
|
||||
this.pictureFile = await buildFileFromIMedia(this.post.picture);
|
||||
}
|
||||
this.editablePost = { ...this.post };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
|
@ -280,11 +281,11 @@ export default class EditPost extends mixins(GroupMixin) {
|
|||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPDATE_POST,
|
||||
variables: {
|
||||
id: this.post.id,
|
||||
title: this.post.title,
|
||||
body: this.post.body,
|
||||
tags: (this.post.tags || []).map(({ title }) => title),
|
||||
visibility: this.post.visibility,
|
||||
id: this.editablePost.id,
|
||||
title: this.editablePost.title,
|
||||
body: this.editablePost.body,
|
||||
tags: (this.editablePost.tags || []).map(({ title }) => title),
|
||||
visibility: this.editablePost.visibility,
|
||||
draft,
|
||||
...(await this.buildPicture()),
|
||||
},
|
||||
|
@ -300,9 +301,9 @@ export default class EditPost extends mixins(GroupMixin) {
|
|||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_POST,
|
||||
variables: {
|
||||
...this.post,
|
||||
...this.editablePost,
|
||||
...(await this.buildPicture()),
|
||||
tags: (this.post.tags || []).map(({ title }) => title),
|
||||
tags: (this.editablePost.tags || []).map(({ title }) => title),
|
||||
attributedToId: this.actualGroup.id,
|
||||
draft,
|
||||
},
|
||||
|
@ -362,16 +363,16 @@ export default class EditPost extends mixins(GroupMixin) {
|
|||
obj = { ...obj, ...pictureObj };
|
||||
}
|
||||
try {
|
||||
if (this.post.picture && this.pictureFile) {
|
||||
if (this.editablePost.picture && this.pictureFile) {
|
||||
const oldPictureFile = (await buildFileFromIMedia(
|
||||
this.post.picture
|
||||
this.editablePost.picture
|
||||
)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(
|
||||
this.pictureFile as File
|
||||
);
|
||||
if (oldPictureFileContent === newPictureFileContent) {
|
||||
obj.picture = { mediaId: this.post.picture.id };
|
||||
obj.picture = { mediaId: this.editablePost.picture.id };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,61 +1,78 @@
|
|||
<template>
|
||||
<div>
|
||||
<article class="container" v-if="post">
|
||||
<section class="heading-section">
|
||||
<h1 class="title">{{ post.title }}</h1>
|
||||
<i18n tag="span" path="By {author}" class="authors">
|
||||
<router-link
|
||||
slot="author"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(post.attributedTo),
|
||||
},
|
||||
}"
|
||||
>{{ post.attributedTo.name }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
<p class="published has-text-grey-dark" v-if="!post.draft">
|
||||
{{ post.publishAt | formatDateTimeString }}
|
||||
</p>
|
||||
<small
|
||||
v-if="post.visibility === PostVisibility.PRIVATE"
|
||||
class="has-text-grey-dark"
|
||||
>
|
||||
<b-icon icon="lock" size="is-small" />
|
||||
{{
|
||||
$t("Accessible only to members", { group: post.attributedTo.name })
|
||||
}}
|
||||
</small>
|
||||
<p class="buttons" v-if="isCurrentActorMember">
|
||||
<b-tag type="is-warning" size="is-medium" v-if="post.draft">{{
|
||||
$t("Draft")
|
||||
}}</b-tag>
|
||||
<router-link
|
||||
v-if="
|
||||
currentActor.id === post.author.id ||
|
||||
isCurrentActorAGroupModerator
|
||||
"
|
||||
:to="{ name: RouteName.POST_EDIT, params: { slug: post.slug } }"
|
||||
tag="button"
|
||||
class="button is-text"
|
||||
>{{ $t("Edit") }}</router-link
|
||||
>
|
||||
</p>
|
||||
<img v-if="post.picture" :src="post.picture.url" alt="" />
|
||||
</section>
|
||||
<section v-html="post.body" class="content" />
|
||||
<section class="tags">
|
||||
<router-link
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.title"
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
>
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<article class="container" v-if="post">
|
||||
<header>
|
||||
<div class="banner-container">
|
||||
<lazy-image
|
||||
v-if="post.picture"
|
||||
:src="post.picture.url"
|
||||
:width="post.picture.metadata.width"
|
||||
:height="post.picture.metadata.height"
|
||||
:blurhash="post.picture.metadata.blurhash"
|
||||
/>
|
||||
</div>
|
||||
<div class="heading-section">
|
||||
<div class="heading-wrapper">
|
||||
<div class="title-metadata">
|
||||
<h1 class="title">{{ post.title }}</h1>
|
||||
<p class="metadata">
|
||||
<router-link
|
||||
slot="author"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(post.attributedTo),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<actor-inline :actor="post.attributedTo" />
|
||||
</router-link>
|
||||
<span class="published has-text-grey-dark" v-if="!post.draft">
|
||||
<b-icon icon="clock" size="is-small" />
|
||||
{{ post.publishAt | formatDateTimeString }}
|
||||
</span>
|
||||
<span
|
||||
v-if="post.visibility === PostVisibility.PRIVATE"
|
||||
class="has-text-grey-dark"
|
||||
>
|
||||
<b-icon icon="lock" size="is-small" />
|
||||
{{
|
||||
$t("Accessible only to members", {
|
||||
group: post.attributedTo.name,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="buttons" v-if="isCurrentActorMember">
|
||||
<b-tag type="is-warning" size="is-medium" v-if="post.draft">{{
|
||||
$t("Draft")
|
||||
}}</b-tag>
|
||||
<router-link
|
||||
v-if="
|
||||
currentActor.id === post.author.id ||
|
||||
isCurrentActorAGroupModerator
|
||||
"
|
||||
:to="{ name: RouteName.POST_EDIT, params: { slug: post.slug } }"
|
||||
tag="button"
|
||||
class="button is-text"
|
||||
>{{ $t("Edit") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section v-html="post.body" class="content" />
|
||||
<section class="tags">
|
||||
<router-link
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.title"
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
>
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -66,11 +83,12 @@ import { PostVisibility } from "@/types/enums";
|
|||
import { IMember } from "@/types/actor/member.model";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor";
|
||||
import { FETCH_POST } from "../../graphql/post";
|
||||
|
||||
import { IPost } from "../../types/post.model";
|
||||
import { usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import Tag from "../../components/Tag.vue";
|
||||
import LazyImage from "../../components/Image/LazyImage.vue";
|
||||
import ActorInline from "../../components/Account/ActorInline.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
|
@ -106,6 +124,8 @@ import Tag from "../../components/Tag.vue";
|
|||
},
|
||||
components: {
|
||||
Tag,
|
||||
LazyImage,
|
||||
ActorInline,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
|
@ -148,78 +168,93 @@ export default class Post extends mixins(GroupMixin) {
|
|||
</script>
|
||||
<style lang="scss" scoped>
|
||||
article {
|
||||
section.heading-section {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: $white !important;
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto -3rem 2rem;
|
||||
|
||||
h1.title {
|
||||
margin: 0 auto;
|
||||
padding-top: 3rem;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.authors {
|
||||
margin-top: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.published {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: 0.2rem;
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: $purple-1;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
.banner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
& > * {
|
||||
z-index: 10;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
& > img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.3;
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
.heading-section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
section.content {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.heading-wrapper {
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
section.tags {
|
||||
padding-bottom: 5rem;
|
||||
.title-metadata {
|
||||
min-width: 300px;
|
||||
flex: 20;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
span {
|
||||
&.tag {
|
||||
margin: 0 2px;
|
||||
p.metadata {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
*:not(:first-child) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
p.buttons {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h1.title {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
font-size: 38px;
|
||||
font-family: "Roboto", "Helvetica", "Arial", serif;
|
||||
}
|
||||
|
||||
.authors {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: 0.2rem;
|
||||
content: " ";
|
||||
display: block;
|
||||
background-color: $purple-1;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > section {
|
||||
margin: 0 2rem;
|
||||
|
||||
&.content {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
&.tags {
|
||||
padding-bottom: 5rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
span {
|
||||
&.tag {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: $white;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 3rem;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue