mobilizon.chapril.org-mobil.../js/src/views/Group/GroupView.vue

1289 lines
39 KiB
Vue

<template>
<div class="container mx-auto is-widescreen">
<div class="header flex flex-col">
<breadcrumbs-nav
v-if="group"
:links="[
{ name: RouteName.MY_GROUPS, text: t('My groups') },
{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) },
text: displayName(group),
},
]"
/>
<!-- <o-loading v-model:active="$apollo.loading"></o-loading> -->
<header class="block-container presentation" v-if="group">
<div class="banner-container">
<lazy-image-wrapper :picture="group.banner" />
</div>
<div class="header flex flex-col">
<div class="flex self-center h-0 mt-4 items-end">
<figure class="" v-if="group.avatar">
<img
class="rounded-full border h-32 w-32"
:src="group.avatar.url"
alt=""
width="128"
height="128"
/>
</figure>
<AccountGroup v-else :size="128" />
</div>
<div class="title-container flex flex-1 flex-col text-center">
<h1 class="m-0" v-if="group.name">
{{ group.name }}
</h1>
<!-- <o-skeleton v-else :animated="true" /> -->
<span dir="ltr" class="" v-if="group.preferredUsername"
>@{{ usernameWithDomain(group) }}</span
>
<!-- <o-skeleton v-else :animated="true" /> -->
<br />
</div>
<div class="flex flex-wrap justify-center flex-col md:flex-row">
<div
class="flex flex-col items-center flex-1 m-0"
v-if="isCurrentActorAGroupMember && !previewPublic"
>
<div class="flex gap-1">
<figure
:title="
t(`{'@'}{username} ({role})`, {
username: usernameWithDomain(member.actor),
role: member.role,
})
"
v-for="member in members"
:key="member.actor.id"
>
<img
class="rounded-full"
:src="member.actor.avatar.url"
v-if="member.actor.avatar"
alt=""
width="32"
height="32"
/>
<AccountCircle v-else :size="32" />
</figure>
</div>
<p>
{{
t(
"{count} members",
{
count: group.members.total,
},
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="flex flex-wrap gap-3 justify-center">
<o-button
outlined
icon-left="timeline-text"
v-if="isCurrentActorAGroupMember && !previewPublic"
tag="router-link"
:to="{
name: RouteName.TIMELINE,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ t("Activity") }}</o-button
>
<o-button
outlined
icon-left="cog"
v-if="isCurrentActorAGroupAdmin && !previewPublic"
tag="router-link"
:to="{
name: RouteName.GROUP_PUBLIC_SETTINGS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ t("Group settings") }}</o-button
>
<o-dropdown
aria-role="list"
v-if="showJoinButton && showFollowButton"
>
<template #trigger>
<o-button
variant="primary"
icon-left="rss"
icon-right="menu-down"
>
{{ t("Follow") }}
</o-button>
</template>
<o-dropdown-item
aria-role="listitem"
class="p-0"
custom
:focusable="false"
:disabled="
isCurrentActorPendingFollow &&
currentActor?.id !== undefined
"
>
<button
class="flex gap-1 text-start py-4 px-2 w-full"
@click="followGroup"
>
<RSS />
<div class="pl-2">
<h3 class="font-medium text-lg">{{ t("Follow") }}</h3>
<p class="whitespace-normal md:whitespace-nowrap text-sm">
{{ t("Get informed of the upcoming public events") }}
</p>
<p
v-if="
doesGroupManuallyApprovesFollowers &&
!isCurrentActorPendingFollow
"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{
t(
"Follow requests will be approved by a group moderator"
)
}}
</p>
<p
v-if="isCurrentActorPendingFollow && currentActor?.id"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ t("Follow request pending approval") }}
</p>
</div>
</button>
</o-dropdown-item>
<o-dropdown-item
aria-role="listitem"
class="p-0 border-t border-solid"
custom
:focusable="false"
:disabled="
isGroupInviteOnly || isCurrentActorAPendingGroupMember
"
>
<button
class="flex gap-1 text-start py-4 px-2 w-full"
@click="joinGroup"
>
<AccountMultiplePlus />
<div class="pl-2">
<h3 class="font-medium text-lg">{{ t("Join") }}</h3>
<div v-if="showJoinButton">
<p
class="whitespace-normal md:whitespace-nowrap text-sm"
>
{{
t(
"Become part of the community and start organizing events"
)
}}
</p>
<p
v-if="isGroupInviteOnly"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ t("This group is invite-only") }}
</p>
<p
v-if="
areGroupMembershipsModerated &&
!isCurrentActorAPendingGroupMember
"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{
t(
"Membership requests will be approved by a group moderator"
)
}}
</p>
<p
v-if="isCurrentActorAPendingGroupMember"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ t("Your membership is pending approval") }}
</p>
</div>
</div>
</button>
</o-dropdown-item>
</o-dropdown>
<o-button
outlined
v-if="isCurrentActorAPendingGroupMember"
@click="leaveGroup"
@keyup.enter="leaveGroup"
variant="primary"
>{{ t("Cancel membership request") }}</o-button
>
<o-button
outlined
v-if="isCurrentActorPendingFollow && currentActor?.id"
@click="unFollowGroup"
@keyup.enter="unFollowGroup"
variant="primary"
>{{ t("Cancel follow request") }}</o-button
><o-button
v-if="
isCurrentActorFollowing && !previewPublic && currentActor?.id
"
variant="primary"
@click="unFollowGroup"
>{{ t("Unfollow") }}</o-button
>
<o-button
v-if="isCurrentActorFollowing"
@click="toggleFollowNotify"
@keyup.enter="toggleFollowNotify"
class="notification-button p-1.5"
outlined
:icon-left="
isCurrentActorFollowingNotify
? 'bell-outline'
: 'bell-off-outline'
"
>
<span class="sr-only">{{
isCurrentActorFollowingNotify
? t("Activate notifications")
: t("Deactivate notifications")
}}</span>
</o-button>
<o-button
outlined
icon-left="share"
@click="triggerShare()"
@keyup.enter="triggerShare()"
v-if="!isCurrentActorAGroupMember || previewPublic"
>
{{ t("Share") }}
</o-button>
<o-dropdown aria-role="list">
<template #trigger>
<o-button
outlined
icon-left="dots-horizontal"
:aria-label="t('Other actions')"
></o-button>
</template>
<o-dropdown-item
aria-role="menuitem"
v-if="isCurrentActorAGroupMember || previewPublic"
>
<o-switch v-model="previewPublic">{{
t("Public preview")
}}</o-switch>
</o-dropdown-item>
<o-dropdown-item
v-if="!previewPublic && isCurrentActorAGroupMember"
aria-role="menuitem"
@click="triggerShare()"
@keyup.enter="triggerShare()"
>
<span class="inline-flex gap-1">
<Share />
{{ t("Share") }}
</span>
</o-dropdown-item>
<hr
role="presentation"
class="dropdown-divider"
v-if="isCurrentActorAGroupMember"
/>
<o-dropdown-item has-link aria-role="menuitem">
<a
:href="`@${preferredUsername}/feed/atom`"
:title="t('Atom feed for events and posts')"
class="inline-flex gap-1"
>
<RSS />
{{ t("RSS/Atom Feed") }}
</a>
</o-dropdown-item>
<o-dropdown-item has-link aria-role="menuitem">
<a
:href="`@${preferredUsername}/feed/ics`"
:title="t('ICS feed for events')"
class="inline-flex gap-1"
>
<CalendarSync />
{{ t("ICS/WebCal Feed") }}
</a>
</o-dropdown-item>
<hr role="presentation" class="dropdown-divider" />
<o-dropdown-item
v-if="ableToReport"
aria-role="menuitem"
@click="isReportModalActive = true"
@keyup.enter="isReportModalActive = true"
>
<span class="inline-flex gap-1">
<Flag />
{{ t("Report") }}
</span>
</o-dropdown-item>
<o-dropdown-item
aria-role="menuitem"
v-if="isCurrentActorAGroupMember && !previewPublic"
@click="openLeaveGroupModal"
@keyup.enter="openLeaveGroupModal"
>
<span class="inline-flex gap-1">
<ExitToApp />
{{ t("Leave") }}
</span>
</o-dropdown-item>
</o-dropdown>
</div>
</div>
<InvitationsList
v-if="
isCurrentActorAnInvitedGroupMember && groupMember !== undefined
"
:invitations="[groupMember]"
/>
<o-notification
v-if="isCurrentActorARejectedGroupMember"
variant="danger"
>
{{ t("You have been removed from this group's members.") }}
</o-notification>
<o-notification
v-if="
isCurrentActorAGroupMember &&
isCurrentActorARecentMember &&
isCurrentActorOnADifferentDomainThanGroup
"
variant="info"
>
{{
t(
"Since you are a new member, private content can take a few minutes to appear."
)
}}
</o-notification>
</div>
</header>
</div>
<div
v-if="isCurrentActorAGroupMember && !previewPublic && group"
class="block-container flex gap-2 flex-wrap mt-3"
>
<!-- Private things -->
<div class="flex-1 m-0 flex flex-col flex-wrap gap-2">
<!-- Group discussions -->
<Discussions :group="group" class="flex-1" />
<!-- Resources -->
<Resources :group="group" class="flex-1" />
</div>
<!-- Public things -->
<div class="flex-1 m-0 flex flex-col flex-wrap gap-2">
<!-- Events -->
<Events
:group="group"
:isModerator="isCurrentActorAGroupModerator"
class="flex-1"
/>
<!-- Posts -->
<Posts
:group="group"
:isModerator="isCurrentActorAGroupModerator"
:isMember="isCurrentActorAGroupMember"
class="flex-1"
/>
</div>
</div>
<o-notification
v-else-if="!group && groupLoading === false"
variant="danger"
>
{{ t("No group found") }}
</o-notification>
<div v-else-if="group" class="public-container flex flex-col">
<aside class="group-metadata">
<div class="sticky">
<o-notification v-if="group.domain && !isCurrentActorAGroupMember">
<p>
{{
t(
"This profile is from another instance, the informations shown here may be incomplete."
)
}}
</p>
<o-button
variant="text"
tag="a"
:href="group.url"
rel="noopener noreferrer external"
>{{ t("View full profile") }}</o-button
>
</o-notification>
<event-metadata-block
:title="t('About')"
v-if="group.summary && group.summary !== '<p></p>'"
>
<div
dir="auto"
class="prose lg:prose-xl dark:prose-invert"
v-html="group.summary"
/>
</event-metadata-block>
<event-metadata-block :title="t('Members')">
<template #icon>
<AccountGroup :size="48" />
</template>
{{
t(
"{count} members",
{
count: group.members.total,
},
group.members.total
)
}}
</event-metadata-block>
<event-metadata-block
v-if="physicalAddress && physicalAddress.url"
:title="t('Location')"
>
<template #icon>
<o-icon
v-if="physicalAddress.poiInfos.poiIcon.icon"
:icon="physicalAddress.poiInfos.poiIcon.icon"
customSize="48"
/>
<Earth v-else :size="48" />
</template>
<div class="address-wrapper">
<span
v-if="!physicalAddress || !addressFullName(physicalAddress)"
>{{ t("No address defined") }}</span
>
<div class="address" v-if="physicalAddress">
<div>
<address dir="auto">
<p
class="addressDescription"
:title="physicalAddress.poiInfos.name"
>
{{ physicalAddress.poiInfos.name }}
</p>
<p class="has-text-grey-dark">
{{ physicalAddress.poiInfos.alternativeName }}
</p>
</address>
</div>
<o-button
class="map-show-button"
variant="text"
@click="showMap = !showMap"
@keyup.enter="showMap = !showMap"
v-if="physicalAddress.geom"
>
{{ t("Show map") }}
</o-button>
</div>
</div>
</event-metadata-block>
</div>
</aside>
<div class="main-content min-w-min flex-auto py-0 px-2">
<section>
<h2 class="text-2xl font-bold">{{ t("Upcoming events") }}</h2>
<div
class="flex flex-col gap-3"
v-if="group && organizedEvents.elements.length > 0"
>
<event-minimalist-card
v-for="event in organizedEvents.elements.slice(0, 3)"
:event="event"
:key="event.uuid"
class="organized-event"
/>
</div>
<empty-content
v-else-if="group"
icon="calendar"
:inline="true"
description-classes="flex flex-col items-stretch"
>
{{ t("No public upcoming events") }}
<template #desc>
<template v-if="isCurrentActorFollowing">
<i18n-t
keypath="You will receive notifications about this group's public activity depending on %{notification_settings}."
>
<template #notification_settings>
<router-link :to="{ name: RouteName.NOTIFICATIONS }">{{
t("your notification settings")
}}</router-link>
</template>
</i18n-t>
</template>
<o-button
tag="router-link"
class="my-2 self-center"
variant="text"
:to="{
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
query: { showPassedEvents: true },
}"
>{{ t("View past events") }}</o-button
>
</template>
</empty-content>
<!-- <o-skeleton animated v-else-if="$apollo.loading"></o-skeleton> -->
<div class="flex justify-center">
<o-button
tag="router-link"
class="my-4"
variant="text"
v-if="organizedEvents.total > 0"
:to="{
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
query: {
showPassedEvents: organizedEvents.elements.length === 0,
},
}"
>{{ t("View all events") }}</o-button
>
</div>
</section>
<section class="flex flex-col items-stretch">
<h2 class="ml-0 text-2xl font-bold">{{ t("Latest posts") }}</h2>
<multi-post-list-item
v-if="
posts.elements.filter(
(post) =>
!post.draft && post.visibility === PostVisibility.PUBLIC
).length > 0
"
:posts="
posts.elements.filter(
(post) =>
!post.draft && post.visibility === PostVisibility.PUBLIC
)
"
/>
<empty-content v-else-if="group" icon="bullhorn" :inline="true">
{{ t("No posts yet") }}
</empty-content>
<!-- <o-skeleton animated v-else-if="$apollo.loading"></o-skeleton> -->
<o-button
class="self-center my-2"
v-if="posts.total > 0"
tag="router-link"
variant="text"
:to="{
name: RouteName.POSTS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ t("View all posts") }}</o-button
>
</section>
</div>
<o-modal
v-if="physicalAddress && physicalAddress.geom"
v-model:active="showMap"
:close-button-aria-label="t('Close')"
>
<div class="map">
<map-leaflet
:coords="physicalAddress.geom"
:marker="{
text: physicalAddress.fullName,
icon: physicalAddress.poiInfos.poiIcon.icon,
}"
/>
</div>
</o-modal>
</div>
<o-modal v-if="group" v-model:active="isReportModalActive">
<report-modal
ref="reportModalRef"
:on-confirm="reportGroup"
:title="t('Report this group')"
:outside-domain="group.domain"
@close="isReportModalActive = false"
/>
</o-modal>
<o-modal v-model:active="isShareModalActive" v-if="group">
<ShareGroupModal :group="group" />
</o-modal>
</div>
</template>
<script lang="ts" setup>
// import EventCard from "@/components/Event/EventCard.vue";
import {
displayName,
IActor,
IFollower,
IPerson,
usernameWithDomain,
} from "@/types/actor";
// import CompactTodo from "@/components/Todo/CompactTodo.vue";
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
import MultiPostListItem from "@/components/Post/MultiPostListItem.vue";
import { Address, addressFullName } from "@/types/address.model";
import InvitationsList from "@/components/Group/InvitationsList.vue";
import addMinutes from "date-fns/addMinutes";
import { JOIN_GROUP } from "@/graphql/member";
import { MemberRole, Openness, PostVisibility } from "@/types/enums";
import { IMember } from "@/types/actor/member.model";
import RouteName from "../../router/name";
import ReportModal from "@/components/Report/ReportModal.vue";
import {
GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED,
PERSON_STATUS_GROUP,
} from "@/graphql/actor";
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { Paginate } from "@/types/paginate";
import { IEvent } from "@/types/event.model";
import { IPost } from "@/types/post.model";
import { FOLLOW_GROUP, UNFOLLOW_GROUP, UPDATE_GROUP_FOLLOW } from "@/graphql/followers";
import { useAnonymousReportsConfig } from "../../composition/apollo/config";
import { computed, defineAsyncComponent, inject, ref, watch } from "vue";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useGroup, useLeaveGroup } from "@/composition/apollo/group";
import { useRouter } from "vue-router";
import { useMutation, useQuery } from "@vue/apollo-composable";
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import RSS from "vue-material-design-icons/Rss.vue";
import Share from "vue-material-design-icons/Share.vue";
import CalendarSync from "vue-material-design-icons/CalendarSync.vue";
import Flag from "vue-material-design-icons/Flag.vue";
import ExitToApp from "vue-material-design-icons/ExitToApp.vue";
import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.vue";
import Earth from "vue-material-design-icons/Earth.vue";
import { useI18n } from "vue-i18n";
import { useCreateReport } from "@/composition/apollo/report";
import { useHead } from "@vueuse/head";
import Discussions from "@/components/Group/Sections/DiscussionsSection.vue";
import Resources from "@/components/Group/Sections/ResourcesSection.vue";
import Posts from "@/components/Group/Sections/PostsSection.vue";
import Events from "@/components/Group/Sections/EventsSection.vue";
import { Dialog } from "@/plugins/dialog";
import { Notifier } from "@/plugins/notifier";
const props = defineProps<{
preferredUsername: string;
}>();
const { anonymousReportsConfig } = useAnonymousReportsConfig();
const { currentActor } = useCurrentActorClient();
const {
group,
loading: groupLoading,
refetch: refetchGroup,
} = useGroup(props.preferredUsername, { afterDateTime: new Date() });
const router = useRouter();
const { t } = useI18n({ useScope: "global" });
// const { person } = usePersonStatusGroup(group);
const { result, subscribeToMore } = useQuery<{
person: IPerson;
}>(
PERSON_STATUS_GROUP,
() => ({
id: currentActor.value?.id,
group: usernameWithDomain(group.value),
}),
() => ({
enabled:
currentActor.value?.id !== undefined &&
currentActor.value?.id !== null &&
group.value?.preferredUsername !== undefined &&
usernameWithDomain(group.value) !== "",
})
);
subscribeToMore<{ actorId: string; group: string }>({
document: GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED,
variables: {
actorId: currentActor.value?.id as string,
group: usernameWithDomain(group.value),
},
});
const person = computed(() => result.value?.person);
const MapLeaflet = defineAsyncComponent(
() => import("@/components/LeafletMap.vue")
);
const ShareGroupModal = defineAsyncComponent(
() => import("@/components/Group/ShareGroupModal.vue")
);
const showMap = ref(false);
const isReportModalActive = ref(false);
const reportModalRef = ref();
const isShareModalActive = ref(false);
const previewPublic = ref(false);
const notifier = inject<Notifier>("notifier");
watch(
currentActor,
(watchedCurrentActor: IActor | undefined, oldActor: IActor | undefined) => {
if (
watchedCurrentActor?.id &&
oldActor &&
watchedCurrentActor?.id !== oldActor.id
) {
refetchGroup();
}
}
);
const { mutate: joinGroupMutation, onError: onJoinGroupError } =
useMutation(JOIN_GROUP);
const joinGroup = async (): Promise<void> => {
if (!currentActor.value?.id) {
router.push({
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(group.value) },
});
return;
}
const [groupUsername, currentActorId] = [
usernameWithDomain(group.value),
currentActor.value?.id,
];
joinGroupMutation(
{
groupId: group.value?.id,
},
{
refetchQueries: [
{
query: PERSON_STATUS_GROUP,
variables: {
id: currentActorId,
group: groupUsername,
},
},
],
}
);
onJoinGroupError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
notifier?.error(error.graphQLErrors[0].message);
}
});
};
const dialog = inject<Dialog>("dialog");
const openLeaveGroupModal = async (): Promise<void> => {
dialog?.confirm({
variant: "danger",
title: t("Leave group"),
message: t(
"Are you sure you want to leave the group {groupName}? You'll loose access to this group's private content. This action cannot be undone.",
{ groupName: `<b>${displayName(group.value)}</b>` }
),
onConfirm: leaveGroup,
confirmText: t("Leave group"),
cancelText: t("Cancel"),
});
};
const {
mutate: leaveGroupMutation,
onError: onLeaveGroupError,
onDone: onLeaveGroupDone,
} = useLeaveGroup();
const leaveGroup = () => {
console.debug("called leaveGroup");
const [groupFederatedUsername, currentActorId] = [
usernameWithDomain(group.value),
currentActor.value?.id,
];
leaveGroupMutation(
{
groupId: group.value?.id,
},
{
refetchQueries: [
{
query: PERSON_STATUS_GROUP,
variables: {
id: currentActorId,
group: groupFederatedUsername,
},
},
],
}
);
};
onLeaveGroupError((error: any) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
notifier?.error(error.graphQLErrors[0].message);
}
});
onLeaveGroupDone(() => {
console.debug("done");
});
const { mutate: followGroupMutation, onError: onFollowGroupError } =
useMutation(FOLLOW_GROUP, () => ({
refetchQueries: [
{
query: PERSON_STATUS_GROUP,
variables: {
id: currentActor.value?.id,
group: usernameWithDomain(group.value),
},
},
],
}));
onFollowGroupError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
notifier?.error(error.graphQLErrors[0].message);
}
});
const followGroup = async (): Promise<void> => {
if (!currentActor.value?.id) {
router.push({
name: RouteName.GROUP_FOLLOW,
params: {
preferredUsername: usernameWithDomain(group.value),
},
});
return;
}
followGroupMutation({
groupId: group.value?.id,
});
};
const { mutate: unfollowGroupMutation, onError: onUnfollowGroupError } =
useMutation(UNFOLLOW_GROUP, () => ({
refetchQueries: [
{
query: PERSON_STATUS_GROUP,
variables: {
id: currentActor.value?.id,
group: usernameWithDomain(group.value),
},
},
],
}));
onUnfollowGroupError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
notifier?.error(error.graphQLErrors[0].message);
}
});
const unFollowGroup = async (): Promise<void> => {
console.debug("unfollow group");
unfollowGroupMutation({
groupId: group.value?.id,
});
};
const { mutate: updateGroupFollowMutation } = useMutation(UPDATE_GROUP_FOLLOW);
const toggleFollowNotify = () => {
updateGroupFollowMutation({
followId: currentActorFollow.value?.id,
notify: !isCurrentActorFollowingNotify.value,
});
};
const {
mutate: createReportMutation,
onError: onCreateReportError,
onDone: onCreateReportDone,
} = useCreateReport();
const reportGroup = (content: string, forward: boolean) => {
isReportModalActive.value = false;
console.debug("report group", {
reportedId: group.value?.id ?? "",
content,
forward,
});
createReportMutation({
reportedId: group.value?.id ?? "",
content,
forward,
});
};
onCreateReportDone(() => {
notifier?.success(
t("Group {groupTitle} reported", { groupTitle: groupTitle.value })
);
});
onCreateReportError((error: any) => {
console.error(error);
notifier?.error(
t("Error while reporting group {groupTitle}", {
groupTitle: groupTitle.value,
})
);
});
const triggerShare = (): void => {
if (navigator.share) {
navigator
.share({
title: displayName(group.value),
url: group.value?.url,
})
.then(() => console.debug("Successful share"))
.catch((error: any) => console.debug("Error sharing", error));
} else {
isShareModalActive.value = true;
// send popup
}
};
const groupTitle = computed((): undefined | string => {
return displayName(group.value);
});
const groupSummary = computed((): undefined | string => {
return group.value?.summary;
});
useHead({
title: computed(() => groupTitle.value ?? ""),
meta: [{ name: "description", content: computed(() => groupSummary.value) }],
});
const personMemberships = computed(
() => person.value?.memberships ?? { total: 0, elements: [] }
);
const groupMember = computed((): IMember | undefined => {
if (personMemberships.value?.total > 0) {
return personMemberships.value?.elements[0];
}
return undefined;
});
const isCurrentActorARejectedGroupMember = computed((): boolean => {
return personMemberships.value.elements
.filter((membership) => membership.role === MemberRole.REJECTED)
.map(({ parent: { id } }) => id)
.includes(group.value?.id);
});
const isCurrentActorAnInvitedGroupMember = computed((): boolean => {
return personMemberships.value.elements
.filter((membership) => membership.role === MemberRole.INVITED)
.map(({ parent: { id } }) => id)
.includes(group.value?.id);
});
/**
* New members, if on a different server,
* can take a while to refresh the group and fetch all private data
*/
const isCurrentActorARecentMember = computed((): boolean => {
return (
groupMember.value !== undefined &&
groupMember.value?.role === MemberRole.MEMBER &&
addMinutes(new Date(`${groupMember.value?.updatedAt}Z`), 10) > new Date()
);
});
const isCurrentActorOnADifferentDomainThanGroup = computed((): boolean => {
return group.value?.domain !== null;
});
const members = computed((): IMember[] => {
return (
group.value?.members.elements.filter(
(member: IMember) =>
![
MemberRole.INVITED,
MemberRole.REJECTED,
MemberRole.NOT_APPROVED,
].includes(member.role)
) ?? []
);
});
const physicalAddress = computed((): Address | null => {
if (!group.value?.physicalAddress) return null;
return new Address(group.value?.physicalAddress);
});
const ableToReport = computed((): boolean => {
return anonymousReportsConfig.value?.allowed === true;
});
const organizedEvents = computed((): Paginate<IEvent> => {
return {
total: group.value?.organizedEvents.total ?? 0,
elements:
group.value?.organizedEvents.elements.filter((event: IEvent) => {
if (previewPublic.value) {
return !event.draft; // TODO when events get visibility access add visibility constraint like below for posts
}
return true;
}) ?? [],
};
});
const posts = computed((): Paginate<IPost> => {
return {
total: group.value?.posts.total ?? 0,
elements:
group.value?.posts.elements.filter((post: IPost) => {
if (previewPublic.value || !isCurrentActorAGroupMember.value) {
return !post.draft && post.visibility == PostVisibility.PUBLIC;
}
return true;
}) ?? [],
};
});
const showFollowButton = computed((): boolean => {
return !isCurrentActorFollowing.value || previewPublic.value;
});
const showJoinButton = computed((): boolean => {
return !isCurrentActorAGroupMember.value || previewPublic.value;
});
const isGroupInviteOnly = computed((): boolean => {
return (
(!isCurrentActorAGroupMember.value || previewPublic) &&
group.value?.openness === Openness.INVITE_ONLY
);
});
const areGroupMembershipsModerated = computed((): boolean => {
return (
(!isCurrentActorAGroupMember.value || previewPublic) &&
group.value?.openness === Openness.MODERATED
);
});
const doesGroupManuallyApprovesFollowers = computed((): boolean | undefined => {
return (
(!isCurrentActorAGroupMember.value || previewPublic) &&
group.value?.manuallyApprovesFollowers
);
});
const isCurrentActorAGroupAdmin = computed((): boolean => {
return hasCurrentActorThisRole(MemberRole.ADMINISTRATOR);
});
const isCurrentActorAGroupModerator = computed((): boolean => {
return hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
});
const isCurrentActorAGroupMember = computed((): boolean => {
return hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
MemberRole.MEMBER,
]);
});
const isCurrentActorAPendingGroupMember = computed((): boolean => {
return hasCurrentActorThisRole([MemberRole.NOT_APPROVED]);
});
const currentActorFollow = computed((): IFollower | undefined => {
if (person?.value?.follows?.total && person?.value?.follows?.total > 0) {
return person?.value?.follows?.elements[0];
}
return undefined;
});
const isCurrentActorFollowing = computed((): boolean => {
return currentActorFollow.value?.approved === true;
});
const isCurrentActorPendingFollow = computed((): boolean => {
return currentActorFollow.value?.approved === false;
});
const isCurrentActorFollowingNotify = computed((): boolean => {
return (
isCurrentActorFollowing.value && currentActorFollow.value?.notify === true
);
});
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
return (
personMemberships.value?.total > 0 &&
roles.includes(personMemberships.value?.elements[0].role)
);
};
watch(isCurrentActorAGroupMember, () => {
refetchGroup();
});
</script>
<style lang="scss" scoped>
@use "@/styles/_mixins" as *;
div.container {
.block-container {
display: flex;
flex-wrap: wrap;
margin-top: 15px;
&.presentation {
padding: 0 0 10px;
position: relative;
flex-direction: column;
& > *:not(img) {
position: relative;
z-index: 2;
}
& > .banner-container {
display: flex;
justify-content: center;
height: 30vh;
:deep(img) {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 50%;
}
}
}
div.address {
flex: 1;
text-align: right;
justify-content: flex-end;
display: flex;
.map-show-button {
cursor: pointer;
}
address {
font-style: normal;
span.addressDescription {
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 0 auto;
min-width: 100%;
max-width: 4rem;
overflow: hidden;
}
:not(.addressDescription) {
color: rgba(46, 62, 72, 0.6);
flex: 1;
min-width: 100%;
}
}
}
.header {
display: flex;
flex-wrap: wrap;
justify-content: center;
flex-direction: column;
flex: 1;
margin: 0;
align-items: center;
.group-metadata {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
.members {
div {
display: flex;
}
figure:not(:first-child) {
@include margin-left(-10px);
}
}
}
}
}
.public-container {
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
padding: 0;
margin-top: 1rem;
.group-metadata {
min-width: 20rem;
flex: 1;
// @include padding-left(1rem);
// @include mobile {
// @include padding-left(0);
// }
.sticky {
position: sticky;
// background: white;
top: 50px;
padding: 1rem;
}
}
section {
margin-top: 0;
}
}
}
.map {
height: 60vh;
width: 100%;
}
</style>