Improve group related UI

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-06-15 17:25:33 +02:00
parent 9639a066ff
commit 6cc233a6d3
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
22 changed files with 892 additions and 427 deletions

View 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>

View File

@ -41,7 +41,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -27,7 +27,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -34,7 +34,7 @@
v-for="detail in details" v-for="detail in details"
:key="detail" :key="detail"
tag="p" tag="p"
class="has-text-grey" class="has-text-grey-dark"
> >
<popover-actor-card <popover-actor-card
:actor="activity.author" :actor="activity.author"
@ -63,7 +63,7 @@
subjectParams.old_group_name subjectParams.old_group_name
}}</b> }}</b>
</i18n> </i18n>
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -34,7 +34,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -27,7 +27,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -37,7 +37,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -32,10 +32,10 @@
}}</span }}</span
> >
</div> </div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt"> <div class="has-text-grey-dark" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }} {{ htmlTextEllipsis }}
</div> </div>
<div v-else class="has-text-grey"> <div v-else class="has-text-grey-dark">
{{ $t("[This comment has been deleted]") }} {{ $t("[This comment has been deleted]") }}
</div> </div>
</div> </div>

View File

@ -55,20 +55,21 @@ section {
} }
div.group-section-title { div.group-section-title {
--title-color: $violet-2;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
background: $secondary; background: $secondary;
color: #3a384c; color: var(--title-color);
&.privateSection { &.privateSection {
color: $violet-2; color: $purple-3;
background: $purple-2; background: $violet-2;
} }
::v-deep & > a { ::v-deep & > a {
align-self: center; align-self: center;
margin-right: 5px; margin-right: 5px;
color: $orange-3; color: var(--title-color);
} }
h2 { h2 {

View 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>

View File

@ -112,7 +112,12 @@
<span @click="setIdentity(identity)"> <span @click="setIdentity(identity)">
<div class="media-left"> <div class="media-left">
<figure class="image is-32x32" v-if="identity.avatar"> <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> </figure>
<b-icon v-else size="is-medium" icon="account-circle" /> <b-icon v-else size="is-medium" icon="account-circle" />
</div> </div>
@ -133,11 +138,6 @@
:to="{ name: RouteName.UPDATE_IDENTITY }" :to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item >{{ $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 <b-navbar-item
v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR" v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR"
tag="router-link" tag="router-link"

View File

@ -5,7 +5,7 @@
> >
<div class="title-info-wrapper"> <div class="title-info-wrapper">
<p class="post-minimalist-title">{{ post.title }}</p> <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), { formatDistanceToNow(new Date(post.publishAt || post.insertedAt), {
locale: $dateFnsLocale, locale: $dateFnsLocale,
addSuffix: true, addSuffix: true,

View File

@ -23,6 +23,7 @@
<div class="title-wrapper"> <div class="title-wrapper">
<img <img
class="favicon" class="favicon"
alt=""
v-if="resource.metadata && resource.metadata.faviconUrl" v-if="resource.metadata && resource.metadata.faviconUrl"
:src="resource.metadata.faviconUrl" :src="resource.metadata.faviconUrl"
/> />
@ -31,7 +32,8 @@
<div class="metadata-wrapper"> <div class="metadata-wrapper">
<span class="host" v-if="!inline || preview">{{ urlHostname }}</span> <span class="host" v-if="!inline || preview">{{ urlHostname }}</span>
<span <span
class="published-at is-hidden-mobile" class="published-at"
:class="{ 'is-hidden-mobile': !inline }"
v-if="resource.updatedAt || resource.publishedAt" v-if="resource.updatedAt || resource.publishedAt"
>{{ >{{
(resource.updatedAt || resource.publishedAt) (resource.updatedAt || resource.publishedAt)

View File

@ -80,10 +80,22 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
avatar { avatar {
id id
url url
name
metadata {
width
height
blurhash
}
} }
banner { banner {
id id
url url
name
metadata {
width
height
blurhash
}
} }
organizedEvents( organizedEvents(
afterDatetime: $afterDateTime afterDatetime: $afterDateTime

View File

@ -41,6 +41,11 @@ export const POST_FRAGMENT = gql`
id id
url url
name name
metadata {
height
width
blurhash
}
} }
} }
${TAG_FRAGMENT} ${TAG_FRAGMENT}

View File

@ -1043,5 +1043,9 @@
"User settings": "User settings", "User settings": "User settings",
"You changed your email or password": "You changed your email or password", "You changed your email or password": "You changed your email or password",
"Organized by you": "Organized by you", "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"
} }

View File

@ -1134,5 +1134,9 @@
"{username} was invited to {group}": "{username} a été invité à {group}", "{username} was invited to {group}": "{username} a été invité à {group}",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap", "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"Organized by you": "Organisé par vous", "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"
} }

View File

@ -7,7 +7,7 @@ import type { IDiscussion } from "../discussions";
import type { IPost } from "../post.model"; import type { IPost } from "../post.model";
import type { IAddress } from "../address.model"; import type { IAddress } from "../address.model";
import { Address } 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 { IMember } from "./member.model";
import type { ITodoList } from "../todolist"; import type { ITodoList } from "../todolist";
import { IActivity } from "../activity.model"; import { IActivity } from "../activity.model";
@ -20,6 +20,7 @@ export interface IGroup extends IActor {
organizedEvents: Paginate<IEvent>; organizedEvents: Paginate<IEvent>;
physicalAddress: IAddress; physicalAddress: IAddress;
openness: Openness; openness: Openness;
visibility: GroupVisibility;
manuallyApprovesFollowers: boolean; manuallyApprovesFollowers: boolean;
activity: Paginate<IActivity>; activity: Paginate<IActivity>;
} }
@ -43,6 +44,7 @@ export class Group extends Actor implements IGroup {
this.patch(hash); this.patch(hash);
} }
visibility: GroupVisibility = GroupVisibility.PUBLIC;
activity: Paginate<IActivity> = { elements: [], total: 0 }; activity: Paginate<IActivity> = { elements: [], total: 0 };
openness: Openness = Openness.INVITE_ONLY; openness: Openness = Openness.INVITE_ONLY;

View File

@ -45,21 +45,145 @@
}} }}
</b-message> </b-message>
<header class="block-container presentation"> <header class="block-container presentation">
<div class="block-column media"> <div class="banner-container">
<div class="media-left"> <lazy-image-wrapper :picture="group.picture" />
</div>
<div class="header">
<div class="avatar-container">
<figure class="image is-128x128" v-if="group.avatar"> <figure class="image is-128x128" v-if="group.avatar">
<img class="is-rounded" :src="group.avatar.url" alt="" /> <img class="is-rounded" :src="group.avatar.url" alt="" />
</figure> </figure>
<b-icon v-else size="is-large" icon="account-group" /> <b-icon v-else size="is-large" icon="account-group" />
</div> </div>
<div class="media-content"> <div class="title-container">
<h1 v-if="group.name">{{ group.name }}</h1> <h1 v-if="group.name">{{ group.name }}</h1>
<b-skeleton v-else :animated="true" /> <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 >@{{ usernameWithDomain(group) }}</small
> >
<b-skeleton v-else :animated="true" /> <b-skeleton v-else :animated="true" />
<br /> <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"> <div class="buttons">
<b-button <b-button
outlined outlined
@ -83,21 +207,55 @@
}" }"
>{{ $t("Group settings") }}</b-button >{{ $t("Group settings") }}</b-button
> >
<b-button
outlined
icon-left="share"
@click="triggerShare()"
v-if="!isCurrentActorAGroupMember"
>
{{ $t("Share") }}
</b-button>
<b-dropdown <b-dropdown
class="menu-dropdown" class="menu-dropdown"
aria-role="list"
v-if="isCurrentActorAGroupMember" v-if="isCurrentActorAGroupMember"
position="is-bottom-left" position="is-bottom-left"
aria-role="menu"
> >
<b-button <b-button
slot="trigger" slot="trigger"
outlined outlined
role="button" role="button"
icon-right="dots-horizontal" icon-left="dots-horizontal"
> aria-label="Other actions"
</b-button> />
<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 <b-dropdown-item
aria-role="listitem" aria-role="menuitem"
v-if="ableToReport" v-if="ableToReport"
@click="isReportModalActive = true" @click="isReportModalActive = true"
> >
@ -107,7 +265,7 @@
</span> </span>
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
aria-role="listitem" aria-role="menuitem"
v-if="isCurrentActorAGroupMember" v-if="isCurrentActorAGroupMember"
@click="leaveGroup" @click="leaveGroup"
> >
@ -116,153 +274,10 @@
{{ $t("Leave") }} {{ $t("Leave") }}
</span> </span>
</b-dropdown-item> </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> </b-dropdown>
</div> </div>
</div> </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> </header>
</div> </div>
<div v-if="isCurrentActorAGroupMember" class="block-container"> <div v-if="isCurrentActorAGroupMember" class="block-container">
@ -285,7 +300,7 @@
:discussion="discussion" :discussion="discussion"
/> />
</div> </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> <p>{{ $t("No discussions yet") }}</p>
</div> </div>
</template> </template>
@ -330,7 +345,7 @@
</div> </div>
<div <div
v-else-if="group" 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> <p>{{ $t("No resources yet") }}</p>
</div> </div>
@ -351,7 +366,7 @@
<div class="block-column"> <div class="block-column">
<!-- Events --> <!-- Events -->
<group-section <group-section
:title="$t('Upcoming events')" :title="$t('Events')"
icon="calendar" icon="calendar"
:privateSection="false" :privateSection="false"
:route="{ :route="{
@ -373,7 +388,7 @@
</div> </div>
<div <div
v-else-if="group" 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> <p>{{ $t("No public upcoming events") }}</p>
</div> </div>
@ -411,7 +426,7 @@
</div> </div>
<div <div
v-else-if="group" 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> <p>{{ $t("No posts yet") }}</p>
</div> </div>
@ -434,71 +449,119 @@
{{ $t("No group found") }} {{ $t("No group found") }}
</b-message> </b-message>
<div v-else class="public-container"> <div v-else class="public-container">
<section> <aside class="group-metadata">
<subtitle>{{ $t("About") }}</subtitle> <div class="sticky">
<div <event-metadata-block
v-html="group.summary" :title="$t('Location')"
v-if="group.summary && group.summary !== '<p></p>'" :icon="
/> physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'
<div v-else-if="group" class="content has-text-grey has-text-centered"> "
<p>{{ $t("This group doesn't have a description yet.") }}</p> >
<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> </div>
</section> </aside>
<section> <div class="main-content">
<subtitle>{{ $t("Upcoming events") }}</subtitle> <section>
<div <subtitle>{{ $t("About") }}</subtitle>
class="organized-events-wrapper" <div
v-if="group && group.organizedEvents.total > 0" v-html="group.summary"
> v-if="group.summary && group.summary !== '<p></p>'"
<EventMinimalistCard
v-for="event in group.organizedEvents.elements"
:event="event"
:key="event.uuid"
class="organized-event"
/> />
</div> <div
<div v-else-if="group"
v-else-if="group && group.organizedEvents.elements.length == 0" class="content has-text-grey-dark has-text-centered"
class="content has-text-grey has-text-centered" >
> <p>{{ $t("This group doesn't have a description yet.") }}</p>
<p>{{ $t("No public upcoming events") }}</p> </div>
</div> </section>
<div v-else-if="group" class="content has-text-grey has-text-centered"> <section>
<p>{{ $t("No public upcoming events") }}</p> <subtitle>{{ $t("Upcoming events") }}</subtitle>
</div> <div
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> class="organized-events-wrapper"
<router-link v-if="group && group.organizedEvents.total > 0"
v-if="group.organizedEvents.total > 0" >
:to="{ <EventMinimalistCard
name: RouteName.GROUP_EVENTS, v-for="event in group.organizedEvents.elements"
params: { preferredUsername: usernameWithDomain(group) }, :event="event"
query: { future: group.organizedEvents.elements.length > 0 }, :key="event.uuid"
}" class="organized-event"
>{{ $t("View all events") }}</router-link />
> </div>
</section> <div
<section> v-else-if="group && group.organizedEvents.elements.length == 0"
<subtitle>{{ $t("Latest posts") }}</subtitle> class="content has-text-grey-dark has-text-centered"
<div v-if="group.posts.total > 0" class="posts-wrapper"> >
<post-list-item <p>{{ $t("No public upcoming events") }}</p>
v-for="post in group.posts.elements" </div>
:key="post.id" <div
:post="post" v-else-if="group"
/> class="content has-text-grey-dark has-text-centered"
</div> >
<div v-else-if="group" class="content has-text-grey has-text-centered"> <p>{{ $t("No public upcoming events") }}</p>
<p>{{ $t("No posts yet") }}</p> </div>
</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.posts.total > 0" :to="{
:to="{ name: RouteName.GROUP_EVENTS,
name: RouteName.POSTS, params: { preferredUsername: usernameWithDomain(group) },
params: { preferredUsername: usernameWithDomain(group) }, query: { future: group.organizedEvents.elements.length > 0 },
}" }"
>{{ $t("View all posts") }}</router-link >{{ $t("View all events") }}</router-link
> >
</section> </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 <b-modal
v-if="physicalAddress && physicalAddress.geom" v-if="physicalAddress && physicalAddress.geom"
:active.sync="showMap" :active.sync="showMap"
@ -526,13 +589,16 @@
@close="$refs.reportModal.close()" @close="$refs.reportModal.close()"
/> />
</b-modal> </b-modal>
<b-modal :active.sync="isShareModalActive" has-modal-card ref="shareModal">
<share-group-modal :group="group" />
</b-modal>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch } from "vue-property-decorator"; import { Component, Prop, Watch } from "vue-property-decorator";
import EventCard from "@/components/Event/EventCard.vue"; 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 Subtitle from "@/components/Utils/Subtitle.vue";
import CompactTodo from "@/components/Todo/CompactTodo.vue"; import CompactTodo from "@/components/Todo/CompactTodo.vue";
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.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 ReportModal from "../../components/Report/ReportModal.vue";
import { PERSON_MEMBERSHIP_GROUP } from "@/graphql/actor"; 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 EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
@Component({ @Component({
apollo: { apollo: {
@ -574,8 +642,14 @@ import { LEAVE_GROUP } from "@/graphql/group";
GroupSection, GroupSection,
Invitations, Invitations,
ReportModal, ReportModal,
LazyImageWrapper,
EventMetadataBlock,
"map-leaflet": () => "map-leaflet": () =>
import(/* webpackChunkName: "map" */ "../../components/Map.vue"), import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
ShareGroupModal: () =>
import(
/* webpackChunkName: "shareGroupModal" */ "../../components/Group/ShareGroupModal.vue"
),
}, },
metaInfo() { metaInfo() {
return { return {
@ -607,6 +681,8 @@ export default class Group extends mixins(GroupMixin) {
isReportModalActive = false; isReportModalActive = false;
isShareModalActive = 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) {
@ -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 { get groupTitle(): undefined | string {
if (!this.group) return undefined; if (!this.group) return undefined;
return this.group.name || this.group.preferredUsername; return this.group.name || this.group.preferredUsername;
@ -822,17 +919,31 @@ export default class Group extends mixins(GroupMixin) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
div.container { div.container {
background: white;
margin-bottom: 3rem; margin-bottom: 3rem;
padding: 2rem 0;
.header, .header,
.public-container { .public-container {
margin: auto 1rem;
display: flex; display: flex;
flex-direction: column; 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 { .block-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -840,8 +951,9 @@ div.container {
&.presentation { &.presentation {
border: 2px solid $purple-2; border: 2px solid $purple-2;
padding: 10px 0; padding: 0 0 10px;
position: relative; position: relative;
flex-direction: column;
h1 { h1 {
color: $purple-1; color: $purple-1;
@ -858,28 +970,16 @@ div.container {
z-index: 2; z-index: 2;
} }
& > img { & > .banner-container {
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 {
display: flex; display: flex;
} justify-content: center;
height: 30vh;
figure:not(:first-child) { ::v-deep img {
margin-left: -10px; width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 50%;
}
} }
} }
@ -925,9 +1025,19 @@ div.container {
.block-column { .block-column {
flex: 1; flex: 1;
margin: 0 1rem; margin: 0 0.5rem;
max-width: 576px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
section { section {
background: $white;
.posts-wrapper { .posts-wrapper {
padding-bottom: 1rem; 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 { .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 { section {
margin-top: 2rem; margin-top: 2rem;
} }

View File

@ -37,10 +37,10 @@
> >
<form @submit.prevent="updateGroup"> <form @submit.prevent="updateGroup">
<b-field :label="$t('Group name')"> <b-field :label="$t('Group name')">
<b-input v-model="group.name" /> <b-input v-model="editableGroup.name" />
</b-field> </b-field>
<b-field :label="$t('Group short description')"> <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>
<b-field :label="$t('Avatar')"> <b-field :label="$t('Avatar')">
<picture-upload <picture-upload
@ -62,7 +62,7 @@
<p class="label">{{ $t("Group visibility") }}</p> <p class="label">{{ $t("Group visibility") }}</p>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.visibility" v-model="editableGroup.visibility"
name="groupVisibility" name="groupVisibility"
:native-value="GroupVisibility.PUBLIC" :native-value="GroupVisibility.PUBLIC"
> >
@ -76,7 +76,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.visibility" v-model="editableGroup.visibility"
name="groupVisibility" name="groupVisibility"
:native-value="GroupVisibility.PRIVATE" :native-value="GroupVisibility.PRIVATE"
>{{ $t("Only accessible through link") }}<br /> >{{ $t("Only accessible through link") }}<br />
@ -110,7 +110,7 @@
<p class="label">{{ $t("New members") }}</p> <p class="label">{{ $t("New members") }}</p>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.openness" v-model="editableGroup.openness"
name="groupOpenness" name="groupOpenness"
:native-value="Openness.OPEN" :native-value="Openness.OPEN"
> >
@ -124,7 +124,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.openness" v-model="editableGroup.openness"
name="groupOpenness" name="groupOpenness"
:native-value="Openness.INVITE_ONLY" :native-value="Openness.INVITE_ONLY"
>{{ $t("Manually invite new members") }}<br /> >{{ $t("Manually invite new members") }}<br />
@ -140,14 +140,14 @@
:label="$t('Followers')" :label="$t('Followers')"
:message="$t('Followers will receive new public events and posts.')" :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") }} {{ $t("Manually approve new followers") }}
</b-checkbox> </b-checkbox>
</b-field> </b-field>
<full-address-auto-complete <full-address-auto-complete
:label="$t('Group address')" :label="$t('Group address')"
v-model="group.physicalAddress" v-model="editableGroup.physicalAddress"
:value="currentAddress" :value="currentAddress"
/> />
@ -171,14 +171,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component } from "vue-property-decorator"; import { Component, Watch } from "vue-property-decorator";
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue"; import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
import { Route } from "vue-router"; import { Route } from "vue-router";
import PictureUpload from "@/components/PictureUpload.vue"; import PictureUpload from "@/components/PictureUpload.vue";
import { mixins } from "vue-class-component"; import { mixins } from "vue-class-component";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { GroupVisibility, Openness } from "@/types/enums"; import { GroupVisibility, Openness } from "@/types/enums";
import RouteName from "../../router/name";
import { UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group"; import { UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
import { IGroup, usernameWithDomain } from "../../types/actor"; import { IGroup, usernameWithDomain } from "../../types/actor";
import { Address, IAddress } from "../../types/address.model"; import { Address, IAddress } from "../../types/address.model";
@ -186,6 +185,7 @@ import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model"; 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";
@Component({ @Component({
components: { components: {
@ -225,9 +225,12 @@ export default class GroupSettings extends mixins(GroupMixin) {
showCopiedTooltip = false; showCopiedTooltip = false;
editableGroup!: IGroup;
async updateGroup(): Promise<void> { async updateGroup(): Promise<void> {
try { try {
const variables = this.buildVariables(); const variables = this.buildVariables();
console.log(variables);
await this.$apollo.mutate<{ updateGroup: IGroup }>({ await this.$apollo.mutate<{ updateGroup: IGroup }>({
mutation: UPDATE_GROUP, mutation: UPDATE_GROUP,
variables, variables,
@ -270,18 +273,26 @@ export default class GroupSettings extends mixins(GroupMixin) {
}, 2000); }, 2000);
} }
@Watch("group")
async watchUpdateGroup(): Promise<void> {
this.editableGroup = { ...this.group };
}
private buildVariables() { private buildVariables() {
let avatarObj = {}; let avatarObj = {};
let bannerObj = {}; let bannerObj = {};
const variables = { ...this.group }; const variables = { ...this.editableGroup };
const physicalAddress = {
...variables.physicalAddress,
};
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
delete variables.__typename; delete variables.__typename;
if (variables.physicalAddress) { if (physicalAddress) {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
delete variables.physicalAddress.__typename; delete physicalAddress.__typename;
} }
delete variables.avatar; delete variables.avatar;
delete variables.banner; delete variables.banner;
@ -291,7 +302,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
avatar: { avatar: {
media: { media: {
name: this.avatarFile.name, name: this.avatarFile.name,
alt: `${this.group.preferredUsername}'s avatar`, alt: `${this.editableGroup.preferredUsername}'s avatar`,
file: this.avatarFile, file: this.avatarFile,
}, },
}, },
@ -303,14 +314,20 @@ export default class GroupSettings extends mixins(GroupMixin) {
banner: { banner: {
media: { media: {
name: this.bannerFile.name, name: this.bannerFile.name,
alt: `${this.group.preferredUsername}'s banner`, alt: `${this.editableGroup.preferredUsername}'s banner`,
file: this.bannerFile, file: this.bannerFile,
}, },
}, },
}; };
} }
return { 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, ...avatarObj,
...bannerObj, ...bannerObj,
}; };
@ -322,7 +339,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
} }
get currentAddress(): IAddress { get currentAddress(): IAddress {
return new Address(this.group.physicalAddress); return new Address(this.editableGroup.physicalAddress);
} }
get avatarMaxSize(): number | undefined { get avatarMaxSize(): number | undefined {

View File

@ -49,7 +49,7 @@
<picture-upload <picture-upload
v-model="pictureFile" v-model="pictureFile"
:textFallback="$t('Headline picture')" :textFallback="$t('Headline picture')"
:defaultImage="post.picture" :defaultImage="editablePost.picture"
/> />
<b-field <b-field
@ -61,21 +61,21 @@
size="is-large" size="is-large"
aria-required="true" aria-required="true"
required required
v-model="post.title" v-model="editablePost.title"
/> />
</b-field> </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"> <div class="field">
<label class="label">{{ $t("Post") }}</label> <label class="label">{{ $t("Post") }}</label>
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p> <p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
<editor v-model="post.body" /> <editor v-model="editablePost.body" />
</div> </div>
<subtitle>{{ $t("Who can view this post") }}</subtitle> <subtitle>{{ $t("Who can view this post") }}</subtitle>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="post.visibility" v-model="editablePost.visibility"
name="postVisibility" name="postVisibility"
:native-value="PostVisibility.PUBLIC" :native-value="PostVisibility.PUBLIC"
>{{ $t("Visible everywhere on the web") }}</b-radio >{{ $t("Visible everywhere on the web") }}</b-radio
@ -83,7 +83,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="post.visibility" v-model="editablePost.visibility"
name="postVisibility" name="postVisibility"
:native-value="PostVisibility.UNLISTED" :native-value="PostVisibility.UNLISTED"
>{{ $t("Only accessible through link") }}</b-radio >{{ $t("Only accessible through link") }}</b-radio
@ -91,7 +91,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="post.visibility" v-model="editablePost.visibility"
name="postVisibility" name="postVisibility"
:native-value="PostVisibility.PRIVATE" :native-value="PostVisibility.PRIVATE"
>{{ $t("Only accessible to members of the group") }}</b-radio >{{ $t("Only accessible to members of the group") }}</b-radio
@ -166,7 +166,7 @@ import {
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import Editor from "../../components/Editor.vue"; 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 TagInput from "../../components/Event/TagInput.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import Subtitle from "../../components/Utils/Subtitle.vue"; import Subtitle from "../../components/Utils/Subtitle.vue";
@ -249,8 +249,6 @@ export default class EditPost extends mixins(GroupMixin) {
tags: [], tags: [],
}; };
group!: IGroup;
PostVisibility = PostVisibility; PostVisibility = PostVisibility;
pictureFile: File | null = null; pictureFile: File | null = null;
@ -259,6 +257,8 @@ export default class EditPost extends mixins(GroupMixin) {
RouteName = RouteName; RouteName = RouteName;
editablePost!: IPost;
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
async mounted(): Promise<void> { async mounted(): Promise<void> {
@ -270,6 +270,7 @@ export default class EditPost extends mixins(GroupMixin) {
if (oldPost.picture !== newPost.picture) { if (oldPost.picture !== newPost.picture) {
this.pictureFile = await buildFileFromIMedia(this.post.picture); this.pictureFile = await buildFileFromIMedia(this.post.picture);
} }
this.editablePost = { ...this.post };
} }
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
@ -280,11 +281,11 @@ export default class EditPost extends mixins(GroupMixin) {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: UPDATE_POST, mutation: UPDATE_POST,
variables: { variables: {
id: this.post.id, id: this.editablePost.id,
title: this.post.title, title: this.editablePost.title,
body: this.post.body, body: this.editablePost.body,
tags: (this.post.tags || []).map(({ title }) => title), tags: (this.editablePost.tags || []).map(({ title }) => title),
visibility: this.post.visibility, visibility: this.editablePost.visibility,
draft, draft,
...(await this.buildPicture()), ...(await this.buildPicture()),
}, },
@ -300,9 +301,9 @@ export default class EditPost extends mixins(GroupMixin) {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: CREATE_POST, mutation: CREATE_POST,
variables: { variables: {
...this.post, ...this.editablePost,
...(await this.buildPicture()), ...(await this.buildPicture()),
tags: (this.post.tags || []).map(({ title }) => title), tags: (this.editablePost.tags || []).map(({ title }) => title),
attributedToId: this.actualGroup.id, attributedToId: this.actualGroup.id,
draft, draft,
}, },
@ -362,16 +363,16 @@ export default class EditPost extends mixins(GroupMixin) {
obj = { ...obj, ...pictureObj }; obj = { ...obj, ...pictureObj };
} }
try { try {
if (this.post.picture && this.pictureFile) { if (this.editablePost.picture && this.pictureFile) {
const oldPictureFile = (await buildFileFromIMedia( const oldPictureFile = (await buildFileFromIMedia(
this.post.picture this.editablePost.picture
)) as File; )) as File;
const oldPictureFileContent = await readFileAsync(oldPictureFile); const oldPictureFileContent = await readFileAsync(oldPictureFile);
const newPictureFileContent = await readFileAsync( const newPictureFileContent = await readFileAsync(
this.pictureFile as File this.pictureFile as File
); );
if (oldPictureFileContent === newPictureFileContent) { if (oldPictureFileContent === newPictureFileContent) {
obj.picture = { mediaId: this.post.picture.id }; obj.picture = { mediaId: this.editablePost.picture.id };
} }
} }
} catch (e) { } catch (e) {

View File

@ -1,61 +1,78 @@
<template> <template>
<div> <article class="container" v-if="post">
<article class="container" v-if="post"> <header>
<section class="heading-section"> <div class="banner-container">
<h1 class="title">{{ post.title }}</h1> <lazy-image
<i18n tag="span" path="By {author}" class="authors"> v-if="post.picture"
<router-link :src="post.picture.url"
slot="author" :width="post.picture.metadata.width"
:to="{ :height="post.picture.metadata.height"
name: RouteName.GROUP, :blurhash="post.picture.metadata.blurhash"
params: { />
preferredUsername: usernameWithDomain(post.attributedTo), </div>
}, <div class="heading-section">
}" <div class="heading-wrapper">
>{{ post.attributedTo.name }}</router-link <div class="title-metadata">
> <h1 class="title">{{ post.title }}</h1>
</i18n> <p class="metadata">
<p class="published has-text-grey-dark" v-if="!post.draft"> <router-link
{{ post.publishAt | formatDateTimeString }} slot="author"
</p> :to="{
<small name: RouteName.GROUP,
v-if="post.visibility === PostVisibility.PRIVATE" params: {
class="has-text-grey-dark" preferredUsername: usernameWithDomain(post.attributedTo),
> },
<b-icon icon="lock" size="is-small" /> }"
{{ >
$t("Accessible only to members", { group: post.attributedTo.name }) <actor-inline :actor="post.attributedTo" />
}} </router-link>
</small> <span class="published has-text-grey-dark" v-if="!post.draft">
<p class="buttons" v-if="isCurrentActorMember"> <b-icon icon="clock" size="is-small" />
<b-tag type="is-warning" size="is-medium" v-if="post.draft">{{ {{ post.publishAt | formatDateTimeString }}
$t("Draft") </span>
}}</b-tag> <span
<router-link v-if="post.visibility === PostVisibility.PRIVATE"
v-if=" class="has-text-grey-dark"
currentActor.id === post.author.id || >
isCurrentActorAGroupModerator <b-icon icon="lock" size="is-small" />
" {{
:to="{ name: RouteName.POST_EDIT, params: { slug: post.slug } }" $t("Accessible only to members", {
tag="button" group: post.attributedTo.name,
class="button is-text" })
>{{ $t("Edit") }}</router-link }}
> </span>
</p> </p>
<img v-if="post.picture" :src="post.picture.url" alt="" /> </div>
</section> <p class="buttons" v-if="isCurrentActorMember">
<section v-html="post.body" class="content" /> <b-tag type="is-warning" size="is-medium" v-if="post.draft">{{
<section class="tags"> $t("Draft")
<router-link }}</b-tag>
v-for="tag in post.tags" <router-link
:key="tag.title" v-if="
:to="{ name: RouteName.TAG, params: { tag: tag.title } }" currentActor.id === post.author.id ||
> isCurrentActorAGroupModerator
<tag>{{ tag.title }}</tag> "
</router-link> :to="{ name: RouteName.POST_EDIT, params: { slug: post.slug } }"
</section> tag="button"
</article> class="button is-text"
</div> >{{ $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> </template>
<script lang="ts"> <script lang="ts">
@ -66,11 +83,12 @@ import { PostVisibility } from "@/types/enums";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { FETCH_POST } from "../../graphql/post"; import { FETCH_POST } from "../../graphql/post";
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import { usernameWithDomain } from "../../types/actor"; import { usernameWithDomain } from "../../types/actor";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import Tag from "../../components/Tag.vue"; import Tag from "../../components/Tag.vue";
import LazyImage from "../../components/Image/LazyImage.vue";
import ActorInline from "../../components/Account/ActorInline.vue";
@Component({ @Component({
apollo: { apollo: {
@ -106,6 +124,8 @@ import Tag from "../../components/Tag.vue";
}, },
components: { components: {
Tag, Tag,
LazyImage,
ActorInline,
}, },
metaInfo() { metaInfo() {
return { return {
@ -148,78 +168,93 @@ export default class Post extends mixins(GroupMixin) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
article { article {
section.heading-section { background: $white !important;
text-align: center; header {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: auto -3rem 2rem; .banner-container {
display: flex;
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 {
justify-content: center; justify-content: center;
} height: 30vh;
& > * {
z-index: 10;
} }
& > img { .heading-section {
position: absolute; position: relative;
left: 0; display: flex;
top: 0; flex-direction: column;
width: 100%; margin-bottom: 2rem;
height: 100%;
opacity: 0.3;
object-fit: cover;
object-position: 50% 50%;
z-index: 0;
}
}
section.content { .heading-wrapper {
font-size: 1.1rem; padding: 15px 10px;
} display: flex;
flex-wrap: wrap;
justify-content: center;
section.tags { .title-metadata {
padding-bottom: 5rem; min-width: 300px;
flex: 20;
a { p.metadata {
text-decoration: none; margin-top: 16px;
} display: flex;
span { justify-content: flex-start;
&.tag { flex-wrap: wrap;
margin: 0 2px;
*: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; margin: 0 auto;
padding: 0 3rem;
} }
</style> </style>